Сопоставление метода Spring MVC Controller с использованием тела формы

Я создаю небольшое приложение, которое будет служить клиентом для какой-нибудь сторонней библиотеки здесь, на работе. API утверждает, что Webhookтребуется для ответа на некоторые асинхронные события, но все их методы имеют одну и ту же сигнатуру, за исключением изменения _method поле между звонками. Например, у меня есть _method знак равно ping, media, так далее.

Я хотел бы иметь отдельные методы на моем контроллере, чтобы отвечать за каждый из этих методов. Если бы приложение позволяло мне указывать разные URL для каждого метода, было бы легко использовать Spring MVC @RequestMapping для каждого из них. Но я должен указать одну конечную точку для приема всех вызовов.

Есть ли способ (например, с помощью Spring HttpMessageConverter или что-то в этом роде) для сопоставления различных методов контроллера, основанных на том, что такое тело запроса? Я уже пробовал с @RequestBody, @RequestParam но, похоже, ничего не нашел.

Я действительно, действительно не хотел использовать кучу case, switch методы на фронт-контроллере для отправки действий на основе моего _method поле, которое поставляется с моими данными POST, поэтому я считаю, что кто-то имел эту проблему раньше и решил ее разумно.

Большое спасибо!

Редактировать 1: Предоставление исходного кода

@Controller
@RequestMapping("/webhooks")
public class WebhookController {

    @RequestMapping(method = RequestMethod.POST, params = {"_method=ping"})
    @ResponseBody
    public String ping(){
        return "pong";
    }

    @RequestMapping(method = RequestMethod.POST, params = {"_method=media"})
    @ResponseBody
    public String media(){
        return "media";
    }
}

Это ответ:

{
  "timestamp": 1440875190389,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.UnsatisfiedServletRequestParameterException",
  "message": "Parameter conditions \"_method=ping\" not met for actual request parameters: ",
  "path": "/webhooks"
}

2 ответа

Решение

Хорошо, я получил это работает. Ответ немного хитрый, поэтому я хотел бы зарегистрировать его здесь, если у кого-то возникнет такая проблема.

@ Нил МакГиган в своем комментарии указал мне правильное направление, но сначала я не обратил на это внимания. Основным виновником здесь является очень, очень, очень плохой дизайн API на стороне нашего удаленного приложения.

_method поле, используемое для указания нестандартных HTTP-глаголов, таких как PUT, PATCH, DELETE, TRACE и так далее. Это поле фильтруется HiddenHttpMethodFilter и HttpServletRequest обернут с этим "новым" методом. Вы можете увидеть в источнике файла, как это работает.

Как я этого хотел _method поле, чтобы пройти через фильтр без изменения всего запроса (и вызывая ошибки, потому что нет такого глагола, как pingили же message на RequestMethod) Сначала я должен был деактивировать фильтр. Это можно сделать двумя способами:

  1. Я мог бы остановить Spring Boot от автоматической настройки Spring MVC, пропуская WebMvcAutoConfiguration от загрузки, когда ApplicationContext был загружен. Как вы можете себе представить, это БОЛЬШОЙ, БОЛЬШОЙ, БОЛЬШОЙ НЕТ, потому что, ну, все может случиться.

  2. Я мог бы использовать FilterRegistrationBean отключить плохой фильтр. Довольно простой и понятный метод, который я выбрал для использования:

    @Bean
    public FilterRegistrationBean registration(HiddenHttpMethodFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }
    

Не в последнюю очередь, я решил дать HiddenHttpMethodFilter небольшое расширение, чтобы каким-то образом улучшить процесс обработки запросов. Спецификация Java EE довольно четко описана в командах спецификаций сервлета, где говорится:

Ты не должен изменять свой запрос на вашей стороне. Вы должны уважать отправителя (что-то подобное)

Хотя я согласен с этим, ради моей психической стабильности я все равно решил это изменить. Чтобы достичь этого, мы можем использовать простой HttpServletRequestWrapper, переопределите выбранные методы и отфильтруйте исходный запрос с обернутой частью. В итоге я сделал что-то вроде этого:

public class WhatoolsHiddenHttpMethodFilter extends OrderedHiddenHttpMethodFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String paramValue = request.getParameter(OrderedHiddenHttpMethodFilter.DEFAULT_METHOD_PARAM);
        if("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            List<String> whatoolsMethods = Arrays.asList("ping", "message", "carbon", "media", "media_carbon", "ack");
            if(whatoolsMethods.contains(paramValue)){
                WhatoolsHiddenHttpMethodFilter.HttpMethodRequestWrapper wrapper = new WhatoolsHiddenHttpMethodFilter
                        .HttpMethodRequestWrapper(request, "POST", paramValue);
                filterChain.doFilter(wrapper, response);
            } else {
                WhatoolsHiddenHttpMethodFilter.HttpMethodRequestWrapper wrapper = new WhatoolsHiddenHttpMethodFilter
                        .HttpMethodRequestWrapper(request, method, null);
                filterChain.doFilter(wrapper, response);
            }
        } else {
            filterChain.doFilter(request, response);
        }
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

        private final String whatoolsMethod;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method, String whatoolsMethod) {
            super(request);
            this.method = method;
            this.whatoolsMethod = whatoolsMethod;
        }

        @Override
        public String getMethod() {
            return this.method;
        }

        @Override
        public String getHeader(String name) {
            if("x-whatools-method".equals(name)){
                return this.whatoolsMethod;
            }
            return super.getHeader(name);
        }

        @Override
        public Enumeration<String> getHeaderNames() {
            List<String> names = Collections.list(super.getHeaderNames());
            if(this.whatoolsMethod != null){
                names.add("x-whatools-method");
            }
            return Collections.enumeration(names);
        }
    }
}

Итак, что это делает, чтобы обернуть запрос с новым x-whatools-method заголовок, когда заголовок в моем whatoolsMethods список. С этим я могу легко использовать @RequestMapping"s headers Свойство и сопоставьте запросы к правильному метадоду контроллера.

Возвращаясь к первоначальному вопросу, я почти уверен (ну, 99,95% должны быть полностью уверены, но давайте не будем рисковать) params собственность на @RequestMapping работает только для параметров запроса на GET URI, например http://foo.bar/?baz=42, Не будет работать фильтрация параметров, отправленных в теле запроса.

Спасибо Нейлу за ваше руководство, даже если оно маленькое! Я надеюсь, что это помогает кому-то.

Вы можете использовать params в отображении запроса:

@RequestMapping (value = "/ foo", params = {"_ method = ping"})

Предполагая, что это почтовые параметры,

params Работает на POST, я обещаю вам

Вот мой контроллер:

@Controller
@RequestMapping("/test1")
public class ParamTestController {

    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody String getA(){
        return "A";
    }

    @RequestMapping(method = RequestMethod.POST, params = {"b"})
    @ResponseBody String getB(){
        return "B";
    }
}

Вот мой тест:

введите описание изображения здесь

Другие вопросы по тегам