Update Mar.04 Thanks to @ewolff some of the points described below are now official feature requests. One (SPR-6928) is actually scheduled in Spring 3.1 (cool!). I’ve updated the post and added all open tickets. Please vote!
This post is somewhat a response to InfoQ’s Comparison of Spring MVC and JAX-RS.
Recently I have completed a migration from a JAX-RS implementation of a web service to Spring 3.0 MVC annotation-based @Controllers. The aforementioned post on InfoQ was published a few days after my migration so I’m dumping below the list of problems I had, along with solutions.
Full list of issues:
- Same relative paths in multiple controllers not supported
- @ExceptionHandler is controller-centric
- Standard content negotiation can’t respond with a fixed response type
- JSR 303 bean validation not applied in @Controllers
- Formatting responses (i.e. Date) not working when using Spring formatter annotations
Same relative paths in multiple @Controllers not supported
Consider two Controllers where I use a versioned URL and a web.xml file that uses two URL mappings:
@Controller public class AdminController { @RequestMapping("/v1/{userId}") public SomeResponse showUserDetails(String userId) { ... } } @Controller public class UserController { @RequestMapping("/v1/{userId}") public SomeOtherResponse showUserStreamtring userId) { ... } } In web.xml: <servlet-mapping> <servlet-name>public-api</servlet-name> <url-pattern>/public</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>admin-api</servlet-name> <url-pattern>/admin</url-pattern> </servlet-mapping>
Here I want to implement handling for /admin/v1/{userId} as well as /user/v1/{userId}. Each controller serves a different purpose (admin and public) and I want to keep the servlet application contexts separate for each servlet mapping (for instance add authentication and different view resolvers to all /admin/* URLs).
Deploying this configuration barfs the following exception:
java.lang.IllegalStateException: Cannot map handler [UserController] to URL path [/v1/{userId}]: There is already handler [com.test.controller. UserController@6b177115] mapped.
I would like Spring MVC to account for web.xml servlet mappings before deciding that two identical @RequestMappings in different controllers refer to the same URI. My solution was to have all @RequestMappings specifying the full path and resorting to a custom view to work around this issue).
@ExceptionHandler is controller-centric
RESTful web services respond with the usual HTTP responses (200s, 400s, 500s, etc.). These responses are being mapped from application exceptions and it only makes sense to specify the same @ExceptionHandler for all @Controllers since most (if not all) exceptions handled by them are for the same business domain. Currently SpringMVC only supports declaring and @ExceptionHander per controller and not globally per webapp. This forces the implementors to extend their @Controller classes from a common one where the @ExceptionHandler is specified or, to use composition and delegate exception handling to one service which leads to duplication. Both are ugly. There are several ways to deal with exceptions (check SPR-5622; please guys, add this to the main Spring documentation) but ideally I’d like to see a global scoped handler like @ExceptionHandler(scope=APPLICATION) where I can remap once framework exceptions to more suitable ones that translate into proper error codes (like working around SPR-5622 like issues).
Standard content negotiation can’t respond with a fixed response type (SPR-6937)
Here’s a simple requirement: Implement an RSS feed that serves a user’s activity stream.
Here’s another requirement (make that best practice):
Service methods should have as arguments primitives, wrappers or other POJOs and respond with void, primitives, wrappers or other POJOs.
I want testability, readability and portability for service classes and using framework objects for arguments/return objects adds unacceptable clutter.
As such, no framework objects are allowed as arguments or return types in a @Controller and in this case I don’t want to work with ModelAndViews just to be able to select the desired View.
Since this new requirement (add RSS view) belongs to the user domain, I want to add a method in the UserController class that returns a POJO which should be always unmarshalled into RSS regardless of request content type (there goes ContentNegotiationViewResolver). The same controller has other methods returning POJO view objects that I want marshalled into JSON or plain text response body and an HTTP response status. This is already available in RESTEasy via the elegant Produces annotation. The same RESTful controller should respond with different response types for different requests since I want to keep its domain centered around the user (and not response types).
... public class UserController { ... @RequestHandler("/rss/{userId}") public SomePOJO getRssFeed(String userId) { ... //returns a POJO that should always be resolved by an RSS view } @RequestMapping("/activity/{userId}") public SomeOtherPOJO showUserStream(String userId) { ... //return POJO that should always be resolved by a JSON view } ... }
It turns out this is not immediately possible. To my knowledge none of the included view resolvers provide a straight mapping between URLs and views so I had to write one that configures like this:
<bean class="UrlToViewViewResolver"> <map name="urlToViewMap"> <property key="/user/rss/*" value="rssView"/> <property key="/user/activity/*" value="jsonView"/> ... </map> </bean>
The implementation (not included) is trivial and based on AntPathMatcher, a utility class readily available in Spring.
JSR 303 bean validation not applied in @Controllers (SPR-6928, scheduled for 3.1)
On Springsource’s weblog there’s a nice tutorial on how to get validation and type conversion in Spring 3.0 here. When it comes to validation though, Spring only supports it “after binding user input to it”. It is not possible to validate arguments and return types for a RESTful controller (no web forms or user input of any kind there). This means this won’t work out of the box:
@RequestHandler("/admin/", method = RequestMethod.PUT ) public void updateUser(@Valid User user) { ... //returns a POJO that should always be resolved by an RSS view }
since in a RESTful scenario the User object won’t be bound from a form but unmarshalled from XML/JSON/CSV or any other wire format.
To enable validation for a RESTful controller I chose to use @Valid at the method level and add an around advice that intercepts all methods annotated as such then use a Validator to validate arguments and return objects.
<aop:config proxy-target-class="true"> <aop:pointcut id="validatableMethodExecution" expression="execution(* com.mycorp..*.*(..)) and @annotation(javax.validation.Valid)"/> <aop:aspect id="validateArgumentsAndReturnObject" ref="validatorAspect"> <aop:around pointcut-ref="validatableMethodExecution" method="validate"/> </aop:aspect> </aop:config> public class ModelValidatorAspect { @Autowired private Validator validator; public Object validate(ProceedingJoinPoint pjp) throws Throwable { Set<ConstraintViolation> violations =new HashSet<ConstraintViolation>(); for(Object obj : pjp.getArgs()) { if(obj != null) { violations.addAll(validator.validate(obj)); } } if(!violations.isEmpty()) { throw new BadRequestException(composeErrorMessage(violations)); } Object ret = pjp.proceed(); //validate return object and throw exception if the validation fails. if(ret != null) { violations = validator.validate(ret); } if(!violations.isEmpty()) { throw new BusinessValidationException(violationsStr); } } }
This could get more complex if you have deal with collections of validatable objects but it’s not hard to enhance the code to handle that.
Formatting responses (i.e. Date) not working using Spring formatter annotations
Spring comes with some niceties like @DateTimeFormat and @NumberFormat and naturally I want to rely on them by annotating the class attributes of return types used by @Controllers.
That’s not quite working since the formatters work with form-backed model objects. However, if you use a MappingJacksonJsonView there is a way of formatting using Jackson’s own @JsonSerialize.
Although at times it felt that the switch to SpringMVC was too much, the features that come with it (like abstracting web-related properties as annotations) will turn your RESTful controllers into annotated POJOs focused on the business domain which ultimately means maintainable code.
Update Mar.04 I left out several points about JSON marshalling that didn’t work as expected (Jackson marshaller is a handful).
– One enhancement I suggested to ease the pain of configuring JSON marshalling is creating a namespace similar to oxm (SPR-6943)
– The second was to add Jetisson support (SPR-6938) as an alternative to Jackson JSON marshaller.
I’m keen on your feedback regarding these issues so if you have better solutions please comment.