Spring中GET請(qǐng)求參數(shù)偶發(fā)性丟失問(wèn)題分析及修復(fù)
一、問(wèn)題現(xiàn)象
最近偶遇一詭異棘手問(wèn)題:一個(gè)用于獲取 token 的 GET 接口,在生產(chǎn)環(huán)境不定期偶發(fā)出現(xiàn) 參數(shù)不存在 的問(wèn)題。一度懷疑是前端的鍋,雖然前端同學(xué)再三以人格擔(dān)保!經(jīng)過(guò)長(zhǎng)時(shí)間觀察,發(fā)現(xiàn)每每出現(xiàn)問(wèn)題時(shí),“再點(diǎn)一下就好了”!錯(cuò)誤信息簡(jiǎn)單明確,是大家熟知的參數(shù)缺失異常:
Required request parameter ‘phone’ for method parameter type String is not present

這是怎么回事呢?這只是再普通不過(guò)的一個(gè) GET 接口!

二、問(wèn)題分析
2.1 發(fā)生時(shí)間
由于項(xiàng)目使用的是 Spring Cloud 微服務(wù)框架,當(dāng)請(qǐng)求從瀏覽器發(fā)送過(guò)來(lái)后,經(jīng)過(guò)了以下步驟:

順著這個(gè)思路逐層排查:
- HTTP請(qǐng)求: F12查看參數(shù)正常,排除。
- Nginx: 日志打印參數(shù)正常,排除。
- Gateway: 日志打印參數(shù)正常,排除。
- Controller: 參數(shù)丟失。。。
所以可以得出結(jié)論:參數(shù)丟失問(wèn)題發(fā)生在 Spring Cloud 微服務(wù)內(nèi)部。
2.2 發(fā)生位置
我們進(jìn)一步分析,在過(guò)濾器增加請(qǐng)求參數(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);
}
}
再次復(fù)現(xiàn)問(wèn)題后,在同一個(gè) traceId 對(duì)應(yīng)的日志中,打印結(jié)果如下:

可以發(fā)現(xiàn)在問(wèn)題請(qǐng)求中 request.queryString() 正常,而 request.getParameter() 值卻沒(méi)有獲取到!
眾所周知,SpringBoot 默認(rèn)內(nèi)置 tomcat 容器,SpringMVC 則通過(guò) request.getParameter() 方法獲取并綁定 Controller 接口參數(shù)的。因此,初步判斷:在 tomcat 獲取 parameter 參數(shù)的時(shí)候出現(xiàn)了問(wèn)題。
那么,parameter 參數(shù)的獲取過(guò)程是怎樣的?
- SpringMVC 框架通過(guò)
DispatcherServlet實(shí)現(xiàn)。 - Tomcat 接收到外部請(qǐng)求,將由 connector 通過(guò) Processor 受理 http 請(qǐng)求。
- SpringMVC 通過(guò) request.getParameter() 獲取并綁定 Controller 接口參數(shù)。
- request.getParameter() 方法 在請(qǐng)求處理過(guò)程中僅在第一次調(diào)用 時(shí)通過(guò)解析 queryString 獲取 parameters 參數(shù)值,并設(shè)置
didQueryParameter=true標(biāo)識(shí)已解析處理。 - Http 請(qǐng)求處理完成,processor 通過(guò) release 方法釋放連接重置參數(shù)屬性,request.recycle 方法重置 request 參數(shù)屬性(注意:這里 連接器及 request 對(duì)象并不會(huì)銷毀,connector 再次受理新的請(qǐng)求時(shí),將復(fù)用連接器、processor 及 request 對(duì)象而非創(chuàng)建)。

2.3 源碼解析
下面,我們可以看一些源碼的片段來(lái)驗(yàn)證一下:
源碼1:SpringBoot 從 request 獲取 parameter 參數(shù)。
RequestParamMethodArgumentResolve 類的 resovleName() 方法,可以看到這里調(diào)用了 request.getParameterValue() 方法。

源碼2:tomcat 封裝了解析參數(shù)。
org.apache.catalina.connector.Request 類的 getParameterValues() 方法,request 通過(guò) Parameters 獲取 parameter 參數(shù)。


源碼3:Parameters 從 queryString 解析封裝 parameter 參數(shù)。
org.apache.tomcat.util.http.Parameters 類的 handleQueryParameters() 方法,可以發(fā)現(xiàn),參數(shù)在解析處理后會(huì)設(shè)置 didQueryParameters 參數(shù)為 true。

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


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

2.5 原因總結(jié)
本次問(wèn)題的根本原因在于 線程中引用了 request 對(duì)象,并在線程中調(diào)用了 request.getParameter() 方法使參數(shù)屬性 didQueryParameter 錯(cuò)誤而導(dǎo)致 http 請(qǐng)求無(wú)法正確獲取參數(shù)值。
- 假設(shè)第一次受理 http 請(qǐng)求的連接器為 connector1;
- 請(qǐng)求 request 在子線程 thread1 中被引用;
- connector1 完成 http 請(qǐng)求并執(zhí)行 release 釋放連接,這時(shí)
request.didQueryParameters值為 false; - 如果子線程 thread1 處理任務(wù)的時(shí)間較長(zhǎng),調(diào)用了 getParameter() 方法,這時(shí)
request.didQueryParameters值將再次被更新為 true; - 當(dāng) tomcat 再次通過(guò) connector1 受理新的 http 請(qǐng)求時(shí),由于 request.didQueryParameters=true,這時(shí)新請(qǐng)求調(diào)用 getParameter() 方法將不會(huì)再解析 queryString,因而無(wú)法正確獲取 parameter 參數(shù)值。

三、問(wèn)題復(fù)現(xiàn)
這里為了方便,我們使用 Hutool 的線程池工具。依賴如下:
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
復(fù)現(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ù)手機(jī)號(hào)獲取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 壓測(cè)工具,設(shè)置 200 線程并發(fā)請(qǐng)求:

壓測(cè) http://localhost:8080/demo/test?phone=111111 接口,配置請(qǐng)求信息如下:

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

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

