SyntaxHighlighter

Friday, March 11, 2011

Customizing Spring 3 mvc:annotation-driven

In Spring 3, it is very easy to configure an application with all the basic MVC components using the mvc:annotation-driven tag.

The default configuration can be customized with arguments to the mvc:annotation-driven tag (such as "validator" and "conversion-service") and other tags in the mvc: namespace. In Spring 3.0.0 the set of customization options was somewhat limited, but it has grown with each Spring release. Spring 3.1 adds a mvc:message-converters to address one of the more common customization needs.

A very common mistake developers make when they need to customize the annotation-driven configuration is to use mvc:annotation-driven and also manually define a AnnotationMethodHandlerAdapter bean with customized properties, like this:
<mvc:annotation-driven/>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
 <property name="customArgumentResolver">
   <bean class="com.example.ExampleArgumentResolver" />   
 </property>
  <property name="messageConverters">
    <list>
      <bean id="marshallingHttpMessageConverter"
            class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter"
            p:marshaller-ref="marshaller" p:unmarshaller-ref="marshaller"/>
    </list>
  </property>
</bean>

This does not work as expected. With this configuration, you end up with two AnnotationMethodHandlerAdapter beans in your application context - one created by mvc:annotation-driven and the one created manually. The one created by the framework is used, and the one created manually is ignored.

One working solution to this problem is to remove the mvc:annotation-driven tag and instead manually define all the annotation support beans. This is not ideal, as there is not a good way to keep up with all the beans defined automatically by mvc:annotation-driven as the annotation support evolves in the framework.

The preferred solution to the customization problem is to implement a BeanPostProcessor to modify properties of the AnnotationMethodHandlerAdapter bean created by mvc:annotation-driven in-place, instead of replacing it. This is not hard to do, and provides all the flexibility needed to customize the annotation support.

Here is an example of what this solution can look like. This solution uses a very flexible class that has all the same properties as AnnotationMethodHandlerAdapter, but configures an existing HandlerAdapter instead of creating a second one. Usage of this solution would look like this:

<mvc:annotation-driven/>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapterConfigurer" init-method="init">
 <property name="customArgumentResolver">
   <bean class="com.example.ExampleArgumentResolver" />   
 </property>
  <property name="messageConverters">
    <list>
      <bean id="marshallingHttpMessageConverter"
            class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter"
            p:marshaller-ref="marshaller" p:unmarshaller-ref="marshaller"/>
    </list>
  </property>
</bean>

Look closely at line 3 in the second example, since this is the only line that is different from the first example. The bean being created is a AnnotationMethodHandlerAdapterConfigurer instead of a AnnotationMethodHandlerAdapter. The Configurer class manipulates the HandlerAdapter already created by the framework, but is configured just like AnnotationMethodHandlerAdapter. The "init-method" argument to the bean definition is important, as it causes the configuration to happen after the application context is up and running.

Here is what the Configurer looks like:
public class AnnotationMethodHandlerAdapterConfigurer {
  @Autowired
  private AnnotationMethodHandlerAdapter adapter;

  private WebBindingInitializer webBindingInitializer;
  private HttpMessageConverter[] messageConverters;
  private PathMatcher pathMatcher;
  private UrlPathHelper urlPathHelper;
  private MethodNameResolver methodNameResolver;
  private WebArgumentResolver[] customArgumentResolvers;
  private ModelAndViewResolver[] customModelAndViewResolvers;

  private boolean replaceMessageConverters = false;

  public void init() {
    if (webBindingInitializer != null) {
      adapter.setWebBindingInitializer(webBindingInitializer);
    }

    if (messageConverters != null) {
      if (replaceMessageConverters) {
        adapter.setMessageConverters(messageConverters);
      } else {
        adapter.setMessageConverters(mergeMessageConverters());
      }
    }

    if (pathMatcher != null) {
      adapter.setPathMatcher(pathMatcher);
    }

    if (urlPathHelper != null) {
      adapter.setUrlPathHelper(urlPathHelper);
    }

    if (methodNameResolver != null) {
      adapter.setMethodNameResolver(methodNameResolver);
    }

    if (customArgumentResolvers != null) {
      adapter.setCustomArgumentResolvers(customArgumentResolvers);
    }

    if (customModelAndViewResolvers != null) {
      adapter.setCustomModelAndViewResolvers(customModelAndViewResolvers);
    }
  }

