springboot?正確的在異步線程中使用request的示例代碼
起因:
有后端同事反饋在異步線程中獲取了request中的參數(shù),然后下一個(gè)請求是get請求的話,發(fā)現(xiàn)會偶爾出現(xiàn)參數(shù)丟失的問題.
示例代碼:
@GetMapping("/getParams")
public String getParams(String a, int b) {
return "get success";
}
@PostMapping("/postTest")
public String postTest(HttpServletRequest request,String age, String name) {
new Thread(new Runnable() {
@Override
public void run() {
String age2 = request.getParameter("age");
String name2 = request.getParameter("name");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String age3 = request.getParameter("age");
String name3 = request.getParameter("name");
System.out.println("age1: " + age + " , name1: " + name + " , age2: " + age2 + " , name2: " + name2 + " , age3: " + age3 + " , name3: " + name3);
}
}).start();
return "post success";
}異常信息如下
java.lang.IllegalStateException:
Optional int parameter 'b' is present but cannot be translated into a null value due to being declared as a primitive type.
Consider declaring it as object wrapper for the corresponding primitive type

看到這里大家可以猜一下是為什么.
我的第一反應(yīng)是不可能,肯定是前端同學(xué)寫的代碼有問題,這么簡單的一個(gè)接口怎么可能有問題,然而等同事復(fù)現(xiàn)后就只能默默debug了.
大概追了一下源碼,發(fā)現(xiàn)
spring 在做參數(shù)解析的時(shí)候沒有獲取到參數(shù),方法如下:
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName
而且很奇怪,queryString 不是null ,獲取到了正確的參數(shù), 但是 parameterMap 卻是空的.
正常來說 parameterMap 里面應(yīng)該存放有 queryString 解析后的參數(shù).
如圖:

