springboot?正確的在異步線程中使用request的示例代碼
起因:
有后端同事反饋在異步線程中獲取了request中的參數(shù),然后下一個(gè)請(qǐng)求是get請(qǐng)求的話,發(fā)現(xiàn)會(huì)偶爾出現(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é)寫的代碼有問題,這么簡(jiǎn)單的一個(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對(duì)象都是會(huì)被循環(huán)使用的,因此這個(gè)時(shí)候也是整個(gè)Request被重置的時(shí)候。 所以根本原因是,在Parameter被重置了之后,didQueryParameters又被置成了true,導(dǎo)致新的請(qǐng)求參數(shù)沒有被正確解析,就報(bào)錯(cuò)了(此時(shí)的parameterMap已經(jīng)被重置,為空)。 而didQueryParameters只有在一種情況下才會(huì)被置為true,也就是handleQueryParameters方法被調(diào)用時(shí)。 而handleQueryParameters會(huì)在多個(gè)場(chǎng)景中被調(diào)用,其中一個(gè)就是getParameterValues,獲取請(qǐng)求參數(shù)的值。
大概就是說 tomcat 會(huì)復(fù)用Request對(duì)象,在異步中使用request中的參數(shù)可能會(huì)影響下一次 請(qǐng)求的參數(shù)解析過程.
最后文章作者的結(jié)論就是
不要將HttpServletRequest傳遞到任何異步方法中!
嘗試尋找官方支持
看到這里我還是有點(diǎn)不信,心想tomcat不會(huì)這么拉吧,異步都不支持,不可能吧...
于是我就去 tomcat的 bugzilla 搜了一下,居然沒搜索到相關(guān)的問題.
然后我還是有點(diǎn)不甘心,tomcat 沒有 ,spring框架出來這么久難道就沒人碰到過這種問題提出疑問嗎?
又去 spring的 issue 里面去搜,可能是我的關(guān)鍵詞沒搜對(duì),還是沒找到什么有用信息.
這時(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請(qǐng)求 url中參數(shù)的是 useDefaultResolution 屬性值為 true 的那一個(gè).
spring源碼對(duì)應(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ù)也跑通了,也不會(huì)報(bào)錯(cuò)了.
但是其實(shí)這是一個(gè)治標(biāo)不治本的方案
還存在一些問題:
- 只能解決接口參數(shù)綁定的問題,不能解決后續(xù)從request中獲取參數(shù)的問題.
- 通過壓測(cè), postTest 和 getParams 這兩個(gè)接口, 發(fā)現(xiàn) age3/name3 大概會(huì)出現(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)目上升級(jí) 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ù),不想改了
壓測(cè)一把發(fā)現(xiàn)沒啥問題
結(jié)論
springboot 中如何正確的在異步線程中使用request
- 使用異步前先獲取 AsyncContext
- 使用線程池處理任務(wù)
- 任務(wù)完成后調(diào)用asyncContext.complete()
到此這篇關(guān)于springboot 中如何正確的在異步線程中使用request的文章就介紹到這了,更多相關(guān)springboot 異步線程使用request內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(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對(duì)象
- Spring Boot 中正確地在異步線程中使用 HttpServletRequest的方法
相關(guān)文章
運(yùn)用Spring?Aop+注解實(shí)現(xiàn)日志記錄
我們都知道Spring框架的兩大特性分別是 IOC (控制反轉(zhuǎn))和 AOP (面向切面),這個(gè)是每一個(gè)Spring學(xué)習(xí)視頻里面一開始都會(huì)提到的,這里,如果我們使用Aop來記錄日志,那是再好不過了,感興趣的朋友跟隨小編一起學(xué)習(xí)下Spring?Aop注解實(shí)現(xiàn)日志記錄的過程吧2022-01-01Java代碼為例講解堆的性質(zhì)和基本操作以及排序方法
堆數(shù)據(jù)結(jié)構(gòu)可以看作一顆完全二叉樹,因而又被成為二叉堆,這里我們以Java代碼為例講解堆的性質(zhì)和基本操作以及排序方法,需要的朋友可以參考下2016-06-06Java Timezone類常見問題_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java Timezone類常見問題的相關(guān)資料,需要的朋友可以參考下2017-05-05Java中instanceof關(guān)鍵字實(shí)例講解
大家好,本篇文章主要講的是Java中instanceof關(guān)鍵字實(shí)例講解,感興趣的同學(xué)趕快來看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01徹底解決java.lang.ClassNotFoundException: com.mysql.jdbc.Dr
這篇文章給大家介紹了如如何徹底解決java.lang.ClassNotFoundException: com.mysql.jdbc.Driver問題,文中有詳細(xì)的解決思路以及解決方法,需要的朋友可以參考下2023-11-11Mybatis之類型處理器TypeHandler的作用與自定義方式
這篇文章主要介紹了Mybatis之類型處理器TypeHandler的作用與自定義方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04