Сопоставление метода 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) Сначала я должен был деактивировать фильтр. Это можно сделать двумя способами:
Я мог бы остановить Spring Boot от автоматической настройки Spring MVC, пропуская
WebMvcAutoConfiguration
от загрузки, когдаApplicationContext
был загружен. Как вы можете себе представить, это БОЛЬШОЙ, БОЛЬШОЙ, БОЛЬШОЙ НЕТ, потому что, ну, все может случиться.Я мог бы использовать
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";
}
}
Вот мой тест: