Spring中GET請求參數(shù)偶發(fā)性丟失問題分析及修復
一、問題現(xiàn)象
最近偶遇一詭異棘手問題:一個用于獲取 token
的 GET
接口,在生產環(huán)境不定期偶發(fā)出現(xiàn) 參數(shù)不存在 的問題。一度懷疑是前端的鍋,雖然前端同學再三以人格擔保!經(jīng)過長時間觀察,發(fā)現(xiàn)每每出現(xiàn)問題時,“再點一下就好了”!錯誤信息簡單明確,是大家熟知的參數(shù)缺失異常:
Required request parameter ‘phone’ for method parameter type String is not present
這是怎么回事呢?這只是再普通不過的一個 GET 接口!
二、問題分析
2.1 發(fā)生時間
由于項目使用的是 Spring Cloud 微服務框架,當請求從瀏覽器發(fā)送過來后,經(jīng)過了以下步驟:
順著這個思路逐層排查:
- HTTP請求: F12查看參數(shù)正常,排除。
- Nginx: 日志打印參數(shù)正常,排除。
- Gateway: 日志打印參數(shù)正常,排除。
- Controller: 參數(shù)丟失。。。
所以可以得出結論:參數(shù)丟失問題發(fā)生在 Spring Cloud 微服務內部。
2.2 發(fā)生位置
我們進一步分析,在過濾器增加請求參數(shù)的打?。?/p>
LogFilter.java
import lombok.extern.slf4j.Slf4j; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j public class LogFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { log.info(">>>>>>>>>>【INFO】request.getQueryString(): {}", httpServletRequest.getQueryString()); log.info(">>>>>>>>>>【INFO】request.getParameter(): {}", httpServletRequest.getParameter("phone")); filterChain.doFilter(httpServletRequest,httpServletResponse); } }
再次復現(xiàn)問題后,在同一個 traceId 對應的日志中,打印結果如下:
可以發(fā)現(xiàn)在問題請求中 request.queryString()
正常,而 request.getParameter()
值卻沒有獲取到!
眾所周知,SpringBoot 默認內置 tomcat 容器,SpringMVC 則通過 request.getParameter() 方法獲取并綁定 Controller 接口參數(shù)的。因此,初步判斷:在 tomcat 獲取 parameter 參數(shù)的時候出現(xiàn)了問題。
那么,parameter 參數(shù)的獲取過程是怎樣的?
- SpringMVC 框架通過
DispatcherServlet
實現(xiàn)。 - Tomcat 接收到外部請求,將由 connector 通過 Processor 受理 http 請求。
- SpringMVC 通過 request.getParameter() 獲取并綁定 Controller 接口參數(shù)。
- request.getParameter() 方法 在請求處理過程中僅在第一次調用 時通過解析 queryString 獲取 parameters 參數(shù)值,并設置
didQueryParameter=true
標識已解析處理。 - Http 請求處理完成,processor 通過 release 方法釋放連接重置參數(shù)屬性,request.recycle 方法重置 request 參數(shù)屬性(注意:這里 連接器及 request 對象并不會銷毀,connector 再次受理新的請求時,將復用連接器、processor 及 request 對象而非創(chuàng)建)。
2.3 源碼解析
下面,我們可以看一些源碼的片段來驗證一下:
源碼1:SpringBoot 從 request 獲取 parameter 參數(shù)。
RequestParamMethodArgumentResolve
類的 resovleName()
方法,可以看到這里調用了 request.getParameterValue() 方法。
源碼2:tomcat 封裝了解析參數(shù)。
org.apache.catalina.connector.Request
類的 getParameterValues()
方法,request 通過 Parameters 獲取 parameter 參數(shù)。
源碼3:Parameters 從 queryString 解析封裝 parameter 參數(shù)。
org.apache.tomcat.util.http.Parameters
類的 handleQueryParameters()
方法,可以發(fā)現(xiàn),參數(shù)在解析處理后會設置 didQueryParameters
參數(shù)為 true。
源碼4:請求處理結束,重置參數(shù)屬性,并不銷毀對象。
org.apache.tomcat.util.http.Parameters
類的 recycle()
方法。
2.4 Tomcat機制
Tomcat 機制如下:
- tomcat 可支持多個 service 示例;
- 每個 service 實例維護了一個包含多個 connector 的連接池;
- 當 service 接收到了一個 http 請求時,則從 connector 池中獲取一個 connector 連接器進行響應處理。
- connector 連接器是通過 Processor 對應 HTTP 請求進行響應處理。
Processor 封裝了 request
、response
對象,在請求處理開始時進行初始化封裝(進封裝參數(shù)屬性,并不創(chuàng)建對象),請求處理完成后,則進行釋放重置。(注意:這里的釋放僅指重置參數(shù)屬性,并不銷毀對象!)
2.5 原因總結
本次問題的根本原因在于 線程中引用了 request 對象,并在線程中調用了 request.getParameter()
方法使參數(shù)屬性 didQueryParameter
錯誤而導致 http 請求無法正確獲取參數(shù)值。
- 假設第一次受理 http 請求的連接器為 connector1;
- 請求 request 在子線程 thread1 中被引用;
- connector1 完成 http 請求并執(zhí)行 release 釋放連接,這時
request.didQueryParameters
值為 false; - 如果子線程 thread1 處理任務的時間較長,調用了 getParameter() 方法,這時
request.didQueryParameters
值將再次被更新為 true; - 當 tomcat 再次通過 connector1 受理新的 http 請求時,由于 request.didQueryParameters=true,這時新請求調用 getParameter() 方法將不會再解析 queryString,因而無法正確獲取 parameter 參數(shù)值。
三、問題復現(xiàn)
這里為了方便,我們使用 Hutool
的線程池工具。依賴如下:
<!-- Hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.23</version> </dependency>
復現(xiàn)代碼如下:
DemoController.java
import cn.hutool.core.thread.ThreadUtil; import com.demo.common.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Slf4j @RestController @RequestMapping("/demo") public class DemoController { /** * 根據(jù)手機號獲取token */ @GetMapping("/getToken") public Result<Object> getToken(@RequestParam String phone) { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); ThreadUtil.execute(() -> { RequestContextHolder.setRequestAttributes(attributes); ThreadUtil.safeSleep(1000); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); System.out.println("********** " + request.getParameter(phone)); }); return Result.succeed(); } }
使用 Jmeter
壓測工具,設置 200 線程并發(fā)請求:
壓測 http://localhost:8080/demo/test?phone=111111 接口,配置請求信息如下:
成功復現(xiàn),結果如下所示:
四、問題修復
修復這個問題的話有兩種方式:
方式一: GET 請求改為 POST請求,使用 JSON 格式傳輸數(shù)據(jù)。
(經(jīng)過嘗試,即使使用 POST 請求,不使用 JSON 格式傳輸數(shù)據(jù)的話,還是會丟失參數(shù)。)
方式二: 將 tomcat 中間件替換為 undertow 中間件。修改后如下所示:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
將 tomcat 替換為 undertow 之后,發(fā)現(xiàn)不再出現(xiàn)參數(shù)丟失的情況。
以上就是Spring中GET請求參數(shù)偶發(fā)性丟失問題分析及修復的詳細內容,更多關于Spring GET請求參數(shù)丟失的資料請關注腳本之家其它相關文章!
相關文章
SpringCloud使用AOP統(tǒng)一處理Web請求日志實現(xiàn)步驟
這篇文章主要為大家介紹了SpringCloud使用AOP統(tǒng)一處理Web請求日志實現(xiàn)步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08Spring?Data?JPA?注解Entity關聯(lián)關系使用詳解
這篇文章主要為大家介紹了Spring?Data?JPA?注解Entity關聯(lián)關系使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09springboot根據(jù)實體類生成表的實現(xiàn)方法
本文介紹了如何通過SpringBoot工程引入SpringDataJPA,并通過實體類自動生成數(shù)據(jù)庫表的過程,包括常見問題解決方法,感興趣的可以了解一下2024-09-09spring基于通用Dao的多數(shù)據(jù)源配置詳解
這篇文章主要為大家詳細介紹了spring基于通用Dao的多數(shù)據(jù)源配置,具有一定的參考價值,感興趣的小伙伴們可以參考一下解2018-03-03一不小心就讓Java開發(fā)踩坑的fail-fast是個什么鬼?(推薦)
這篇文章主要介紹了Java fail-fast,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04SpringBoot項目中JDK動態(tài)代理和CGLIB動態(tài)代理的使用詳解
JDK動態(tài)代理和CGLIB動態(tài)代理都是SpringBoot中實現(xiàn)AOP的重要技術,JDK動態(tài)代理通過反射生成代理類,適用于目標類實現(xiàn)了接口的場景,性能較好,易用性高,但必須實現(xiàn)接口且不能代理final方法,CGLIB動態(tài)代理通過生成子類實現(xiàn)代理2025-03-03