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.

3 comments:

Gregory Denton said...

Seems like it would be possible to set things up such that an object returned from a controller method could simply be marshalled depending on the Accept header content type (or file extension, or some negotiator/resolver). Not sure if returning a random class would interfere with existing mechanisms (i.e. as here). The benefit would be a REST-type controller class not having to explicitly use the framework Model/View classes.

See this post for a similar mechanism.

Anonymous said...

This is a comment about your 2 posts on Spring 3 and rest.

Would it be possible to see the sources of your experiment as this may be a great tutorial.

I can help to write the tutorial.

Here is how I see the tutorial :
* setup (pom.mxl, web.xml, applicationContext.xml, HelloWorldController with a method hello that returns {"res":"Hello world !"} with a code 200 and a method error that returns {"error":"this is an error message !" with a code 404)
* simple ExtJs application with a Panel having 2 buttons to launch the rest methods (messages displayed in a messagebox)
* returning json with JacksonJsonView instead of brutal return(I agree with you, a real pity to have a dependency on spring-js project...but JacksonJsonView really is the best approach IMHO)
* add a param to the hello method (ex: the "hello " + name + " !")
* securing the hello service with Basic auth (spring security 3)
* complexify the ExtJs part by adding a login form + automatically adds the credentials infos for every ajax request
* ...

You can contact me at "christophespam dot blinspam @ freespam dot frspam".replace("spam", "") ;)

Grzegorz Borkowski said...

Chris, that's a nice idea to have such tutorial. You are welcome to provide it. Unfortunately, 3 months ago I started my own business, which at this moment consumes my whole time, so I'm not able to help you much with this task.
Please share your results when you're done with it.
As a site note: for using basic authentication with spring security (2.x, not sure about 3.x) and Ext, you may need to extend and replace BasicProcessingFilterEntryPoint to override the header returned by server: instead of "WWW-Authenticate: Basic..." I return "WWW-Authenticate: XBasic..." to bypass default browser basic authentication window.