springboot?正確的在異步線程中使用request的示例代碼
起因:
有后端同事反饋在異步線程中獲取了request中的參數(shù),然后下一個請求是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é)寫的代碼有問題,這么簡單的一個接口怎么可能有問題,然而等同事復(fù)現(xiàn)后就只能默默debug了.
大概追了一下源碼,發(fā)現(xiàn)
spring 在做參數(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,誰動了我的參數(shù)?
由于Tomcat中,Request以及Response對象都是會被循環(huán)使用的,因此這個時候也是整個Request被重置的時候。 所以根本原因是,在Parameter被重置了之后,didQueryParameters又被置成了true,導(dǎo)致新的請求參數(shù)沒有被正確解析,就報錯了(此時的parameterMap已經(jīng)被重置,為空)。 而didQueryParameters只有在一種情況下才會被置為true,也就是handleQueryParameters方法被調(diào)用時。 而handleQueryParameters會在多個場景中被調(diào)用,其中一個就是getParameterValues,獲取請求參數(shù)的值。
大概就是說 tomcat 會復(fù)用Request對象,在異步中使用request中的參數(shù)可能會影響下一次 請求的參數(shù)解析過程.
最后文章作者的結(jié)論就是
不要將HttpServletRequest傳遞到任何異步方法中!
嘗試尋找官方支持
看到這里我還是有點不信,心想tomcat不會這么拉吧,異步都不支持,不可能吧...
于是我就去 tomcat的 bugzilla 搜了一下,居然沒搜索到相關(guān)的問題.
然后我還是有點不甘心,tomcat 沒有 ,spring框架出來這么久難道就沒人碰到過這種問題提出疑問嗎?
又去 spring的 issue 里面去搜,可能是我的關(guān)鍵詞沒搜對,還是沒找到什么有用信息.
這時我就有點泄氣了,官方都沒解決這個問題我咋個辦?
嘗試自己解決
不過我又突然想到既然參數(shù)解析的時候 queryString 里面有參數(shù),那豈不是自己再解析一次不就完美了嗎?
那這個時候我們只要
- 繼承原始的參數(shù)解析器,當它獲取不到的時候嘗試從 queryString 尋找,queryString 中存在我們就返回 queryString 中的參數(shù).
- 替換掉原始的參數(shù)解析器,具體做法就是 在 RequestMappingHandlerAdapter 初始化后,拿到 argumentResolvers,遍歷所有的參數(shù)解析器,找到 RequestParamMethodArgumentResolver ,換成我們的即可.
這里有兩個問題需要注意就是 :
- argumentResolvers 是一個 UnmodifiableList,不能直接set
- RequestParamMethodArgumentResolver 有兩個,其中一個 useDefaultResolution 屬性值為 true,另外一個 屬性值為 false,
解析get請求 url中參數(shù)的是 useDefaultResolution 屬性值為 true 的那一個.
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; }
這個方案實現(xiàn)以后給項目組上的同事集成后看起來是沒什么問題了.
參數(shù)也能獲取到了,業(yè)務(wù)也跑通了,也不會報錯了.
但是其實這是一個治標不治本的方案
還存在一些問題:
- 只能解決接口參數(shù)綁定的問題,不能解決后續(xù)從request中獲取參數(shù)的問題.
- 通過壓測, postTest 和 getParams 這兩個接口, 發(fā)現(xiàn) age3/name3 大概會出現(xiàn)null, age2/name2 也可能獲取到null, 只有接口參數(shù)中的 name 和age 能正確獲取到.
還是甩給官方
這個時候我已經(jīng)沒什么好的辦法了,于是給spring 提了一個issue:
等待回復(fù)是痛苦的,issue提了以后
等了三天,開發(fā)者叫我提交一個復(fù)現(xiàn)的 demo (大家也可以嘗試復(fù)現(xiàn)一下).
又等了兩天,我想著這樣等也不是個辦法
主要是我看到 issue 還有 1.2k,輪到我的時候估計都猴年馬月了
而且就算修復(fù)了估計也是新版本, 在項目上升級 springboot 版本 估計也不太現(xiàn)實(版本不兼容)
解決
于是我開始看源碼.直到我看到了一個
org.apache.coyote.Request#setHook
它里面有個 ActionCode,是一個枚舉類型,其中有一個枚舉值是
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 中使用異步線程時的 HttpServletRequest 復(fù)用問題記錄
- SpringBoot異步線程父子線程數(shù)據(jù)傳遞的5種方式
- Spring?Boot異步線程間數(shù)據(jù)傳遞的四種方式
- SpringBoot?異步線程間傳遞上下文方式
- SpringBoot獲取HttpServletRequest的3種方式總結(jié)
- SpringBoot詳細講解異步任務(wù)如何獲取HttpServletRequest
- SpringBoot實現(xiàn)任意位置獲取HttpServletRequest對象
- Spring Boot 中正確地在異步線程中使用 HttpServletRequest的方法
相關(guān)文章
Java代碼為例講解堆的性質(zhì)和基本操作以及排序方法
堆數(shù)據(jù)結(jié)構(gòu)可以看作一顆完全二叉樹,因而又被成為二叉堆,這里我們以Java代碼為例講解堆的性質(zhì)和基本操作以及排序方法,需要的朋友可以參考下2016-06-06Java Timezone類常見問題_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了Java Timezone類常見問題的相關(guān)資料,需要的朋友可以參考下2017-05-05徹底解決java.lang.ClassNotFoundException: com.mysql.jdbc.Dr
這篇文章給大家介紹了如如何徹底解決java.lang.ClassNotFoundException: com.mysql.jdbc.Driver問題,文中有詳細的解決思路以及解決方法,需要的朋友可以參考下2023-11-11Mybatis之類型處理器TypeHandler的作用與自定義方式
這篇文章主要介紹了Mybatis之類型處理器TypeHandler的作用與自定義方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04