發(fā)現(xiàn)有人踩過坑,但是沒解決
搜索了一下,發(fā)現(xiàn)有人碰到過類似的情況
偶現(xiàn)的MissingServletRequestParameterException,誰動(dòng)了我的參數(shù)?
由于Tomcat中,Request以及Response對象都是會被循環(huán)使用的,因此這個(gè)時(shí)候也是整個(gè)Request被重置的時(shí)候。 所以根本原因是,在Parameter被重置了之后,didQueryParameters又被置成了true,導(dǎo)致新的請求參數(shù)沒有被正確解析,就報(bào)錯(cuò)了(此時(shí)的parameterMap已經(jīng)被重置,為空)。 而didQueryParameters只有在一種情況下才會被置為true,也就是handleQueryParameters方法被調(diào)用時(shí)。 而handleQueryParameters會在多個(gè)場景中被調(diào)用,其中一個(gè)就是getParameterValues,獲取請求參數(shù)的值。
大概就是說 tomcat 會復(fù)用Request對象,在異步中使用request中的參數(shù)可能會影響下一次 請求的參數(shù)解析過程.
最后文章作者的結(jié)論就是
不要將HttpServletRequest傳遞到任何異步方法中!
嘗試尋找官方支持
看到這里我還是有點(diǎn)不信,心想tomcat不會這么拉吧,異步都不支持,不可能吧...
于是我就去 tomcat的 bugzilla 搜了一下,居然沒搜索到相關(guān)的問題.
然后我還是有點(diǎn)不甘心,tomcat 沒有 ,spring框架出來這么久難道就沒人碰到過這種問題提出疑問嗎?
又去 spring的 issue 里面去搜,可能是我的關(guān)鍵詞沒搜對,還是沒找到什么有用信息.
這時(shí)我就有點(diǎn)泄氣了,官方都沒解決這個(gè)問題我咋個(gè)辦?
嘗試自己解決
不過我又突然想到既然參數(shù)解析的時(shí)候 queryString 里面有參數(shù),那豈不是自己再解析一次不就完美了嗎?
那這個(gè)時(shí)候我們只要
- 繼承原始的參數(shù)解析器,當(dāng)它獲取不到的時(shí)候嘗試從 queryString 尋找,queryString 中存在我們就返回 queryString 中的參數(shù).
- 替換掉原始的參數(shù)解析器,具體做法就是 在 RequestMappingHandlerAdapter 初始化后,拿到 argumentResolvers,遍歷所有的參數(shù)解析器,找到 RequestParamMethodArgumentResolver ,換成我們的即可.
這里有兩個(gè)問題需要注意就是 :
- argumentResolvers 是一個(gè) UnmodifiableList,不能直接set
- RequestParamMethodArgumentResolver 有兩個(gè),其中一個(gè) useDefaultResolution 屬性值為 true,另外一個(gè) 屬性值為 false,
解析get請求 url中參數(shù)的是 useDefaultResolution 屬性值為 true 的那一個(gè).
spring源碼對應(yīng)位置:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultInitBinderArgumentResolvers
private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
}
這個(gè)方案實(shí)現(xiàn)以后給項(xiàng)目組上的同事集成后看起來是沒什么問題了.
參數(shù)也能獲取到了,業(yè)務(wù)也跑通了,也不會報(bào)錯(cuò)了.
但是其實(shí)這是一個(gè)治標(biāo)不治本的方案
還存在一些問題:
- 只能解決接口參數(shù)綁定的問題,不能解決后續(xù)從request中獲取參數(shù)的問題.
- 通過壓測, postTest 和 getParams 這兩個(gè)接口, 發(fā)現(xiàn) age3/name3 大概會出現(xiàn)null, age2/name2 也可能獲取到null, 只有接口參數(shù)中的 name 和age 能正確獲取到.
還是甩給官方
這個(gè)時(shí)候我已經(jīng)沒什么好的辦法了,于是給spring 提了一個(gè)issue:
等待回復(fù)是痛苦的,issue提了以后
等了三天,開發(fā)者叫我提交一個(gè)復(fù)現(xiàn)的 demo (大家也可以嘗試復(fù)現(xiàn)一下).
又等了兩天,我想著這樣等也不是個(gè)辦法
主要是我看到 issue 還有 1.2k,輪到我的時(shí)候估計(jì)都猴年馬月了
而且就算修復(fù)了估計(jì)也是新版本, 在項(xiàng)目上升級 springboot 版本 估計(jì)也不太現(xiàn)實(shí)(版本不兼容)
解決
于是我開始看源碼.直到我看到了一個(gè)
org.apache.coyote.Request#setHook
它里面有個(gè) ActionCode,是一個(gè)枚舉類型,其中有一個(gè)枚舉值是
ASYNC_START
這玩意看著就和異步有關(guān).于是開始搜索相關(guān)資料
最后終于在
RequestLoggingFilter: afterRequest is executed before Async servlet finishes
中找到答案.
結(jié)合我的代碼改造如下
@PostMapping("/postTest")
public String postTest(HttpServletRequest request, HttpServletResponse response, String age, String name) {
AsyncContext asyncContext =
request.isAsyncStarted()
? request.getAsyncContext()
: request.startAsync(request, response);
asyncContext.start(new Runnable() {
@Override
public void run() {
String age2 = request.getParameter("age");
String name2 = request.getParameter("name");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String age3 = request.getParameter("age");
String name3 = request.getParameter("name");
System.out.println("age1: " + age + " , name1: " + name + " , age2: " + age2 + " , name2: " + name2 + " , age3: " + age3 + " , name3: " + name3);
asyncContext.complete();
}
});
return "post success";
}
ps: 此處應(yīng)該用線程池提交任務(wù),不想改了
壓測一把發(fā)現(xiàn)沒啥問題
結(jié)論
springboot 中如何正確的在異步線程中使用request
- 使用異步前先獲取 AsyncContext
- 使用線程池處理任務(wù)
- 任務(wù)完成后調(diào)用asyncContext.complete()
到此這篇關(guān)于springboot 中如何正確的在異步線程中使用request的文章就介紹到這了,更多相關(guān)springboot 異步線程使用request內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 在 Spring Boot 中使用異步線程時(shí)的 HttpServletRequest 復(fù)用問題記錄
- SpringBoot異步線程父子線程數(shù)據(jù)傳遞的5種方式
- Spring?Boot異步線程間數(shù)據(jù)傳遞的四種方式
- SpringBoot?異步線程間傳遞上下文方式
- SpringBoot獲取HttpServletRequest的3種方式總結(jié)
- SpringBoot詳細(xì)講解異步任務(wù)如何獲取HttpServletRequest
- SpringBoot實(shí)現(xiàn)任意位置獲取HttpServletRequest對象
- Spring?Boot?中正確地在異步線程中使用?HttpServletRequest的方法
相關(guān)文章
Spring?Cloud?中使用?Sentinel?實(shí)現(xiàn)服務(wù)限流的兩種方式
這篇文章主要介紹了Spring?Cloud?中使用?Sentinel?實(shí)現(xiàn)服務(wù)限流的方式,通過示例代碼主要介紹了Sentinel的兩種實(shí)現(xiàn)限流的方式,需要的朋友可以參考下2024-03-03
Idea如何導(dǎo)入java mysql驅(qū)動(dòng)包
本文介紹了如何在IntelliJ IDEA中配置MySQL數(shù)據(jù)庫連接,首先下載MySQL Connector/J驅(qū)動(dòng)并解壓,然后在Idea項(xiàng)目中創(chuàng)建lib文件夾并將.jar文件復(fù)制到該文件夾,接著,將.jar文件添加為項(xiàng)目庫,通過這些步驟,可以成功配置MySQL數(shù)據(jù)庫連接2024-12-12
一文解決System.in關(guān)閉后無法再繼續(xù)使用流的問題
這篇文章主要給大家介紹如何解決System.in關(guān)閉后無法再繼續(xù)使用流的問題,文中有詳細(xì)的解決方法和代碼示例,具有一定的參考價(jià)值,需要的朋友可以參考下2023-07-07
springboot 基于Tomcat容器的自啟動(dòng)流程分析
這篇文章主要介紹了springboot 基于Tomcat容器的自啟動(dòng)流程分析,Spring通過注解導(dǎo)入Bean大體可分為四種方式,我們主要來說Import的兩種實(shí)現(xiàn)方法,需要的朋友可以參考下2020-02-02
配置pom.xml用maven打包java工程的方法(推薦)
下面小編就為大家?guī)硪黄渲胮om.xml用maven打包java工程的方法(推薦)。小編覺得挺不錯(cuò)的, 現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-06-06

