Motivation
I want to generate request mappings during execution time of my application. I want to have several URL paths that points to a single 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
/broccoli
[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*"})
Request mapping to a controller method in Spring MVC
Following figure shows request's lifecycle in a Spring application.

-
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. -
Then
DispatcherServlet
in itsdoDispatch
method tries to find handler capable of processing request.DispatcherServlet
holds a list ofHandlerMapping
s 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 ofHandlerMethod
wrapped inHandlerExecutionChain
object and actually it's kind of pointer to the controller method. -
Handler (
HandlerMethod
) is passed to theHandlerAdapter
's specializationRequestMappingHandlerAdapter
and then executed.Actually
RequestMappingHandlerAdapter
could've been named asHandlerMethodHandlerAdapter
, because this adapter doesn't have almost no relation to@RequestMapping
annotation. This will be useful in one of my solutions. -
The handler is executed in
RequestMappingHandlerAdapter.invokeHandlerMethod
method. Result is wrapped intoModelAndView
object and then send back to the servlet. -
The servlet resolves a view, processes it, and so on.
Solution: 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.
Here is a small demonstration of this approach. Of course in real-world application WebMvcRegistrations
and controller shouldn't be combined.
@Controller
public class VegetableController extends WebMvcRegistrationsAdapter {
private VegetableService vegetableService;
@GetMapping("/{name}")
public String vegetable(@PathVariable("name") String normalizedTitle, Model model) {
// TODO Do something...
}
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new VegetableRequestMappingHandlerMapping();
}
public class VegetableRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private VegetableRequestCondition condition = null;
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
if (method.getDeclaringClass() == VegetableController.class && "vegetable".equals(method.getName())) {
if (condition == null) {
condition = new VegetableRequestCondition();
}
return condition;
}
return null;
}
}
public class VegetableRequestCondition implements RequestCondition<VegetableRequestCondition> {
@Override
public VegetableRequestCondition getMatchingCondition(HttpServletRequest request) {
String lang = request.getHeader("Accept-Language");
String vegetableName = request.getServletPath();
if (vegetableService.exists(lang, vegetableName)) {
return this;
}
return null;
}
@Override
public VegetableRequestCondition combine(VegetableRequestCondition other) {
throw new UnsupportedOperationException("getMatchingCondition should not return multiple conditions so there is no need for a combination!");
}
@Override
public int compareTo(VegetableRequestCondition other, HttpServletRequest request) {
throw new UnsupportedOperationException("getMatchingCondition should not return multiple conditions for a request " + request.getPathInfo() + " so there is no need for a comparison");
}
}
}