  private HttpMessageConverter[] mergeMessageConverters() {
    return (HttpMessageConverter[])
                  ArrayUtils.addAll(messageConverters, adapter.getMessageConverters());
  }

  public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
    this.webBindingInitializer = webBindingInitializer;
  }

  public void setPathMatcher(PathMatcher pathMatcher) {
    this.pathMatcher = pathMatcher;
  }

  public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
    this.urlPathHelper = urlPathHelper;
  }

  public void setMethodNameResolver(MethodNameResolver methodNameResolver) {
    this.methodNameResolver = methodNameResolver;
  }

  public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
    this.customArgumentResolvers = new WebArgumentResolver[] {argumentResolver};
  }

  public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
    this.customArgumentResolvers = argumentResolvers;
  }

  public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) {
    this.customModelAndViewResolvers = new ModelAndViewResolver[] {customModelAndViewResolver};
  }

  public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) {
    this.customModelAndViewResolvers = customModelAndViewResolvers;
  }

  public void setMessageConverters(HttpMessageConverter[] messageConverters) {
    this.messageConverters = messageConverters;
  }

  public void setReplaceMessageConverters(boolean replaceMessageConverters) {
    this.replaceMessageConverters = replaceMessageConverters;
  }
}

There is one property of the Configurer class that does not correspond directly to a property of AnnotationMethodHandlerAdapter: "replaceMessageConverters". By default this is false, which causes any configured MessageConverters to be added to the converters already configured into the AnnotationMethodHandlerAdapter. If this property is set to true, then the automatically-configured MessageConverters are thrown away and only those injected into the Configurer will be used.

This approach will become less and less interesting as Spring 3.1 is released and mvc:annotation-driven continues to evolve. Until then, it solves a problem that a lot of developers have spent time figuring out the hard way.

12 comments:

  1. Great tip. I could not find this AnnotationMethodHandlerAdapterConfigurer class in the org.springframework.web.servlet-3.0.5.RELEASE.jar. Do you know where i could find it.

    ReplyDelete
  2. @Anonymous
    It is a custom class he created and posted in the post.

    ReplyDelete
  3. Awesome tip! Quite elegant in fact.

    ReplyDelete
  4. This seems like a feasible solution but I keep getting an error because it can not autowire the AnnotationMethodHandlerAdapter bean, complaining that it does not exist. Any ideas?

    ReplyDelete
  5. @Hamed
    were you ever able to resolve this?
    @Scott
    I'm getting the exact issue Hamed is seeing
    " No matching bean of type [org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter] found for dependency"
    Any advice?
    Thanks

    ReplyDelete
  6. A few questions:

    Do you have in your Spring XML file? If not, then the default AnnotationMethodHandlerAdapter won't get instantiated.

    Are you using Spring 3.0 or 3.1? If you are using 3.1, then the default beans created by will be different (see http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/new-in-3.1.html#d0e1515), and there are better ways to override Spring defaults using new Java Config features.

    If you have and are using Spring 3.0, then I would have to see your XML configuration

    ReplyDelete
  7. Thanks for posting this example. I was having a lot of trouble trying to set a custom webBindingInitializer until I stumbled upon this page.

    ReplyDelete
  8. Or simply write this:

    ReplyDelete
  9. Use RequestMappingHandlerAdapter instead starting with Spring 3.1.

    ReplyDelete
  10. Another helpful tip, which would allow you to remove the 'init-method="init"' attribute in the XML config, would be to implement the spring 'InitializingBean' interface, which gets called after Spring finishes injecting dependencies. Yes makes more dependent on the Spring framework (not that it isn't already), but also makes it more transparent for usage.

    ReplyDelete