一尘不染

使用表单主体的Spring MVC Controller方法映射

spring-boot

我正在构建一个小型应用程序,以充当此处工作的某些第三方库的客户端。API指出,Webhook需要a来响应一些异步事件,但是除了_method调用之间的字段更改之外,它们的所有方法都具有相同的签名。例如,我有个_method=
pingmedia

我想在控制器上使用单独的方法来对这些方法中的每一个进行响应。如果应用程序允许我为每种方法指定不同的URL,则对每种方法使用Spring
MVC会很容易@RequestMapping。但是我必须指定一个端点来接收所有呼叫。

有没有一种方法(例如,使用Spring
HttpMessageConverter或类似方法)根据请求主体是什么来映射不同的控制器方法?我已经尝试过@RequestBody@RequestParam但似乎没有找到任何东西。

我真的 非常 不想case, switch在前端控制器上使用一堆方法来根据_methodPOST数据附带的字段来调度操作,因此我碰巧相信有人以前曾遇到过这个问题并可以智能地解决它。

非常感谢!

编辑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"
}

阅读 284

收藏
2020-05-30

共1个答案

一尘不染

是的,我知道了。答案有些棘手,所以如果有人遇到这种问题,我想在这里注册。

@Neil McGuigan在他的评论中为我指出了正确的方向,但起初我没有注意。罪魁祸首是我们的远程应用程序方面非常 非常 糟糕的API设计。

_method是用于指定非标准HTTP动词诸如场PUTPATCHDELETETRACE等。该字段由过滤,HiddenHttpMethodFilterHttpServletRequest使用此“新”方法进行包装。您可以在文件源中看到其工作方式。

因为我希望该_method字段通过过滤器而不修改整个请求(并且由于没有诸如RequestMethod
ping或之类的动词而导致错误message),所以我首先必须停用过滤器。这可以通过两种方式完成:

  1. 我可以阻止Spring Boot自动配置Spring MVC,WebMvcAutoConfiguration而在加载时跳过ApplicationContext加载。正如你能想象这是一个很大很大的,BIIIIG NO 因为,事情可能发生。

  2. 我可以使用FilterRegistrationBean来禁用错误的过滤器。非常简单明了,这是我选择使用的方法:

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

最后但并非最不重要HiddenHttpMethodFilter的一点是,我决定稍作扩展,以某种方式 改进 请求的处理方式。Java
EE规范在Servlet规范指令中非常清楚地指出:

不应该 改变您的要求。您必须尊重发件人(类似的东西)

尽管我同意这一点,但是为了我的心理稳定,我还是决定更改它。为此,我们可以使用simple
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列表中时用新标头包装请求。这样,我可以轻松使用@RequestMappingheaders属性并将请求映射到正确的控制器方法。

回到最初的问题,我几乎可以肯定(99.95%应该完全确定,但不要冒险),该params属性@RequestMapping仅对GET
URI上的请求参数有效http://foo.bar/?baz=42。它不能过滤请求正文上发送的参数。

感谢尼尔的指导,即使很小!我希望这可以帮助别人。

2020-05-30