Configuring the ContentNegotiatingViewResolver for Spring 3.0 RESTful services

4 Jan 2010

Update 1: There's some weird AspectJ error that keeps occurring.

Update 2: The AspectJ error was actually in AspectJ.

I have what I think will be a not particularly uncommon scenario, but couldn't find a direct answer to how to handle it.  I finally worked it out, so I wanted to make it available to anyone else that's trying to get past this bump in the road!

Update Below: There's some weird AspectJ error that keeps occurring.

I have what I think will be a not particularly uncommon scenario, but couldn't find a direct answer to how to handle it.  I finally worked it out, so I wanted to make it available to anyone else that's trying to get past this bump in the road!

I have a set of RESTful services that can be consumed in different ways:

  • As JSON to feed a Flex or jQuery-based client
  • As XML to feed some services that may need the XML (this is really the least of my priorities, honestly, being so done with XML for the most part, but since it basically comes for free, why the hell not?)
  • As a regular browser-based view using JSP to allow users to work with the data directly

Spring provides the ContentNegotiatingViewResolver for just this use. It will find the appropriate view for the content based on the Accept header in the browser request or the file extension used. What's cool about this is that you can map a method in your controller class to some REST URL:

@RequestMapping("/items")
public ModelAndView getAllItems()
{
   List items = _itemService.getItems();
return new ModelAndView("items", "items", items);
}

The URL is mapped to /app/items, but you can call it as /app/items.html, /app/items.xml, /app/items.json, and so on.  The extension will tell Spring the content type to use when returning the data.

The way the ContentNegotiatingViewResolver works is by taking a list of media types that it supports, set to the property mediaTypes.  You can then set up a list of default views to handle the media types.  If the view finds a view that matches the content type, it uses that view.  If it doesn't find a view for the requested content type, Spring will go onto any other view resolvers and try to resolve the view in the standard way, going along until one of the view resolvers returns a view for the model.

Note that you can also embed view resolvers within the ContentNegotiatingViewResolver and have a view resolver handle a particular content type.  If the view returned from a view resolver handles that content type, then the ContentNegotiatingViewResolver will use that.  I originally had this configured using the standard InternalResourceViewResolver within the ContentNegotiatingViewResolver, but that actually then precluded using the extension-less URL.  The way to deal with that issue is to have InternalResourceViewResolver specified twice: once within the ContentNegotiatingViewResolver, once as a stand-alone view resolver at the same level as the content negotiating.  That's a bit inelegant, so I didn't do that, but that could also be useful, providing a way to map a different set of JSP templates if the user specifies the HTML extension.

So without further ado, here's the configuration entry for the view resolvers that make this all work:

<bean 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" />
    </map>
  </property>
  <property name="defaultViews">
    <list>
      <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
      <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
        <constructor-arg>
          <bean class="org.springframework.oxm.xstream.XStreamMarshaller" />
        </constructor-arg>
      </bean>
    </list>
  </property>
  <property name="ignoreAcceptHeader" value="true" />
</bean>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="order" value="2" />
  <property name="prefix" value="/WEB-INF/views/"/>
  <property name="suffix" value=".jsp"/>
</bean>

 Obviously, you can easily add other media types just by adding them to the mediaTypes property and adding a view that handles that type to the list of defaultViews.

Update 1: I keep getting this weird AspectJ exception.  It's a StackOverflowError and it seems to happen sort of randomly.  As I only started hitting the AspectJ stuff when I added the org.codehaus.jackson mapper functionality (which is required by the MappingJacksonJsonView), this is somehow related to that.  But it doesn't seem to affect the content negotiating view resolver, so I'm not really sure what to do about it!

Update 2: As it happens, the issue was in the AspectJ framework itself. There's a development build that will fix the issue and/or the next point release should take care of the problem. Andy Clement, the developer on that project, was really helpful getting the issue resolved.