Как стать автором
Обновить

Кастомизируем маппинг контроллеров Spring MVC

Время на прочтение6 мин
Количество просмотров10K

Введение


Недавно столкнулся с задачей реализовать контроллер, различным образом обрабатывающий ситуации, где есть параметры запроса и где их нет. Проблема усугублялась тем, что нужны были именно два разных метода в контроллере. Стандартные возможности Spring MVC не позволяли этого сделать. Пришлось копнуть чуть глубже. Кому интересно — добро пожаловать под кат.


Что мы хотим


Вот небольшой тест, который описывает сущность задачи. 


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

Надеемся, что все разрешится само-собой


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

Однако Spring был не так дружелюбен, как ожидалось, и на выходе получил:


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

Не сдаемся


Были предприняты попытки разделить маппинг этих методов через аннотацию @RequestMapping(params = "some condition"), но к сожалению в используемой версии Spring 5.1.8 нет возможность задать условие на то, чтобы запрос содержал какие-то параметры. Можно задать либо наличие параметров с конкретными именами, либо отсутствие параметров с конкретными именами. Ни то, ни другое не подходило, т.к. параметры могли быть в каждом запросе разными. Хотелось бы написать что-то в этом роде @RequestMapping(params = "*") для указания, что запрос должен содержать какие-то параметры или @RequestMapping(params = "!*")` — для указания, что в запросе не должно быть никаких параметров.


А что в документации?


Покурив документацию, был найден раздел CustomAnnotations, в котором видим:


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

Было принято решение  сделать свою аннотацию, которая позволила указать нужные мне условия на наличие параметров запроса.
 


Цель


Я хочу добавить к методу аннотацию @NoRequestParams, указав таким образом, что этот метод обрабатывает запросы без параметров.


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

Контроллер с нашей аннотацией:


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

Приступим 


Стандартная конфигурация Spring-MVC, активируемая аннотацией @EnableWebMvc описана в классе WebMvcConfigurationSupport. Она инстанцирует бин класса RequestMappingHandlerMapping, который реализует маппинг, используя для этого объекты класса RequestMappingInfo, которые в свою очередь инкапсулируют в себе информацию о маппинге запросов на методы. Эта информация представлена в виде условий — объектов классов, реализующих интерфейс RequestCondition. В Spring имеются 6 готовых реализаций:



В дополнение к этим реализациям мы можем определить свою. Для этого нам нужно реализовать интерфейс RequestCondition, и использовать эту реализацию для своих нужд. Можно реализовать интерфейс напрямую или же воспользоваться абстрактным классом
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;
    }
}

Первые два метода используются для строкового представления нашего условия.


Метод T combine(T other) нужен для комбинирования условий, например, при наличии аннотации на методе и классе. В таком случае для комбинирования используется метод combine. Наша аннотация не предполагает комбинирования — поэтому мы просто возвращаем наш текущий экземпляр условия.


Метод int compareTo(T other, HttpServletRequest request) служит для сравнения условий в контексте некоего запроса. Т.е. при наличии нескольких однотипных условий для запроса, выясняет какое из них наиболее специфично. Но опять же наше условие единственно возможное поэтому мы просто возвращаем 0, т.е. все наши условия равны между собой.


Основная логика работы содержится в методе T getMatchingCondition(HttpServletRequest request). В этом методе мы должны решить для запроса, применяется ли к нему наше условие или нет. Если да, то возвращаем объект условия. Если нет — возвращаем null. В нашем случае мы возвращаем объект условия, если запрос не содержит в себе никаких параметров.


Теперь нам нужно подключить наше условие в процесс маппинга. Чтобы это сделать мы унаследуемся от стандартной реализации RequestMappingHandlerMapping
и переопределим метод getCustomMethodCondition(Method method), который как раз и создан для того, чтобы добавлять свои кастомизированые условия. Причем этот метод используется при определении условий для методов контроллера. Есть еще метод getCustomTypeCondition(Class<?> handlerType), который можно использовать для определения условий на основе информации о классе контроллера. В нашем случае он нам не нужен.


В итоге имеем следующую реализацию:


CustomRequestMappingHandlerMapping.java
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

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

Логика не сложная — проверяем наличие нашей аннотации и, если она присутствует, возвращаем объект нашего условия.


Чтобы подключить нашу реализацию маппинга, расширим стандартую Spring MVC конфигурацию:


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

    @Bean
    public TestController getTestController() {
        return new TestController();
    }
}

Добавляем нашу аннотацию в контроллер:


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

и проверяем результат:


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

Таким образом мы можем изменять логику маппинга в соответсвие с нашими нуждами и придумывать свои условия. Например, условия на IP адрес или же на используемый User-agent. Возможно, есть и более простые решения, но в любом случае собирать свой велосипед тоже иногда полезно.


Спасибо за внимание.


Код примера на гитхабе

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+5
Комментарии5

Публикации

Истории

Работа

Java разработчик
350 вакансий

Ближайшие события

24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань