A Comprehensive Example of a Spring MVC Application - Part 3

The Web Layer

Designing the Application REST API

When designing the REST API of an application I try to adhere to the "REST rules". For example - use only nouns in the URI and not verbs, use identifiers in the URI to reach a specific entity and use the HTTP verbs correctly. In addition, as a standard I like to use all nouns in plural form to simplify the URIs - avoid, for example, /person/{id} to reach a specific person and /persons to get a list of all of them.

 

The URIs we'll want to implement for the example application are:

Verb

URI

Description

GET

/persons

Get a list of all the persons defined in the system (yes, it's a little naïve, and in a real application you would probably use some way to filter or at least page the result)

GET

/persons/{id}

Get the details of a specific person by id

POST

/persons

Create a new person in the system. The body contains the person details

DELETE

/persons/{id}

Delete the person with given id

 

To complicated things (just a little) we also want to get the list of all persons (GET /persons) in 4 different media types (formats):

  • application/json
  • application/vnd.ms-excel (a.k.a Microsoft Excel sheet)
  • text/csv (a.k.a comma separated values)
  • text/html

 

Implementing the Web Layer

The implementation here is somewhat more complex because of the different formats requirement. To do this we need to define both a content negotiation view resolver and view renderers for each of the specific media types. But let's do this step by step.

 

Spring MVC provides a Model-View-Controller framework. This framework defines several key components (beans) that handle requests end to end - from their instantiation until the response is returned. Among other things Spring MVC provides means to handle themes, locales, different views, etc.

 

Implementing a REST API with Spring MVC is very convenient, though does not conform to JAX-RS. This is the only down side I find in the Spring MVC solution for REST, and it's not that much of a down side…

 

So, as mentioned above we want a REST API and we want to be able to render different views according to the media type. Spring MVC supports 2 ways to generate the response:

  1. Using message converters
  2. Using views

 

Message converters, as the name suggests, simply convert the request/response from/to an object. There are several out of the box message converters - some support both ways, some support only converting to a response. The most useful message converters are the mapping jackson2 message converter which converts objects from/to json and the marshalling message converter which converts objects from/to XML. Message converts convert from objects to their target media (and optionally back) in a generic way - potentially any object could be converted. There is no special rendering done for a specific response.

 

Views are means to render specific responses to specific view representations. Spring MVC provides several views which are really just adapters to existing view rendering technologies such as JSP/JSTL, excel, jasper reports, RSS/ATOM feeds, velocity, freemarker and others. If your favorite view technology is not supported you can always write your own view - an example for a CSV view renderer will follow.

 

However, to get from a response to the desired view the framework has to resolve your view. For that Spring MVC provides view resolvers. Again, the framework supplies several useful view resolvers such as the internal resource view resolver which resolves to a resources internal to the web application such as a JSP file, and the bean name view resolver which resolves to a spring bean by its name.

 

In the example application we need to use both ways to render our responses. We'll use the MappingJackson2HttpMessageConverter to generate JSON, the StringHttpMessageConverter to generate simple strings (from the exception handler - see below), the InternalResourceViewResolver to render html responses with JSP/JSTL and finally the BeanNameViewResolver for all other formats (CSV, excel).

 

Finally - we need a way to identify the view renderer according to the requested media type, but in such a way that the code in the controller will not be aware of it, or as least aware as possible. No view related code should ever exist in the controller itself - just model manipulation. Spring MVC does not come out of the box with means to do this directly, but there is a very simple way to implement it ourselves. I used the RequestToViewNameTranslator - this is a special bean that resolves the view name out of the request. By default the view name is resolved by removing the URI path from the request and removing the media type. So, for example, if the request is made to http://host:port/context/servlet/some/path/in/the/app.html then by default the view will be resolved to "app" - removing the path and the .html suffix. My solution adds to this default behavior and appends the media type to the resolved view name, except when resolving to JSPs because the JSPs use the view name as the name of the JSP file.

 

And here're the details of the implementation. The controller itself:

@Controller
public final class PersonController {
    @Autowired
    private PersonService personService;

    @RequestMapping(value = "/persons", produces = "application/json")
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public List<Person> getPersons() {
        return personService.readAllPersons();
    }

    @RequestMapping(value = "/persons/{id:[\\p{Digit}]+}", produces = "application/json")
    @ResponseBody
    public ResponseEntity<Person> getPerson(@PathVariable long id) {
        Person p = personService.readPerson(id);

        if (p == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }

        return new ResponseEntity<>(p, HttpStatus.OK);
    }

    @RequestMapping(value = "/persons", produces = {"text/html", "application/vnd.ms-excel", "text/csv"})
    @ResponseStatus(HttpStatus.OK)
    public Model personsWithView(Model model) {
        return model.addAttribute("persons", getPersons());
    }

    @RequestMapping(value = "/persons", produces = "application/json", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public long createPerson(@RequestBody @Valid Person person) {
        return personService.createPerson(person.getFirstName(), person.getLastName());
    }

    @RequestMapping(value = "/persons/{id:[\\p{Digit}]+}", produces = "application/json", method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.OK)
    public void deletePerson(@PathVariable long id) {
        personService.deletePerson(id);
    }

    @ExceptionHandler(IOException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public String exceptionHandler() {
        return "f^@k";
    }
}

 

Notes for the "advanced features" which I'll explain in details later:

  • Notice the usage of regular expressions in the URIs
  • Notice the usage of the exception handler
  • Notice the usage of @Valid in the createPerson method

 

But first the basics. In the above implementations each method handles a specific REST verb. let's take a closer look at the GET /persons:

    @RequestMapping(value = "/persons", produces = "application/json")
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public List<Person> getPersons() {
        return personService.readAllPersons();
    }

 

In the @RequestMapping annotation we define the URI and the fact that it produces application/json. In addition we have the @ResponseBody annotation which tells Spring that the return value of this method is the response. The result is that the return value is passed through a message converter that is able to handle the produced media type. In this case this would be the mapping jackson2 converter - which uses Jackson (http://jackson.codehaus.org/) as a JSON processor.

 

Next we have the @ResponseStatus(OK) which tells spring to set the HTTP status to 200 if the method exists normally.

 

The method itself is very simple - just delegate to the service. In this case there's no need to convert any inputs to the service layer because there are no inputs. The conversion back to the http response is done by spring with its message converters.

 

Now let's examine the GET /persons/{id}:

    @RequestMapping(value = "/persons/{id:[\\p{Digit}]+}", produces = "application/json")
    @ResponseBody
    public ResponseEntity<Person> getPerson(@PathVariable long id) {
        Person p = personService.readPerson(id);

        if (p == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }

        return new ResponseEntity<>(p, HttpStatus.OK);
    }

 

This is somewhat more complex - here we have 2 conversions - first the id accepted from the request - the inbound conversion, and second the response from the service - the outbound conversion.

 

In the @RequestMapping we can specify placeholders or "path variables" - these are templates from which we can extract arguments and pass them to the method, using Spring MVC to convert them to the requested data type.

 

The id in the method signature is a strongly typed long. Spring extracts it from the path using the @PathVariable annotation. The matching is done by the argument name. if you want you can add the name explicitly in the annotation, but if you're compiling (like everyone else because this is the default) with debug information in your code then Spring can take the name directly from the argument.

 

In addition, in the above example, we are also using a regular expression in the path variable definition. This ensures that only digits are accepted in the id field in the path. Anything else will automatically generate an http bad request response status (400).

 

The method itself again delegates the business logic to the service layer. However, here we also have a little more complex response conversion. Here if there is no such person with the requested id we want to return an http response status not found - 404. To do this we declare that our method getPerson returns a ResponseEntity<> - this is a generic class wrapping our response - Person - with the ability to affect the http response. We could add headers, and set the http status code. In this example we are just changing the http response status code. If the Person was found we return (in addition to the person itself) the status OK (200) and if not we return the status not found (404).

 

Moving on to the POST /persons:

    @RequestMapping(value = "/persons", produces = "application/json", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public long createPerson(@RequestBody @Valid Person person) {
        return personService.createPerson(person.getFirstName(), person.getLastName());

    }

 

We see here again all the usages we've already seen of the @RequestMapping, @ResponseBody and @ResponseStatus (but this time it returns the status creates (201) when everything is ok). The new things here are the @RequestBody and @Valid annotations in front of the Person.

 

The @RequestBody tells spring that we expect the Person argument in this method to be the entire body of the request. Spring converts using a message converter from the body of the request according to the Content-Type header of this request. So, assume we had both a marshalling message converter and a mapping jackson2 message converter - in this case the user could send both an XML body and a JSON body and still access the same method, provided that the Content-Type header is setup correctly.

 

The @Valid annotation causes spring to validate the Person object created from the message converter using a JSR 303 validator. In the example I'm using Hibernate Validator 5.0.1.Final (http://search.maven.org/#artifactdetails%7Corg.hibernate%7Chibernate-validator%7C5.0.1.Final%7Cjar).

 

The JSR 303 validation is a framework that uses annotations to validate properties on beans. In this example the Person entity has a JSR 303 annotation on top of its firstName field - requiring its size to be at least 1 character long. If we try to create a Person with an empty first name we'll get an http response status bad request (400). This is done just by the presence of the @Valid annotation and @Size annotation on the Person. Nothing else was required.

 

Nothing special about the delete method and the exception handler is pretty self-explanatory:

    @ExceptionHandler(IOException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public String exceptionHandler() {
        return "f^@k";
    }

 

This is obviously just an example - the exception handler here is invoked whenever an IOException is thrown from anywhere within a request in this controller, then the result would be the string "f^@k" and http response status 500.

 

Finally we're getting to the multiple media types (formats) responses in the GET /persons. This is achieved by a technical method that returns a Model:

    @RequestMapping(value = "/persons", produces = {"text/html", "application/vnd.ms-excel", "text/csv"})
    @ResponseStatus(HttpStatus.OK)
    public Model personsWithView(Model model) {
        return model.addAttribute("persons", getPersons());
    }

 

This method does nothing on its own except put the result of the json getPersons() method on an MVC Model object. Spring MVC passes all Model return values from controller methods through the RequestToViewNameTranslator. Here's my implementation of this translator:

@Component("viewNameTranslator")
public final class MediaTypeRequestToViewTranslator implements RequestToViewNameTranslator {
    @Autowired
    private ContentNegotiationManager contentNegotiationManager;

    @SuppressWarnings("UnusedDeclaration")
    private DefaultRequestToViewNameTranslator defaultTranslator = new DefaultRequestToViewNameTranslator();

    // a list of media types to ignore - not output on the translated view
    private List<String> ignoredTypes = Arrays.asList("text/html");

    @Override
    public String getViewName(HttpServletRequest request) {
        // first resolve the media type (see below)
        String mediaType = resolveMediaType(request);

        // delegate to the default translator to get the view name
        String viewName = defaultTranslator.getViewName(request);

        // concatenate the resolved media type to the default name and return it
        return viewName + mediaType;
    }

    private String resolveMediaType(HttpServletRequest request) {
        try {
            // use the content negotiation manager to resolve the media type from the request. The manager does it
            // according to its own search path and preferences - using the suffix, using the Accept header and
            // preferring one of them. The result would always be a single type or none at all, but it returns a list
            List<MediaType> types = contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));

            // resolve to a single type
            String type = types == null || types.size()==0 ? "" : types.get(0).toString();
            
            // if it's not in the ignored media types - prepend a semi-colon and return it 
            return ignoredTypes.contains(type) ? "" : ";" + type;
        } catch (HttpMediaTypeNotAcceptableException e) {
            return "";
        }
    }
}

 

The media type request to view translator delegates to the default translator and adds the media type separated by a semi-colon to the resolved view name. So if we requested GET /persons with Accept: text/csv the translator will return "persons;text/csv"; when the Accept header was set to application/vnd.ms-excel the result would be "persons;application/vnd.ms-excel" and finally if the Accept header was text/html the result would be "persons".

 

After the view name translation Spring passes the result to the different view resolvers. There are 2 view resolvers defined in the example application in this order: bean name view resolver, internal resource view resolver. The order is important because the internal resource view resolver never returns "I don't know this view" - it will return an http not found status (404) instead. The bean name translator, however, tells the Spring MVC framework that it cannot resolve the view if there is no bean defined in the requested name.

 

We can now add 2 dedicated view renderer beans - one for CSV and one for Microsoft Excel. Let's take a look at the Microsoft Excel view renderer first:

@Component("persons;application/vnd.ms-excel")
public final class PersonsExcelReportView extends AbstractJExcelView {
    @Override
    protected void buildExcelDocument(Map<String, Object> model, WritableWorkbook workbook, HttpServletRequest request,
                                      HttpServletResponse response) throws Exception {
        response.setContentType("application/vnd.ms-excel");

        //noinspection unchecked
        List<Person> people = (List<Person>) model.get("persons");

        WritableSheet sheet = workbook.createSheet("Persons", 0);

        sheet.addCell(new Label(0, 0, "First Name"));
        sheet.addCell(new Label(1, 0, "Last Name"));

        int row = 1;
        for (Person p : people) {
            sheet.addCell(new Label(0, row, p.getFirstName()));
            sheet.addCell(new Label(1, row++, p.getLastName()));
        }
    }
}

 

The bean is defined with the name "persons;application/vnd.ms-excel" so that the bean name view resolver would find it. internally the implementation here is naïve - just take the persons list from the model and put it in cells. The underlying library that provides Microsoft Excel support is jExcel: http://jexcelapi.sourceforge.net/. The AbstractJExcelView is provided by Spring MVC.

 

For CSV Spring does not provide an out of the box view renderer, so here's my implementation using super-csv: http://supercsv.sourceforge.net/:

@Component("persons;text/csv")
public final class PersonsCsvView implements View {
    @Override
    public String getContentType() {
        return "text/csv";
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType("text/csv");
        //noinspection unchecked
        List<Person> persons = (List<Person>) model.get("persons");

        try (CsvBeanWriter writer = new CsvBeanWriter(new OutputStreamWriter(response.getOutputStream()), CsvPreference.STANDARD_PREFERENCE)) {
            writer.writeHeader("firstName", "lastName");
            for (Person p : persons) {
                writer.write(p, "firstName", "lastName");
            }
        }
    }

}

 

Again - the view bean name is "persons;text/csv" and the implementation takes the persons list from the model and generates a CSV from it.

 

Now finally we can look at the web.xml and the rest-servlet.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <servlet>
        <servlet-name>rest</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>rest</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

 

The web.xml defines just a single Spring dispatcher servlet - the starting point of the Spring MVC. The web application context of this dispatcher servlet is in the default location <servlet-name>-servlet.xml in this case it is the rest-servlet.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- scan the controllers -->
    <context:component-scan base-package="com.hp.example.controllers"/>

    <!-- scan the views -->
    <context:component-scan base-package="com.hp.example.views"/>

    <!-- define the REST content negotiating view resolver -->
    <bean id="contentNegotiationManager"
          class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"
          p:defaultContentType="application/json"
          p:favorPathExtension="true"
          p:useJaf="false">
        <property name="mediaTypes">
            <map>
                <entry key="json" value="application/json"/>
                <entry key="html" value="text/html"/>
                <entry key="xls" value="application/vnd.ms-excel"/>
                <entry key="csv" value="text/csv"/>
            </map>
        </property>
    </bean>

    <!-- add a bean name view resolver to support the multiple views results based on bean names -->
    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="1"/>

    <bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:viewClass="org.springframework.web.servlet.view.JstlView"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp"
          p:order="2"/>

    <!-- define the annotation driven MVC with the content negotiation -->
    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>
</beans>

 

The above definition is for Spring 3.2 - the content negotiation manager is a new syntactic sugar for defining a content negotiating view resolver in Spring 3.1 and earlier.

 

 Next blog entry: Testing the Web Layer

Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the Community Guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
About the Author


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation