Scott Frederick's humble blog

on sofware development and technology

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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.