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)文章
SpringCloud使用AOP統(tǒng)一處理Web請(qǐng)求日志實(shí)現(xiàn)步驟
這篇文章主要為大家介紹了SpringCloud使用AOP統(tǒng)一處理Web請(qǐng)求日志實(shí)現(xiàn)步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08swagger文檔增強(qiáng)工具knife4j使用圖文詳解
這篇文章主要介紹了swagger文檔增強(qiáng)工具knife4j使用詳解,想要使用knife4j非常簡(jiǎn)單,只要在Springboot項(xiàng)目中引入knife4j的依賴即可,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08Spring?Data?JPA?注解Entity關(guān)聯(lián)關(guān)系使用詳解
這篇文章主要為大家介紹了Spring?Data?JPA?注解Entity關(guān)聯(lián)關(guān)系使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09springboot根據(jù)實(shí)體類生成表的實(shí)現(xiàn)方法
本文介紹了如何通過(guò)SpringBoot工程引入SpringDataJPA,并通過(guò)實(shí)體類自動(dòng)生成數(shù)據(jù)庫(kù)表的過(guò)程,包括常見(jiàn)問(wèn)題解決方法,感興趣的可以了解一下2024-09-09spring基于通用Dao的多數(shù)據(jù)源配置詳解
這篇文章主要為大家詳細(xì)介紹了spring基于通用Dao的多數(shù)據(jù)源配置,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下解2018-03-03一不小心就讓Java開(kāi)發(fā)踩坑的fail-fast是個(gè)什么鬼?(推薦)
這篇文章主要介紹了Java fail-fast,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04SpringBoot項(xiàng)目中JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理的使用詳解
JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理都是SpringBoot中實(shí)現(xiàn)AOP的重要技術(shù),JDK動(dòng)態(tài)代理通過(guò)反射生成代理類,適用于目標(biāo)類實(shí)現(xiàn)了接口的場(chǎng)景,性能較好,易用性高,但必須實(shí)現(xiàn)接口且不能代理final方法,CGLIB動(dòng)態(tài)代理通過(guò)生成子類實(shí)現(xiàn)代理2025-03-03ArrayList的自動(dòng)擴(kuò)充機(jī)制實(shí)例解析
本文主要介紹了ArrayList的自動(dòng)擴(kuò)充機(jī)制,由一個(gè)題目切入主題,逐步向大家展示了ArrayList的相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10