When I teach Spring, I tell my students that Spring’s MVC architecture is one that allows the human facing Web site to easily be adapted to address non-human users. That is, the Spring MVC architecture easily supports RESTful Web services. In fact, Spring MVC applications can address human and machine users alike without the need for separate controller components or alternative request mapping paths.
By adding the ContentNegotiatingViewResolver, along with a few extra XML, JSON or alternative formatting beans, your other Web application components can do it all: Web site and RESTful service in one.
A Demonstration Web Application
In order to demonstrate just how flexible the Spring MVC architecture is, let’s begin by looking at a simple Web application. Then, we’ll turn that simple Web application into one that also dishes out JSON, XML or other formatted data with the same components.
The example here is a simple contact management application. This Web application allows for adding, updating, deleting, and displaying a collection of contacts that are persistent in a database. At the heart of the application is the Spring controller. The code for the controller is below.
package com.intertech.controller; import java.util.List; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.Errors; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import com.intertech.domain.Contact; import com.intertech.service.ContactService; @Controller public class ContactController { @Autowired private ContactService contactService; public void setContactService(ContactService contactService) { this.contactService = contactService; } @RequestMapping(value = "addcontact", method = RequestMethod.GET) protected String showContactForm(Contact contact) { return "editcontact"; } @RequestMapping(value = "addcontact", method = RequestMethod.POST) protected String addContact(@Valid Contact contact, Errors errors, Model model) { if (errors.hasErrors()) { for (ObjectError error : errors.getAllErrors()) { System.out.println("Validation error: " + error.getDefaultMessage()); } return "editcontact"; } if (contact.getId() > 0) { contactService.updateContact(contact); List<Contact> contacts = contactService.getContacts(); model.addAttribute("contacts", contacts); return "displaycontacts"; } contactService.addContact(contact); return "successfuladd"; } @RequestMapping("displaycontacts") protected ModelAndView displayContacts() { List<Contact> contacts = contactService.getContacts(); return new ModelAndView("displaycontacts", "contacts", contacts); } @RequestMapping("deletecontact") protected String deleteContact(@RequestParam int id, Model model) { contactService.removeContact(id); List<Contact> contacts = contactService.getContacts(); model.addAttribute("contacts", contacts); return "displaycontacts"; } @RequestMapping(value = "editcontact", method = RequestMethod.GET) protected String editContact(@RequestParam long id, Model model) { Contact contact = contactService.getContact(id); model.addAttribute("contact", contact); return "editcontact"; } @RequestMapping(value = "displaycontact", method = RequestMethod.GET) protected void displayContact(@RequestParam long id, Model model) { Contact contact = contactService.getContact(id); model.addAttribute("contact", contact); } }
Note that it has a @RequestMapping and handler method for each of the add, edit, update, delete, display and display Contacts features of the Web application. The ContactService, which is dependency injected into the controller, handles the persistence work.
The details of both the service (model) and the JSPs (views) are not particularly important to this study. However, a complete copy of this application with the necessary ingredients to address RESTful services is available for download at the Intertech Web site here. It may not address all your needs, but its enough to see how to build Web site/service in one.
At this point, the servlet mapping in web.xml is not too exciting either, but because it will require some adjustment in order to address RESTful services in a bit, the starting relevant <servlet> and <servlet-mapping> elements are shown here.
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/mvc-beans.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>*.request</url-pattern> </servlet-mapping>
Here are some pictures of the working Web application.
Adding RESTful Components
With a Web application like this in place, what do you need to add to allow the same application to dispense JSON or XML data about contacts in a RESTful way?
First, JAXB annotations on the domain beans allowing those beans to be used to produce XML.
@XmlRootElement(name = "contact") public class Contact { ... }
Next, add the very important ContentNegotiatingViewResolver bean (shortened to CNVR for the rest of this post) to your Spring metadata. The CNVR “does not resolve views itself, but delegates to other ViewResolvers” (per the Spring JavaDocs here). The algorithm the CNVR uses to figure out which view resolver (and associated View bean) to delegate to can be a bit confusing. The details of its inner-workings are covered in the next section.
<bean id="contentNegotiatingViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="order" value="1" /> <property name="mediaTypes"> <map> <entry key="json" value="application/json" /> <entry key="xml" value="application/xml" /> <entry key="request" value="text/html" /> </map> </property> <!-- <property name="favorPathExtension" value="false" /> --> <!-- <property name="favorParameter" value="true" /> --> <!-- <property name="ignoreAcceptHeader" value="false" /> --> <property name="defaultViews"> <list> <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"> </bean> <bean class="org.springframework.web.servlet.view.xml.MarshallingView"> <constructor-arg> <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="classesToBeBound"> <list> <value>com.intertech.domain.Contact</value> </list> </property> </bean> </constructor-arg> </bean> </list> </property> </bean>
The mediaTypes property of the CNVR bean is discussed below, but you should note a couple of other dependency injections made to this bean. First, note that the CNVR is dependency injected with an order. This order makes sure that it is first view resolver in line to when it comes to view resolution. This allows it to pick the right view resolver before any other view resolver is selected by Spring to fulfill a response. Note I set my view resolver to 1.
Next, the CNVR bean is dependency injected with a list of default view beans (defaultViews). The default views are view beans used when a view cannot be obtained from the view resolver chain. In this case, these view beans are going to be used to produce the non-HTML output – i.e. the JSON and XML data.
One last change required of the application to address REST needs is to widen the servlet mapping to allow requests for JSON or XML by URL to enter and be handled by the application.
<servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>*.request</url-pattern> <url-pattern>*.json</url-pattern> <url-pattern>*.xml</url-pattern> </servlet-mapping>
That’s it! Now, our Web site addresses human needs as well as machines that need JSON or XML data. Note the three requests below. All three are a request to “display contact #35”. The URL’s extension dictates the type of content required and how the application responds.
The CNVR Algorithm
So how does the CNVR handle the negotiation for different types of content (HTML, JSON, or XML in this case)? The CNVR has a set of configuration options which dictate the delegation to other view resolvers.
CNVR Option 1 – using the extension
First, and by default, the CNVR inspects the incoming URL for a URL extension (.request, .json or .xml in our case) and then uses the mediaType list to match the extension to a media type. With an established media type, the CNVR looks for an appropriate view resolver/view bean to produced the requested media type.
<property name="mediaTypes"> <map> <entry key="json" value="application/json" /> <entry key="xml" value="application/xml" /> <entry key="request" value="text/html" /> </map> </property>
In my example above, a .request URL extension maps to a text/html output whereas a .json URL extension maps to a JSON output. The HTML output is handled by the application’s normal view resolvers/view beans (examine the downloadable example code and you’ll see this is an InternalResourceViewResolver and InternalResourceView bean). When the media type is JSON, the CNVR delegates to a view resolver/view bean that can address application/json media types. It eventually finds the MappingJacksonJsonView bean to provide the response.
CNVR Option 2 – using a URL parameter
If the favorPathExtension parameter is set to false (it is true by default), favorParameter is set to true (false by default), and if a URL query parameter (called format by default) is provided in the request, then the parameter value in the request is used to select the media type of the request in the mediaList. Here is the configuration of CNVR using this strategy.
<bean id="contentNegotiatingViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="order" value="1" /> <property name="mediaTypes"> <map> <entry key="json" value="application/json" /> <entry key="xml" value="application/xml" /> <entry key="request" value="text/html" /> </map> </property> <property name="favorPathExtension" value="false" /> <property name="favorParameter" value="true" /> <property name="defaultViews"> <list> ... per original configuration </list> </property> </bean>
And here are the results. Notice the “format” query parameter that is not dictating the response. Again, format is the default parameter, but this too can be configured.
As a side note to this option, the servlet mapping had to be altered to allow the URL’s minus the extension to enter the application.
<servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
Option 3 – use JAF
Per the documentation, If there is no match in the mediaTypes property and if the JavaBeans Activation Framework (JAF) is enabled and present on the classpath, FileTypeMap.getContentType(String) is used to determine the response content. The JAF is a bit much to cover in this post. So I refer you to the JAF documentation page for help.
Option 4 – use the accept header
The last option for CNVR configuration is to have it use the HTTP request’s accept header to determine the appropriate response content. If the CNVR’s ignoreAcceptHeader parameter is set to false (true by default), and the above options did not result in a media type select, the CNVR uses the standard HTTP accept header to determine the media type.
Here is the CNVR configuration for using the accept header.
<bean id="contentNegotiatingViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="order" value="1" /> <property name="ignoreAcceptHeader" value="false" /> <property name="defaultViews"> <list> ... same per earlier configuration </list> </property> </bean>
In order to test/use this option, a tool has to be used to edit the accept header. Below, I have used Firefox’s HttpRequestor plugin to do the job and show the results.
Enhancing the Controller – Consuming Alternate Content
While our original controller (or any other Java code) has not changed to this point, you may want your application to also consume XML or JSON data. For example, you may want client machines to be able to add a new contact or update an existing contact. Presumably, these machines would want to send in additions/updates via XML or JSON. To facilitate this need, we will add some code to the controller. However, and importantly, this will not require changes to the existing handler methods (or other Java code)! As an example, below is a new controller handler method to add a contact via JSON.
@RequestMapping(value = "/addcontactbyjson", method = RequestMethod.POST) public String addUpdateContactByJSON(@RequestBody Contact contact) { if (contact.getId() > 0) { contactService.updateContact(contact); } else { contactService.addContact(contact); } return "successfuladd"; }
Using the HttpRequester tool again, you can see how to post a new contact via JSON to the Web application/REST Web service.
It’s Not JAX-RS
A cautionary note about Spring MVC’s RESTful Web services is that they are not JAX-RS compliant. The architecture of Spring MVC makes adding REST to a Web application a snap, but not in a way that keeps to the JAX-RS specification for Java RESTful Web services. If you need to keep to the Java specification, you will need to research an alternate solution such as Jersey (the reference implementation of JAX-RS). If you have a Spring MVC application and also need to provide the same (or nearly the same) information provided by your Web site to client machines via RESTful communications, give the ContentNegotiatingViewResolver a look.
Wrap Up
I’d like to thank Paul Croarkin, one of my students, for the questions and impetus for this post. Paul recently posted some information himself about how to use CNVR to StackOverflow and you can read his post here.
Are you exploring Spring and find you need some help getting your application put together? Intertech can help! Whether through consulting or instruction (see our Spring classes here: https://www.intertech.com/software-education//Java/Frameworks/Spring), we can provide you the expertise you need to launch, complete or finely tune your Spring implementation.