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
DispatcherServletin itsdoDispatchmethod tries to find handler capable of processing request.DispatcherServletholds a list ofHandlerMappings built on application start.These mappings tells the servlet which particular mapping is suitable for processing request.
RequestMappingHandlerMappingis interesting in our case because it maps a controller method according to@RequestMappingannotation.If request matches conditions specified by
@RequestMappingannotation then a request handler will be send back to the servlet. The handler is instance ofHandlerMethodwrapped inHandlerExecutionChainobject and actually it's kind of pointer to the controller method. -
Handler (
HandlerMethod) is passed to theHandlerAdapter's specializationRequestMappingHandlerAdapterand then executed.Actually
RequestMappingHandlerAdaptercould've been named asHandlerMethodHandlerAdapter, because this adapter doesn't have almost no relation to@RequestMappingannotation. This will be useful in one of my solutions. -
The handler is executed in
RequestMappingHandlerAdapter.invokeHandlerMethodmethod. Result is wrapped intoModelAndViewobject 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");
}
}
}