Thursday, May 7, 2009

Spring Framework 3.0 M3 and REST enhancements

In my previous blog post, I described how Spring 3.0 Milestone 2 can be applied in real REST web service project, and I enumerated several limitations and problems. M3 is out now. What has changed then?


M3 contains several enhancements over M2, addressing my postulations:



  1. Fixed URL mapping with @RequestMapping annotations: type-level and method-level finally work together in the logical manner, allowing for easy writing of one controller per one REST resource (SPR-5631). I also noticed that at container startup, all the resolved mappings are printed to the log at INFO level - that's nice too.

  2. Trailing slashes insensitive mapping (SPR-5636)

  3. Input data (body) conversion - easy access to request body as controller method param, converter for form encoded data (and other types) (SPR-5409)

  4. Type conversion error for @PathVariable annotated params returns code 400, not 500 (SPR-5622)

  5. Custom processing of object returned from controller method (SPR-5426)

  6. Header-based request matching (SPR-5690)

  7. Flexible exception handling (SPR-5622 and related)


I haven't yet verified in practice the last three of them, but overall it looks pretty good, and the picture is now much more complete than in case of M2.


Small bugs that I've found:


  • Exception when method-level annotation starts with slash (SPR-5631, SPR-5726) - should be already fixed

  • Code 404 returned, when it should be 405 (SPR-4927)


One thing that worries me is the sentence from M3 documentation: Available separately is the JacksonJsonView included as part of the Spring JavaScript project. I thought it was supposed to be moved to core REST packages? As I said earlier, JSON is not a JavaScript. It's a data exchange format you can use to talk Java to .NET, with no JavaScript involved. So having dependency on JavaScript project makes no sense.


I really like the idea of FormHttpMessageConverter, which can bind body data to MultiValueMap:


@RequestMapping(method=PUT, value="{projectName}")
public HttpResponse createOrUpdate(@PathVariable String projectName,
@RequestBody MultiValueMap<String,String> newValues)

It solves (at least partially) problem of servlet getParameter method used with PUT (see my post for REST in M2, and also SPR-5628). Actually, FormHttpMessageConverter calls the getInputStream method on ServletResponse, so after it is called by the framework, the attempt to call getParameter inside controller method will return null not only for PUT requests, but also for POST, because body was already parsed into MultiValueMap (this behavior is valid according to servlet API). But having the map of parameters, there is no need for calling getParameter again.


I have only one problem with MultiValueMap. Previously, I parsed manually the form data from body into PropertyValues object, which was then passed to Spring's DataBinder for updating the model entity class. Now, in M3, the form data is parsed by Spring and I get MultiValueMap<String,String> directly. That's nice, but how to pass it to DataBinder? There is a bind(Map) method in DataBinder, but the problem is that MultiValueMap<String,String> at the very bottom is actually the Map<String,List<String>>. Now if I pass it to bind method directly, I get many binding errors saying "cannot convert List<String> to String", because model class has fields of type String, and the MultiValueMap keeps it as one-element string lists. The simplest solution would be to extend MultiValueMap interface with one method, called e.g. 'asFlatMap()' or so, that would return either copy or the live view of the MultiValueMap<K,V> as Map<K,Object> with all values which are single-element lists replaced by the element itself (so Object is actually either V or List<V>). This way I could pass the map returned by this method to the DataBinder easily. The code below shows the conversion:


Map<String,Object> asFlatMap(MultiValueMap<String,String> map) {
Map<String,Object> convertedMap = new LinkedHashMap<String,Object>();
for (Entry<String,List<String>> entry : map.entrySet()) {
if (entry.getValue().size() == 1) {
convertedMap.put(entry.getKey(), entry.getValue().get(0));
} else {
convertedMap.put(entry.getKey(), entry.getValue());
}
}
return convertedMap;
}

I've created a SPR-5733 issue for this. AFAIK, Spring binding is going to be refactored in RC1, so perhaps it can be aligned with it too.