Customize the mapping of Spring MVC controllers

Introduction



Recently I was faced with the task of implementing a controller that handles situations in various ways where there are request parameters and where they are not. The problem was compounded by the fact that exactly two different methods were needed in the controller. The standard features of Spring MVC did not allow this. I had to dig a little deeper. Who cares - welcome to cat.







What do we want



Here is a short test that describes the essence of the task.







TestControllerTest.java
@SpringJUnitWebConfig(WebConfig.class) class TestControllerTest {     @Autowired     WebApplicationContext webApplicationContext;     @Test     void testHandleRequestWithoutParams() throws Exception {         MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();         mockMvc.perform(MockMvcRequestBuilders.get("/test"))                 .andExpect(status().isOk())                 .andExpect(result ->                         assertEquals(TestController.HANDLE_REQUEST_WITHOUT_PARAMS, result.getResponse().getContentAsString()));     }     @Test     void testHandleRequestWithParams() throws Exception {         MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();         mockMvc.perform(MockMvcRequestBuilders                 .get("/test")                 .param("someparam", "somevalue"))                 .andExpect(status().isOk())                 .andExpect(result ->                         assertEquals(TestController.HANDLE_REQUEST_WITH_PARAMS, result.getResponse().getContentAsString()));     } }
      
      





We hope that everything will be resolved by itself



TestController.java
 @Controller @RequestMapping("/test") public class TestController {    public static final String HANDLE_REQUEST_WITH_PARAMS = "handleRequestWithParams";    public static final String HANDLE_REQUEST_WITHOUT_PARAMS = "handleRequestWithoutParams";    @GetMapping    @ResponseBody    public String handleRequestWithParams(SearchQuery query) {        return HANDLE_REQUEST_WITH_PARAMS;    }    @GetMapping    @ResponseBody    public String handleRequestWithoutParams() {        return HANDLE_REQUEST_WITHOUT_PARAMS;    } }
      
      





However, Spring was not as friendly as expected, and the output was:







 java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'getTestController' method public java.lang.String ru.pchurzin.spring.customannotations.TestController.handleRequestWithoutParams() to {GET /test}: There is already 'getTestController' bean method public java.lang.String ru.pchurzin.spring.customannotations.TestController.handleRequestWithParams(ru.pchurzin.spring.customannotations.SearchQuery) mapped.`
      
      





Don't give up



Attempts have been made to separate the mapping of these methods via the @RequestMapping(params = "some condition")



annotation, but unfortunately in the used version of Spring 5.1.8 there is no way to set the condition for the request to contain any parameters. You can either specify the presence of parameters with specific names, or the absence of parameters with specific names. Neither one nor the other was suitable, because Parameters could be different in each request. I would like to write something @RequestMapping(params = "*")



to indicate that the request should contain some parameters or @RequestMapping (params = "! *") `To indicate what should not be in the request to be no parameters.







And what is in the documentation?



Having smoked the documentation, the section CustomAnnotations was found, in which we see:







 Spring MVC also supports custom request-mapping attributes with custom request-matching logic.
      
      





It was decided to make my annotation, which allowed me to specify the conditions I needed for the availability of query parameters.







purpose



I want to add the @NoRequestParams



annotation to the method, thus indicating that this method handles requests without parameters.







 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRequestParams { }
      
      





Controller with our annotation:







