Ajax 直接發送 PUT 請求,后端無法接收到數據的原因及解決方案


一、問題描述:

    使用 Ajax 直接發送 PUT 請求,但 Spring MVC 封裝的對象中,除過 URI 中帶有的 id 字段被成功封裝,請求體中的數據沒有被封裝到對象中。

    通過測試,前端傳來的請求體中有數據;通過 HttpServletRequest 對象,使用 request.getParameter() 方法卻也獲取不到數據

image

二、解決方案:

    在 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 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM