Spring中GET請求參數(shù)偶發(fā)性丟失問題分析及修復
一、問題現(xiàn)象
最近偶遇一詭異棘手問題:一個用于獲取 token 的 GET 接口,在生產(chǎn)環(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 微服務(wù)框架,當請求從瀏覽器發(fā)送過來后,經(jīng)過了以下步驟:

順著這個思路逐層排查:
- HTTP請求: F12查看參數(shù)正常,排除。
- Nginx: 日志打印參數(shù)正常,排除。
- Gateway: 日志打印參數(shù)正常,排除。
- Controller: 參數(shù)丟失。。。
所以可以得出結(jié)論:參數(shù)丟失問題發(fā)生在 Spring Cloud 微服務(wù)內(nèi)部。
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 對應(yīng)的日志中,打印結(jié)果如下:

可以發(fā)現(xiàn)在問題請求中 request.queryString() 正常,而 request.getParameter() 值卻沒有獲取到!
眾所周知,SpringBoot 默認內(nèi)置 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() 方法 在請求處理過程中僅在第一次調(diào)用 時通過解析 queryString 獲取 parameters 參數(shù)值,并設(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() 方法,可以看到這里調(diào)用了 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ù)在解析處理后會設(shè)置 didQueryParameters 參數(shù)為 true。

源碼4:請求處理結(jié)束,重置參數(shù)屬性,并不銷毀對象。
org.apache.tomcat.util.http.Parameters 類的 recycle() 方法。


2.4 Tomcat機制
Tomcat 機制如下:
- tomcat 可支持多個 service 示例;
- 每個 service 實例維護了一個包含多個 connector 的連接池;
- 當 service 接收到了一個 http 請求時,則從 connector 池中獲取一個 connector 連接器進行響應(yīng)處理。
- connector 連接器是通過 Processor 對應(yīng) HTTP 請求進行響應(yīng)處理。
Processor 封裝了 request、response 對象,在請求處理開始時進行初始化封裝(進封裝參數(shù)屬性,并不創(chuàng)建對象),請求處理完成后,則進行釋放重置。(注意:這里的釋放僅指重置參數(shù)屬性,并不銷毀對象!)

2.5 原因總結(jié)
本次問題的根本原因在于 線程中引用了 request 對象,并在線程中調(diào)用了 request.getParameter() 方法使參數(shù)屬性 didQueryParameter 錯誤而導致 http 請求無法正確獲取參數(shù)值。
- 假設(shè)第一次受理 http 請求的連接器為 connector1;
- 請求 request 在子線程 thread1 中被引用;
- connector1 完成 http 請求并執(zhí)行 release 釋放連接,這時
request.didQueryParameters值為 false; - 如果子線程 thread1 處理任務(wù)的時間較長,調(diào)用了 getParameter() 方法,這時
request.didQueryParameters值將再次被更新為 true; - 當 tomcat 再次通過 connector1 受理新的 http 請求時,由于 request.didQueryParameters=true,這時新請求調(diào)用 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 壓測工具,設(shè)置 200 線程并發(fā)請求:

壓測 http://localhost:8080/demo/test?phone=111111 接口,配置請求信息如下:

成功復現(xiàn),結(jié)果如下所示:

四、問題修復
修復這個問題的話有兩種方式:
方式一: 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ā)性丟失問題分析及修復的詳細內(nèi)容,更多關(guān)于Spring GET請求參數(shù)丟失的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用線程實現(xiàn)動態(tài)顯示系統(tǒng)時間
編寫Applet小程序,通過在HTML文檔中接收參數(shù),顯示當前的系統(tǒng)時間,需要的朋友可以參考下2015-10-10
關(guān)于ThreadLocal的用法和說明及注意事項
這篇文章主要介紹了關(guān)于ThreadLocal的用法和說明及注意事項,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
Java使用synchronized修飾方法來同步線程的實例演示
synchronized下的方法控制多線程程序中的線程同步非常方便,這里就來看一下Java使用synchronized修飾方法來同步線程的實例演示,需要的朋友可以參考下2016-06-06
SpringBoot讀取properties配置文件中的數(shù)據(jù)的三種方法
本文主要介紹了SpringBoot讀取properties配置文件中的數(shù)據(jù)的三種方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-06-06
SpringBoot項目使用yml文件鏈接數(shù)據(jù)庫異常問題解決方案
在使用SpringBoot時,利用yml進行數(shù)據(jù)庫連接配置需小心數(shù)據(jù)類型區(qū)分,如果用戶名或密碼是數(shù)字,必須用雙引號包裹以識別為字符串,避免連接錯誤,特殊字符密碼也應(yīng)用引號包裹2024-10-10
Mybatis-Plus 多表聯(lián)查分頁的實現(xiàn)代碼
本篇文章主要介紹了Mybatis-Plus 多表聯(lián)查分頁的實現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
IDEA創(chuàng)建Java項目保姆級教程(超詳細!)
這篇文章主要給大家介紹了關(guān)于IDEA創(chuàng)建Java項目保姆級教程的相關(guān)資料,Java是一種廣泛使用的編程語言,廣泛用于Web應(yīng)用程序和客戶端應(yīng)用程序的開發(fā),文中通過圖文介紹的非常詳細,需要的朋友可以參考下2023-09-09
Spring6.x對調(diào)度和異步執(zhí)行的注解支持示例詳解
這篇文章主要為大家介紹了Spring6.x對調(diào)度和異步執(zhí)行的注解支持示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11

