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

时间:2015-08-29 17:34:32

标签: spring spring-mvc jackson spring-boot

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

我希望我的控制器上有单独的方法来响应这些方法中的每一种。如果应用程序允许我为每个方法指定不同的URL,那么就可以很容易地为每个方法使用Spring MVC @RequestMapping。但我必须指定一个端点来接收所有呼叫。

有没有办法(例如使用Spring' s HttpMessageConverter或类似的东西)根据Request Body的内容映射不同的控制器方法?我已尝试使用@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 个答案:

答案 0 :(得分:2)

是的,我得到了它的工作。答案有点棘手,所以如果有人有这样的问题,我想在这里注册。

@Neil McGuigan在评论中向我指出了正确的方向,但我起初并没有注意。这里的主要罪魁祸首是我们远程应用程序的一个非常非常非常错误的API设计。

_method是一个字段,用于指定非标准HTTP谓词,例如PUTPATCHDELETETRACE等。此字段按HiddenHttpMethodFilter进行过滤,HttpServletRequest包含此新内容'方法。您可以在the file's source查看其工作原理。

我希望这个_method字段能够通过过滤器而不修改整个请求(并导致错误,因为没有pingmessage这样的动词`RequestMethod)我首先必须停用过滤器。这可以通过两种方式完成:

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

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

    @Bean
    public FilterRegistrationBean registration(HiddenHttpMethodFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }
    
  3. 最后但并非最不重要的是,我决定给HiddenHttpMethodFilter一点扩展,以某种方式改进请求是如何通过的。 Java EE规范在Servlet规范命令中非常清楚,它指出:

      

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

    虽然我同意这一点,但为了我的精神稳定,我决定改变它。为此,我们可以使用简单的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的{​​{1}}属性,并将请求映射到正确的控制器方法。

    回到最初的问题,我几乎可以肯定(好吧,99,95%应该完全确定,但不要冒风险)headers params属性@RequestMapping仅适用于GET URI上的请求参数,例如http://foo.bar/?baz=42。它不会在请求的主体上发送过滤参数。

    感谢Neil的指导,即使很小!我希望这有助于某人。

答案 1 :(得分:1)

您可以在请求映射中使用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";
    }
}

这是我的测试:

enter image description here