Testcontoller.java
 @Controller @RequestMapping("/test") public class TestController {    public static final String HANDLE_REQUEST_WITH_PARAMS = "handleRequestWithParams";    public static final String HANDLE_REQUEST_WITHOUT_PARAMS = "handleRequestWithoutParams";    @GetMapping    @ResponseBody    public String handleRequestWithParams(SearchQuery query) {        return HANDLE_REQUEST_WITH_PARAMS;    }    @GetMapping    @ResponseBody @NoRequestParams //     public String handleRequestWithoutParams() {        return HANDLE_REQUEST_WITHOUT_PARAMS;    } }
      
      





Let's get started



The standard Spring-MVC configuration, activated by the @EnableWebMvc annotation, is described in the WebMvcConfigurationSupport class. It instantiates a bean of the RequestMappingHandlerMapping class, which implements mapping using objects of the RequestMappingInfo class, which in turn encapsulate information about the mapping of method requests. This information is presented in the form of conditions - class objects that implement the RequestCondition interface . Spring has 6 ready-made implementations:











In addition to these implementations, we can define our own. To do this, we need to implement the RequestCondition interface, and use this implementation for our needs. You can implement the interface directly or use an abstract class

AbstractRequestCondition .







NoRequestParamsCondition.java
 public class NoRequestParamsCondition extends AbstractRequestCondition<NoRequestParamsCondition> {    public final static NoRequestParamsCondition NO_PARAMS_CONDITION = new NoRequestParamsCondition();    @Override    protected Collection<?> getContent() {        return Collections.singleton("no params");    }    @Override    protected String getToStringInfix() {        return "";    }    @Override    public NoRequestParamsCondition combine(NoRequestParamsCondition other) {        return this;    }    @Override    public NoRequestParamsCondition getMatchingCondition(HttpServletRequest request) {        if (request.getParameterMap().isEmpty()) {            return this;        }        return null;    }    @Override    public int compareTo(NoRequestParamsCondition other, HttpServletRequest request) {        return 0;    } }
      
      





The first two methods are used to string represent our condition.







The T combine (T other) method is needed to combine conditions, for example, if there is annotation on the method and class. In this case, the combine method is used for combining. Our annotation does not involve combining - therefore, we simply return our current instance of the condition.







The int compareTo (T other, HttpServletRequest request) method is used to compare conditions in the context of a certain request. Those. if there are several conditions of the same type for the request, it finds out which one is the most specific. But again, our condition is the only possible one, therefore we simply return 0, i.e. All our conditions are equal.







The basic logic of work is contained in the T method getMatchingCondition (HttpServletRequest request) . In this method, we must decide to query whether our condition applies to it or not. If so, then return the condition object. If not, return null



. In our case, we return a condition object if the request does not contain any parameters.







Now we need to include our condition in the mapping process. To do this, we will inherit from the standard implementation of RequestMappingHandlerMapping

and redefine the getCustomMethodCondition(Method method)



, which is just created in order to add your own customized conditions. Moreover, this method is used to determine the conditions for controller methods . There is also a getCustomTypeCondition(Class<?> handlerType)



, which can be used to determine conditions based on information about the controller class. In our case, we do not need it.







As a result, we have the following implementation:







CustomRequestMappingHandlerMapping.java
 public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {    @Override    protected RequestCondition<?> getCustomMethodCondition(Method method) {        return method.isAnnotationPresent(NoRequestParams.class) ? NO_PARAMS_CONDITION : null;    } }
      
      





The logic is not complicated - we check the existence of our annotation and, if it is present, we return the object of our condition.







To enable our mapping implementation, weโ€™ll extend the standard Spring MVC configuration:







Webconfig.java
 @Configuration public class WebConfig extends WebMvcConfigurationSupport {       @Override    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {        return new CustomRequestMappingHandlerMapping();    }    @Bean    public TestController getTestController() {        return new TestController();    } }
      
      





Add our annotation to the controller:







TestController.java
 @Controller @RequestMapping("/test") public class TestController {    public static final String HANDLE_REQUEST_WITH_PARAMS = "handleRequestWithParams";    public static final String HANDLE_REQUEST_WITHOUT_PARAMS = "handleRequestWithoutParams";    @GetMapping    @ResponseBody    public String handleRequestWithParams(SearchQuery query) {        return HANDLE_REQUEST_WITH_PARAMS;    }    @GetMapping    @ResponseBody    @NoRequestParams    public String handleRequestWithoutParams() {        return HANDLE_REQUEST_WITHOUT_PARAMS;    } }
      
      





and check the result:







 Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.851 s - in ru.pchurzin.spring.customannotations.TestControllerTest Results: Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
      
      





Thus, we can change the logic of mapping in accordance with our needs and come up with our own conditions. For example, conditions on the IP address or on the used User-agent. There may be simpler solutions, but in any case, assembling your bike is also sometimes useful.







Thanks for attention.







Github example code












All Articles