記一次getParameter()獲取不到參數問題的排查


這不是簡單的獲取不到參數的問題.

一、問題背景

項目使用springboot+mybatis, 是一個后台系統. 其中有一些功能:

  • 有一個自定義的全局過濾器DecryptFilter, 用於接口的解密與驗簽
  • 有一個自定義的全局過濾器LogFilter, 用於打印所有接口的請求參數與響應內容.
    因此我們自定義了一個RequestWrapper對象包裝了原本的HttpServletRequest, 並將其中的InputStream保存起來, 方便后面的代碼去獲取請求體或者請求流.
    基於此背景的項目, 在新版本的迭代中, 一個表單提交的POST接口的處理便出現了問題.

二、問題現象.

問題的現象不僅在新版本中, 老版本也有. 只不過沒引起注意, 在這次的問題暴露出來才排查到的.

  • 在新的迭代中, Service層的邏輯代碼中, 通過httpServletRequest.getParameter()獲取參數為空
  • 在新的迭代中, 全局過濾器LogFilter打印了請求體的內容, 與調用方調用時的請求參數一致(不為空).
  • 在老版本中, 全局過濾器LogFilter打印的請求體內容為空, 但是httpServletRequest.getParameter()獲取參數卻是正確的.

三、分析思路

  1. 首先第一反應就想到:

是不是迭代的時候改到了全局過濾器或者自己封裝的RequestWrapper對象, 導致后面再通過getParameter()就獲取不到.
那么對比這幾個關鍵的代碼發現並沒有任何改動, 並且在老版本中getParameter是可以獲取到參數的. 因此這個猜想暫時的排除.

  1. 那接下來根據現象, 之前打印請求體是空, 現在又可以打印出請求體內容(GET請求只會打印QueryString, 其他請求只會打印請求體), 自然想到:

會不會是調用方修改了調用方式, 原來是將請求參數拼接到URL后面, 現在將請求參數放到請求體里面去啦?
這個猜想明顯不能說明為什么新版本使用getParameter獲取參數, 但還是求證了一下. 查看Nginx的訪問日志便可知道, 之前的調用是否是將參數拼接在URL之后. 結果自然是沒有.

  1. 查看getParameter的方法說明如圖:

    里面有重要的一點說明, 就是如果在POST請求中, 你直接使用getInputStream或getReader來獲取請求體的話, 會對getParameter方法的執行造成影響. 但是什么影響呢, 目前並不清楚.

  2. 沒有其他思路, 那便模擬接口調用, 在本地進行DEBUG看看問題出現在哪里.
    在對新老版本分別DEBUG的時候, getParameter方法的執行路徑在RequestWrapper中都一摸一樣符合預期, 也進一步佐證了不會是第一個猜想. 然后在專門對新版本進行DEBUG時發現了一些有趣的現象

  • 在全局過濾器的第一行進行getParameter操作, 發現是可以獲取到參數的.
  • 慢慢調試發現, 在生成RequestWrapper對象之后進行getParameter就獲取不到參數了.
  • 並且如果在new RequestWrapper之前進行了getParameter操作, 那么在后續的Service層中, 也可以獲取到參數了. 但是RequestWrapper中獲取到的原始InputStream就為空了
  • 如果是在new RequestWrapper之后進行的getParameter操作時, 其中獲取原始的InputStream不為空.
    但是new RequestWrapper()里面啥也沒做呀, 就只有關鍵一行代碼如下:
body = StreamUtil.readBytes(request.getReader(), "UTF-8");

而且老版本也是一樣的代碼呀, 卻可以正常獲取到參數值呀.

  1. 帶着疑問繼續DEBUG到getParameter內部具體實現, 到了org.apache.catalina.connector.Request這個類中, 如下圖:

    解釋就是, 如果參數沒有被解析, 那么就去解析, 否則就直接獲取. OK那么繼續DEBUG發現當時是未解析的, 因此就進入到了詳細的參數解析中, 前一部分代碼如圖:

    結果呢, 在執行到紅框那部分代碼時, 便直接return了, 難怪沒有參數呢. 那為啥直接return不繼續解析了呢. 那就看看usingInputStream和usingReader到底干啥的.
    跟蹤這兩個變量值發現:

只有在getInputStream()方法中, usingInputStream=true. 而只有在getReader()方法中, usingReader=true.
那這就表明, 只要你事先調用過了getInputStream或者getReader, 再調用getParameter就不會進行解析了,也就解釋了為啥獲取不到參數.
所以還真的是RequestWrapper的錯!? 那為啥老版本沒有問題??

  1. 其實老版本也是有問題的, 像最開始所說的, 只是沒有引起注意而已:

在老版本中, 全局過濾器LogFilter打印的請求體內容為空, 但是httpServletRequest.getParameter()獲取參數卻是正確的.
那就要找到為啥老版本不打印參數了. 現在就開始DEBUG老版本.
首先, 同樣在全局過濾器的第一行獲取請求體進行打印, 發現......getReader()獲取到的是空的. 我第一行打印都是空的是怎么回事?!!之前都執行過啥什么!
然后, 看了一眼DEBUG的棧調用路徑, 發現調用路徑與新版本不一樣啊, 執行的一些Spring的過濾器也不一樣. 為啥突然調用路徑都變化了?
接着, 由於沒有好的解釋, 只能使用排除法, 切換回DEBUG新版本, 優先還原配置相關的改動一點點的試.
最后, 在我將新增加的WebMvcConfigurationSupport配置注釋掉之后, 發現新版本的表現與老版本一致了.將WebMvcConfigurationSupport作為關鍵詞搜了一下, 發現這貨居然關聯着WebMvcAutoConfiguration這個自動配置.

Springboot只要加了這貨, 就不會加載WebMvcAutoConfiguration, 看來自動配置還是坑多呀. 而且新版本缺少的正是上圖下面框起來的過濾器.

  1. 那這些默認自動配置的過濾器為啥就能讓getReader為空呢? 找到HiddenHttpMethodFilter這個過濾器, 發現了下面的代碼

    原來這個過濾器里面執行了一次getParameter(). 這下所有問題都能解釋通了.

四、總結

  • Tomcat的ServletRequest中, getParameter()方法與getInputStream()/getReader()不兼容, 只能選擇一方.調用了一方, 另一方就會是空的(前提:表單的POST請求).
  • WebMvcConfigurationSupport配置會導致WebMvcAutoConfiguration不加載
  • WebMvcAutoConfiguration中會加載一些配置, 可能影響你的一些行為.
  • 最后, 為了降低影響跟老版本保持一致, 還是采用了WebMvcAutoConfiguration, 去掉了新增的WebMvcConfigurationSupport, 使用了WebMvcRegistrations來作為代替.

最后有兩點疑問:

  1. 為什么使用@RequestBody或者@RequestParam可以獲取到請求參數
  2. Servlet為什么會將請求體保存在流中,只能讀一次? 這樣多次讀取請求體都不方便. 為什么不直接存在一個字節數組中?


免責聲明!

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



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