Personnaliser le mappage des contrôleurs Spring MVC

Présentation


Récemment, j'ai été confronté à la tâche d'implémenter un contrôleur qui gère les situations de différentes manières où il existe des paramètres de demande et où il n'y en a pas. Le problème était aggravé par le fait qu'exactement deux méthodes différentes étaient nécessaires dans le contrôleur. Les fonctionnalités standard de Spring MVC ne le permettaient pas. J'ai dû creuser un peu plus profondément. Peu importe - bienvenue au chat.


Que voulons-nous


Voici un court test qui décrit l'essence de la tâche.


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()));     } } 

Nous espérons que tout sera résolu par lui-même


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;    } } 

Cependant, Spring n'était pas aussi convivial que prévu, et la sortie était:


 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.` 

N'abandonne pas


Des tentatives ont été faites pour séparer le mappage de ces méthodes via l' @RequestMapping(params = "some condition") , mais malheureusement dans la version utilisée de Spring 5.1.8 il n'y a aucun moyen de définir la condition pour que la requête contienne des paramètres. Vous pouvez spécifier la présence de paramètres avec des noms spécifiques ou l'absence de paramètres avec des noms spécifiques. Ni l'un ni l'autre ne convenaient, car les paramètres peuvent être différents dans chaque demande. Je voudrais écrire quelque chose @RequestMapping(params = "*") pour indiquer que la demande doit contenir certains paramètres ou @RequestMapping (params = "! *") `Pour indiquer ce qui ne devrait pas être dans la demande être aucun paramètre.


Et que contient la documentation?


Après avoir fumé la documentation, la section CustomAnnotations a été trouvée, dans laquelle nous voyons:


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

Il a été décidé de faire mon annotation, ce qui m'a permis de préciser les conditions dont j'avais besoin pour la disponibilité des paramètres de requête.


But


Je veux ajouter l'annotation @NoRequestParams à la méthode, indiquant que cette méthode traite les demandes sans paramètres.


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

Contrôleur avec notre 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;    } } 

Commençons


La configuration Spring-MVC standard, activée par l'annotation @EnableWebMvc , est décrite dans la classe WebMvcConfigurationSupport . Il instancie un bean de la classe RequestMappingHandlerMapping , qui implémente le mappage à l'aide des objets de la classe RequestMappingInfo , qui à leur tour encapsule des informations sur le mappage des demandes de méthode. Ces informations sont présentées sous forme de conditions - objets de classe qui implémentent l'interface RequestCondition. Spring a 6 implémentations prêtes à l'emploi:



En plus de ces implémentations, nous pouvons définir la nôtre. Pour ce faire, nous devons implémenter l'interface RequestCondition et utiliser cette implémentation pour nos besoins. Vous pouvez implémenter l'interface directement ou utiliser une classe abstraite
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;    } } 

Les deux premières méthodes sont utilisées pour représenter notre condition en chaîne.


La méthode T combine (T other) est nécessaire pour combiner des conditions, par exemple, s'il y a une annotation sur la méthode et la classe. Dans ce cas, la méthode combine est utilisée pour la combinaison. Notre annotation n'implique pas une combinaison - par conséquent, nous renvoyons simplement notre instance actuelle de la condition.


La méthode int compareTo (T other, HttpServletRequest request) est utilisée pour comparer les conditions dans le contexte d'une certaine demande. C'est-à-dire s'il existe plusieurs conditions du même type pour la demande, il recherche celle qui est la plus spécifique. Mais encore une fois, notre condition est la seule possible, donc nous retournons simplement 0, c'est-à-dire Toutes nos conditions sont égales.


La logique de base du travail est contenue dans la méthode T getMatchingCondition (requête HttpServletRequest) . Dans cette méthode, nous devons décider de demander si notre condition s'y applique ou non. Si tel est le cas, renvoyez l'objet condition. Sinon, retournez null . Dans notre cas, nous retournons un objet condition si la requête ne contient aucun paramètre.


Maintenant, nous devons inclure notre condition dans le processus de cartographie. Pour ce faire, nous hériterons de l'implémentation standard de RequestMappingHandlerMapping
et redéfinissez la getCustomMethodCondition(Method method) , qui vient d'être créée afin d'ajouter vos propres conditions personnalisées. De plus, cette méthode est utilisée pour déterminer les conditions des méthodes du contrôleur. Il existe également une getCustomTypeCondition(Class<?> handlerType) , qui peut être utilisée pour déterminer les conditions en fonction des informations sur la classe du contrôleur. Dans notre cas, nous n'en avons pas besoin.


En conséquence, nous avons l'implémentation suivante:


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

La logique n'est pas compliquée - on vérifie l'existence de notre annotation et, si elle est présente, on retourne l'objet de notre condition.


Pour activer notre implémentation de mappage, nous allons étendre la configuration Spring MVC standard:


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

Ajoutez notre annotation au contrôleur:


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;    } } 

et vérifiez le résultat:


 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 

Ainsi, nous pouvons changer la logique de la cartographie en fonction de nos besoins et arriver à nos propres conditions. Par exemple, des conditions sur l'adresse IP ou sur l'agent utilisateur utilisé. Il peut y avoir des solutions plus simples, mais dans tous les cas, l'assemblage de votre vélo est également parfois utile.


Merci de votre attention.


Exemple de code Github

Source: https://habr.com/ru/post/fr478766/


All Articles