Showing posts with label request mapping handler. Show all posts
Showing posts with label request mapping handler. Show all posts

2016-05-03

Request mapping on demand in Spring MVC

Motivation

I want to generate request mappings during execution time of my application. I want to have several URL paths that points to one controller method. Paths will be generated from e-shop product names. Each path should be bound to a language code send in a header. Of course paths can vary as user changes product names during an application's excution.

Here is an example list of mappings. Mapping for English language will match only if lang-header is set to en code. Same rule should apply for Czech lang-header.

[header: Accept-Language=en*]
/spinach
/carrot
/brocoli

[header: Accept-Language=cs*]
/spenat
/mrkev
/brokolice

These prerequisites disqualifies @RequestMapping annotation because:

  • Request mapping conditions are evaluated on application's init phase and cannot be changed on runtime.
  • It's not possible to attach a mapping path to a specific header value. There is many to many relation between paths and headers defined as a request mapping condition.
    @RequestMapping(value = {"/spinach", "/carrot", "/spenat", "/mrkev"}, headers = {"Accept-Language=en*", "Accept-Language=cs*"})
        

Theory of mapping a request to a controller method in Spring MVC

Following figure shows request's lifecycle in a Spring application.

  1. The first place where the request is processed by an application is DispatcherServlet. There is only one servlet of this kind which processes all incoming requests.
  2. Then DispatcherServlet in its doDispatch method tries to find handler capable of processing request. DispatcherServlet holds a list of HandlerMappings built on application start. These mappings tells the servlet which particular mapping is suitable for processing request. RequestMappingHandlerMapping is interesting in our case because it maps a controller method according to @RequestMapping annotation. If request matches conditions specified by @RequestMapping annotation then a request handler will be send back to the servlet. The handler is instance of HandlerMethod wrapped in HandlerExecutionChain object and actually it's kind of pointer to the controller method.
  3. Handler (HandlerMethod) is passed to the HandlerAdapter's specialization RequestMappingHandlerAdapter and then executed. Actually RequestMappingHandlerAdapter can be easily renamed as HandlerMethodHandlerAdapter because this adapter doesn't have almost no relation to @RequestMapping annotation. This will be useful in one of my solutions.
  4. The handler is executed in RequestMappingHandlerAdapter.invokeHandlerMethod method. Result is wrapped into ModelAndView object and then send back to the servlet.
  5. The servlet resolves a view, processes it, and so on.

Solution #1: Custom request condition

Spring allows to extend @RequestMapping with a custom condition. By overwriting RequestMappingHandlerMapping.getCustomMethodCondition or RequestMappingHandlerMapping.getCustomTypeCondition methods you can create your own condition that will (or will not) match requests. If request matches mapping's condition and your custom conditions then handler is returned by the handler mapping.

Custom conditions are created during application's runtime so it can also be changed on application's runtime.

To demonstrate this approach I've created a demo application Request Mapping On Demand (Custom Condition).

Pros:

  • Sort of Spring-way approach.
  • Less custom code.

Cons:

  • In Spring Boot it's not possible (in a sense of "clean way") to extend existing RequestMappingHandlerMapping. By adding new HandlerMapping to the dispatcher you can create pretty mess. Check the comment in CustomConditionRequestMappingHandlerMapping. Eventually this will be fixed soon.
  • You have to process raw HttpServletRequest object in your condition.

This solution was inspired by Rob Hinds's article Spring MVC & custom routing conditions.

Solution #2: Creating a new handler mapping

There is also demo application which demonstrates this approach: Request Mapping On Demand Demo.

In this solution the @RequestMapping annotation is replaced by custom @RequestMappingOnDemand. All conditions of this new annotation are created by application. So there's no conditions evaluated on application's init phase. Except for a pointer to a condition manager which creates these conditions.

The handler mapping RequestMappingHandlerMapping is replaced by RequestMappingOnDemandHandlerMapping. This custom handler mapping is capable of processing conditions attached to a controller method by @RequestMappingOnDemand annotation.

Because the custom handler mapping returns HandlerMethod there is no need to change anything in the adapter.

Pros:

  • More robust handling of requests.
  • Works flawlessly with current version of Spring Boot.

Cons:

  • More custom code.
  • Code duplication in RequestMappingOnDemandHandlerMapping and RequestMappingHandlerMapping.

Conclusion

I've used second solution in my project because of Spring Boot's inability to extend RequestMappingHandlerMapping. I will probably switch to first solution when it will be fixed.