Ajax 直接发送 PUT 请求,后端无法接收到数据的原因及解决方案
一、问题描述:
使用 Ajax 直接发送 PUT 请求,但 Spring MVC 封装的对象中,除过 URI 中带有的 id 字段被成功封装,请求体中的数据没有被封装到对象中。
通过测试,前端传来的请求体中有数据;通过 HttpServletRequest 对象,使用 request.getParameter() 方法却也获取不到数据
二、解决方案:
在 web.xml 中添加 HttpPutFormContentFilter 过滤器,原理向下看:
1 <filter> 2 <filter-name>httpPutFormContentFilter</filter-name> 3 <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class> 4 </filter> 5 <filter-mapping> 6 <filter-name>httpPutFormContentFilter</filter-name> 7 <url-pattern>/*</url-pattern> 8 </filter-mapping>
三、原因分析:
1. Tomcat 会将请求体中的数据封装成一个 Map,调用 request.getParameter() 会从这个 Map 中取值,Spring MVC 封装 POJO 对象时,对于对象中的每个属性值也会调用 request.getParameter() 方法去获取,然后赋值给对象相应的属性,完成属性封装。
2. 但是,对于 PUT 请求,Tomcat 不会封装请求体中的数据为一个 Map,只会将 POST 请求的请求体中数据封装成 Map;这样的话,无论是直接调用 request.getParameter() 方法,还是 Spring MVC 封装对象,肯定都拿不到属性值了。
四、源码分析:
1. 在 Tomcat 源码中 org.apache.catalina.connector 包的 Request.java 类中 parseParameters() 方法用来解析请求参数,方法先会进行一系列设置和判断,然后再解析请求参数。其中,下面这段代码就会判断请求类型,如果符合 if 条件就会直接返回,也就是判断 parseBodyMethod 中是不是包含当前的请求方式,如果包含,Tomcat 服务器就继续处理解析请求体中的参数,封装到 Map 中;如果不包含,就直接返回,而不再执行下面的方法解析参数,那么 Map 中也就没有相应的参数了。
1 if( !getConnector().isParseBodyMethod(getMethod()) ) { 2 success = true; 3 return; 4 }
2. getConnector() 方法用来获取当前连接对象,然后调用 isParseBodyMethod() 方法判断 parseBodyMethodSet 集合中是不是包含传进来的 method 的值,这个 method 值就是当前的请求方式。
1 /** 2 * Request.java类中 3 * 获取当前的请求方式 4 */ 5 public String getMethod() { 6 return coyoteRequest.method().toString(); 7 } 8 9 /** 10 * Connector.java类中 11 */ 12 protected boolean isParseBodyMethod(String method) { 13 return parseBodyMethodsSet.contains(method); 14 }
3. parseBodyMethodSet 集合中一般使用的都是默认值,也就是相当于将 parseBodyMethods 的值赋给了 parseBodyMethodSet,而 parseBodyMethods 的值默认是 post。
1 /** 2 * 调用 setParseBodyMethods 将 getParseBodyMethods() 获取的 parseBodyMethods 的值赋值给 3 * parseBodyMethods 4 */ 5 protected void initInternal() throws LifecycleException { 6 7 // ... 8 9 // Make sure parseBodyMethodsSet has a default 10 if( null == parseBodyMethodsSet ) { 11 setParseBodyMethods(getParseBodyMethods()); 12 } 13 14 // ... 15 } 16 17 /** 18 * 获取 parseBodyMethods 的值 19 */ 20 protected String parseBodyMethods = "POST"; 21 22 public String getParseBodyMethods() { 23 return this.parseBodyMethods; 24 } 25 26 /** 27 * 定义连接器的规则,如果连接器允许解析非POST请求的请求体,就传入规则,默认都是没有的 28 * 所以 parseBodyMethodSet 使用的是默认值 29 * 将 methods 的值赋值给 parseBodyMethods 30 * 将 methodSet 的值赋值给 parseBodyMethodsSet 31 */ 32 public void setParseBodyMethods(String methods) { 33 34 HashSet<String> methodSet = new HashSet<String>(); 35 36 if( null != methods ) { 37 methodSet.addAll(Arrays.asList(methods.split("\\s*,\\s*"))); 38 } 39 40 // ... 41 42 this.parseBodyMethods = methods; 43 this.parseBodyMethodsSet = methodSet; 44 }
4. 通过前面几步分析,parseBodyMethodSet 中默认只有 post,也只有当前请求方式是 post 的时候才会解析参数,因此,不管是直接通过 Request 对象的 getParameter() 还是 Spring MVC 封装 POJO 对象都不会获取到参数值。
五、HttpPutFormContentFilter 过滤器原理
1 /** 2 * 封装请求体数据,重新包装 Request 对象 3 */ 4 protected void doFilterInternal(final HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 5 // 当是 put 请求或 patch 请求的时候 6 if (("PUT".equals(request.getMethod()) || "PATCH".equals(request.getMethod())) && this.isFormContentType(request)) { 7 HttpInputMessage inputMessage = new ServletServerHttpRequest(request) { 8 // 获取请求体中的数据流,封装成HttpInputMessage 9 public InputStream getBody() throws IOException { 10 return request.getInputStream(); 11 } 12 }; 13 14 // 将上一步封装的 HttpInputMessage 读取封装成一个MultiValueMap 对象, 15 // 这个对象继承自 Map 16 // 将请求体中的数据封装成 Map 17 MultiValueMap<String, String> formParameters = this.formConverter.read((Class)null, inputMessage); 18 19 // 使用 HttpPutFormContentRequestWrapper 重新包装 Request 对象 20 HttpServletRequest wrapper = new HttpPutFormContentFilter.HttpPutFormContentRequestWrapper(request, formParameters); 21 filterChain.doFilter(wrapper, response); 22 } else { 23 filterChain.doFilter(request, response); 24 } 25 } 26 27 /** 28 * 包装 Request 对象具体实现 29 * 重写父类的 getParameter() 等方法,先调用父类的方法 30 * 如果能获取到就是用父类获取的; 31 * 如果获取不到,就是用当前类获取的。 32 */ 33 private static class HttpPutFormContentRequestWrapper extends HttpServletRequestWrapper { 34 private MultiValueMap<String, String> formParameters; 35 36 public String getParameter(String name) { 37 String queryStringValue = super.getParameter(name); 38 String formValue = (String)this.formParameters.getFirst(name); 39 return queryStringValue != null ? queryStringValue : formValue; 40 } 41 42 // ... 43 }