Spring MVC request parameter conversion with minimal configuration

I’ve been playing around with Spring Web MVC a bit and was looking for something similar to Jersey’s Parameter Classes that would provide conversion to custom types. I liked how with Jersey, you could encapsulate the conversion logic in a single class and have that reused across multiple methods with minimal configuration.

Here’s how I achieved a similar result in Spring Web MVC. (Note: the following examples were done with Spring 3.2.1)

Built-in?

Spring does provide some build-in support for conversion to specific types. For example, you can convert to Date and various numeric types. But, what if you want to convert a request parameter to some other custom type or a type from a third party? (Such as the date-time classes from Joda Time)

Minimize configuration

I searched for a bit and found various solutions, but they all seemed to require too much configuration: In addition to writing a converter class, you’d have to manually the converter with a ConversionService (either in code or in XML configuration). I didn’t like the idea of having to register the converter class; instead, I wanted it to be registered automatically based on some annotations.

Solution

Here’s what I came up with, based on this answer on Stack Overflow.

  1. Define a custom annotation for your converters so they can be automatically found

    @Target({ ElementType.TYPE, ElementType.FIELD })
    public @interface MyConverter {
    }
  2. Create a converter class, annotated with your custom annotation and @Component

    This is so it will be available for injection via @Resource. (In this example, we convert to a Joda Time LocalDate)

    The converter class must implement Spring’s Converter interface.

    @Component
    @MyConverter 
    public class LocalDateConverter implements Converter<String, LocalDate> {
    
      public static final String DATE_FORMAT = "YYYY-MM-dd";
    
      @Override
      public LocalDate convert(final String source) {
        return LocalDate.parse(source, DateTimeFormat.forPattern(DATE_FORMAT));
      }
    }
  3. Create a bean that extends FormattingConversionServiceFactoryBean that will auto-register all converters

    Here is where all the converters annotated with your custom annotation (and @Resource) are injected.

    public class MyConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {
    
      @Resource
      @MyConverter
      private List<Converter<?, ?>> myConverters;
    
      @Override
      protected void installFormatters(final FormatterRegistry registry) {
        // NOTE: This method call is deprecated.
        super.installFormatters(registry);
    
        for (final Converter<?, ?> converter : this.myConverters) {
          registry.addConverter(converter);
        }
      }
    
    }
  4. Edit Spring configuration so that the auto-registering bean is loaded

    Ok, I lied: There is one piece of configuration you need, but once it’s made it need never be changed, even when you define additional converters.

    <mvc:annotation-driven conversion-service="applicationConversionService" />
    <bean class="com.myapplication.spring.mvc.controller.converters.MyConversionServiceFactoryBean" id="applicationConversionService"/>
  5. Modify controller method to use the proper type

    Note that if conversion fails (via an uncaught exception from the convert() method) then the client will see a 400 Bad Request response.

    @RequestMapping(value = "widget/{date}", method = RequestMethod.GET)
    @ResponseBody
    public String getWidgetDate(@PathVariable("date") final LocalDate date) {
      // We get auto-conversion to a LocalDate type... 
      // Just spits back the date to the client.
      return date.toString();
    }

Summary

I hope you found this useful. With just a little bit of work, we have a bean that will auto-register and make available any new converters you define, so as long as you annotate the converter properly.

One Comment »

  1. Thanks a lot of … very useful tutorial …

Comments are now closed for this entry.