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.
@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
@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:
@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:
- PatternsRequestCondition
- RequestMethodsRequestCondition
- ParamsRequestCondition
- HeadersRequestCondition
- ConsumesRequestCondition
- ProducesRequestCondition
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 .
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:
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:
@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:
@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.