SpringMVC多線程下無(wú)法獲取請(qǐng)求的原因和解決方法
前言
眾所周知,在SpringMVC
中如果我們期待獲取當(dāng)前請(qǐng)求的HttpServletRequest
對(duì)象,通常有如下幾種方式:
- 通過(guò)方法參數(shù)注入:在
Controller
的方法中,可以直接聲明HttpServletRequest
類型的參數(shù),Spring MVC
會(huì)自動(dòng)將當(dāng)前請(qǐng)求的HttpServletRequest
對(duì)象注入進(jìn)來(lái)。例如:
@Controller public class MyController { @RequestMapping("/example") public String handleRequest(HttpServletRequest request) { // 使用request對(duì)象 return "example"; } }
- 通過(guò)
RequestContextHolder
:Spring MVC
提供了一個(gè)RequestContextHolder
類,例如:
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
而本次我們重點(diǎn)分析當(dāng)使用RequestContextHolder
在多線程環(huán)境下獲取請(qǐng)求所可能導(dǎo)致的一些問(wèn)題。
(注:在獲取當(dāng)前請(qǐng)求的HttpServletRequest
我們會(huì)利用RequestContextHolder
先獲取到ServletRequestAttributes
后,再通過(guò)ServletRequestAttributes
來(lái)獲取對(duì)應(yīng)的httpServletRequest
對(duì)象)
問(wèn)題復(fù)現(xiàn)
為了直觀的理解RequestContextHolder
在多線程使用下所導(dǎo)致的問(wèn)題,我們先來(lái)通過(guò)一個(gè)業(yè)務(wù)中的真實(shí)場(chǎng)景來(lái)進(jìn)行分析。
在國(guó)際化功能開(kāi)發(fā)中,我們通常會(huì)將用戶當(dāng)前的語(yǔ)言信息存放在Request
請(qǐng)求中,這樣后端通過(guò)獲取請(qǐng)求頭的中的語(yǔ)言信息就能成功獲取到用戶所支持的語(yǔ)言。
進(jìn)一步,對(duì)于一些涉及到國(guó)際化的導(dǎo)入,導(dǎo)出的耗時(shí)操作來(lái)說(shuō),我們通常會(huì)將其放在異步線程中進(jìn)行執(zhí)行,以提升程序性能。代碼邏輯大致如下:
@GetMapping("/missing-request-header") public String getMissingRequestHeader() { // 主線程獲取請(qǐng)求頭信息 String mainThreadLanguages = ServletUtils.getLanguagesExistProblem(); log.info("主線程獲取請(qǐng)求頭信息:{}", mainThreadLanguages); new Thread(() -> { // 子線程獲取請(qǐng)求頭信息 模擬執(zhí)行耗時(shí)操作 String subThreadLanguages = ServletUtils.getLanguagesExistProblem(); log.info("子線程獲取請(qǐng)求頭信息:{}", subThreadLanguages); }).start(); return "success"; }
上述程序的邏輯相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,唯一可能讓你困惑的可能在于ServletUtils.getLanguagesExistProblem()
方法的調(diào)用。
該方法是筆者
所寫(xiě)的一個(gè)工具類,其主要作用就是獲取當(dāng)前請(qǐng)求頭中的Lang
屬性,方法內(nèi)部具體邏輯如下所示:
/** * 獲取客戶端請(qǐng)求頭中的語(yǔ)言信息。 * 其會(huì)從當(dāng)前的HTTP請(qǐng)求中提取客戶端所設(shè)置的語(yǔ)言信息。 * 主要通過(guò)讀取請(qǐng)求頭中的"X_CLIENT_LANG"字段來(lái)獲取客戶端語(yǔ)言偏好。 * * @return String 客戶端請(qǐng)求頭中指定的語(yǔ)言信息。 * 如果不存在該字段則返回默認(rèn)的zh-cn。 */ public static String getLanguagesExistProblem() { HttpServletRequest request = getRequest(); Assert.notNull(request); String lang = request.getHeader(X_CLIENT_LANG); if (StrUtil.isNotBlank(lang)) { return lang; } return "zh-cn"; }
可以看到,在getLanguagesExistProblem
方法內(nèi)部又會(huì)通過(guò)getRequest
獲取到當(dāng)前請(qǐng)求的HttpServletRequest
信息,而getRequest
內(nèi)部邏輯如下所示:
public static HttpServletRequest getRequest() { HttpServletRequest httpServletRequest = null; try { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; httpServletRequest = servletRequestAttributes.getRequest(); } } catch (Exception e) { // 記錄異常,但不向外拋出,以避免可能的業(yè)務(wù)邏輯中斷 log.error("獲取HttpServletRequest時(shí)發(fā)生異常:", e); } // 返回獲取到的請(qǐng)求對(duì)象,如果失敗則返回null return httpServletRequest; }
整體來(lái)看上述代碼調(diào)用邏輯如下:
至此,相信你對(duì)于示例代碼的邏輯其實(shí)已經(jīng)清楚了。其無(wú)非就是會(huì)首先會(huì)在主線程中獲取當(dāng)前請(qǐng)求頭中的語(yǔ)言信息,接著,又會(huì)新建一個(gè)子線程嘗試去獲取到請(qǐng)求頭中的語(yǔ)言信息??雌饋?lái)似乎代碼似乎沒(méi)什么問(wèn)題,嘗試執(zhí)行代碼,你會(huì)發(fā)現(xiàn)有如下提示:
追蹤溯源
通過(guò)錯(cuò)誤提示不難發(fā)現(xiàn)是子線程在調(diào)用getLanguagesExistProblem()
方法時(shí)所提示的錯(cuò)誤。具體來(lái)看,是因?yàn)槠鋬?nèi)部Assert.notNull(request);
斷言所提示的錯(cuò)誤,而導(dǎo)致問(wèn)題發(fā)生的原因是因?yàn)閭魅氲?code>request對(duì)象為null
,進(jìn)而導(dǎo)致不滿足斷言條件notNull
從而提示異常信息。
正如我們前面所說(shuō),我們的request
對(duì)象是通過(guò)RequestContextHolder
來(lái)獲取的。具體我們的代碼來(lái)看,其本質(zhì)是通過(guò)ServletRequestAttributes
的getRequest
來(lái)完成這一操作。那為什么會(huì)在多線程情況下有這樣的問(wèn)題呢?初看這一問(wèn)題你可能會(huì)有點(diǎn)摸不到頭腦,不知該如何下手。沒(méi)有思路也別慌,接下來(lái),不妨聽(tīng)一聽(tīng)筆者是如何對(duì)這一問(wèn)題進(jìn)行分析的。
首先,既然RequestContextHolder
可以實(shí)現(xiàn)獲取當(dāng)前請(qǐng)求的功能,其一定會(huì)把請(qǐng)求進(jìn)行一層緩存
,以確保我們無(wú)論在程序的任何位置都能獲取到請(qǐng)求,順著這一思路,如果要你來(lái)實(shí)現(xiàn)這一需求,你會(huì)如何設(shè)計(jì)呢?
我想你大概率會(huì)將此處的邏輯設(shè)計(jì)在程序公共的入口位置,那SpringMVC
中請(qǐng)求第一次進(jìn)入時(shí)公共的會(huì)首先在哪處理呢?顯示是Servlet
中的service
方法。具體到DispatcherServlet
來(lái)看,其內(nèi)部邏輯如下所示:
@Override protected void service(HttpServletRequest request, HttpServletResponse response) { // ... 省略其他無(wú)關(guān)代碼 // 處理請(qǐng)求核心代碼,點(diǎn)擊該方法進(jìn)入 processRequest(request, response); }
(Ps: 這里需要讀者有一點(diǎn)Servlet
相關(guān)知識(shí),簡(jiǎn)單來(lái)看,所有請(qǐng)求進(jìn)入Servlet
后都會(huì)先通過(guò)Service
方法的處理~~~)
不難發(fā)現(xiàn),service
方法其內(nèi)部的核心邏輯會(huì)委托processRequest
進(jìn)行處理,而processRequest
其內(nèi)部邏輯如下:
protected final void processRequest(HttpServletRequest request, // ... 省略其他無(wú)關(guān)邏輯 // 初始化ContextHolders, 便于訪問(wèn)上下文信息 initContextHolders(request, localeContext, requestAttributes); doService(request, response); }
通過(guò)initContextHolders
方法的名稱我們不難猜出,其大概的作用微在于初始化一個(gè)ContextHolder
相關(guān)屬性。事實(shí)上,在該方法內(nèi)其會(huì)將 requestAttributes
與當(dāng)前線程進(jìn)行綁定。具體邏輯如下:
private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) { // 將參數(shù)localeContext設(shè)置為當(dāng)前線程的LocaleContext,并設(shè)置參數(shù)threadContextInheritable為true,表示上下文對(duì)象可以在子線程中繼承 if (localeContext != null) { LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable); } // 將參數(shù)requestAttributes設(shè)置為當(dāng)前線程的RequestAttributes,并設(shè)置參數(shù)threadContextInheritable為true,表示上下文對(duì)象可以在子線程中繼承 if (requestAttributes != null) { RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); } }
(Ps:RequestAttributes
是Spring
框架中的一個(gè)接口,用于表示一個(gè)請(qǐng)求的屬性集合。它允許應(yīng)用程序在線程中存儲(chǔ)和訪問(wèn)請(qǐng)求的屬性,這些屬性可以用于在請(qǐng)求的不同階段共享數(shù)據(jù)或傳遞數(shù)據(jù)。)
而在RequestContextHolder
內(nèi)部的setRequestAttributes
方法中,其會(huì)根據(jù)inheritable
屬性的不同來(lái)將request
屬性選擇性的放入requestAttributesHolder
和inheritableRequestAttributesHolder
兩個(gè)不同的ThreadLocal
。
而兩者的區(qū)別在于子線程是否可以共享父線程屬性。而默認(rèn)情況下inheritable
的取值為false
,也就是說(shuō)在SpringMVC
默認(rèn)情況下requestAttributes
是不會(huì)線程共享的。
(Ps:此處的RequestAttributes
是我們之前提及ServletRequestAttributes
的父接口)
進(jìn)一步,setRequestAttributes
的內(nèi)部邏輯如下:
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) { if (attributes == null) { resetRequestAttributes(); } else { if (inheritable) { inheritableRequestAttributesHolder.set(attributes); requestAttributesHolder.remove(); } else { requestAttributesHolder.set(attributes); inheritableRequestAttributesHolder.remove(); } } }
總結(jié)
回到我們之前的問(wèn)題,我們以多線程下無(wú)法從從請(qǐng)求頭中獲取相關(guān)屬性為入口,逐步深入剖析了其在多線程情況下無(wú)法失效的原因。具體來(lái)看,在SpringMVC
中,如果我們想獲取當(dāng)前請(qǐng)求的Request
對(duì)象,通常我們會(huì)通過(guò)RequestContextHolder
進(jìn)行獲取。進(jìn)一步RequestContextHolder
獲取Request
對(duì)象需要先獲取ServletRequestAttributes
對(duì)象,進(jìn)而通過(guò)其getRequest
方法來(lái)獲取到當(dāng)前的請(qǐng)求信息。
換言之,如果想獲取當(dāng)前請(qǐng)求的Request
對(duì)象,我們首先需要確保能獲取到ServletRequestAttributes
這一中間信息,因?yàn)槠鋬?nèi)部會(huì)維護(hù)相關(guān)的請(qǐng)求對(duì)象。而在SpringMVC
內(nèi)部ServletRequestAttributes
在保存在RequestContextHolder
中的ThreadLocal
。
而默認(rèn)情況下,其實(shí)不支持父子線程間傳遞的,所以在多線程環(huán)境下當(dāng)我們通過(guò)RequestContextHolder
獲取請(qǐng)求時(shí)會(huì)出現(xiàn)請(qǐng)求無(wú)法獲取的現(xiàn)象,而導(dǎo)致這一問(wèn)題本質(zhì)發(fā)生的本質(zhì)原因在于ServletRequestAttributes
并未實(shí)現(xiàn)父子線程間的共享!
以上就是SpringMVC多線程下無(wú)法獲取請(qǐng)求的原因和解決方法的詳細(xì)內(nèi)容,更多關(guān)于SpringMVC無(wú)法獲取請(qǐng)求的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JAVA實(shí)現(xiàn)第三方短信發(fā)送過(guò)程詳解
這篇文章主要介紹了JAVA實(shí)現(xiàn)第三方短信發(fā)送過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09SpringBoot利用jpa連接MySQL數(shù)據(jù)庫(kù)的方法
這篇文章主要介紹了SpringBoot利用jpa連接MySQL數(shù)據(jù)庫(kù)的方法,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-10-10SpringMVC實(shí)現(xiàn)注解式權(quán)限驗(yàn)證的實(shí)例
本篇文章主要介紹了SpringMVC實(shí)現(xiàn)注解式權(quán)限驗(yàn)證的實(shí)例,可以使用Spring MVC中的action攔截器來(lái)實(shí)現(xiàn),具有一定的參考價(jià)值,有興趣的可以了解下。2017-02-02Spring?Security內(nèi)置過(guò)濾器的維護(hù)方法
這篇文章主要介紹了Spring?Security的內(nèi)置過(guò)濾器是如何維護(hù)的,本文給我們分析一下HttpSecurity維護(hù)過(guò)濾器的幾個(gè)方法,需要的朋友可以參考下2022-02-02StackTraceElement獲取方法調(diào)用棧信息實(shí)例詳解
這篇文章主要介紹了StackTraceElement獲取方法調(diào)用棧信息實(shí)例詳解,分享了相關(guān)代碼示例,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02SpringCloud?Eureka服務(wù)治理之服務(wù)注冊(cè)服務(wù)發(fā)現(xiàn)
這篇文章主要介紹了SpringCloud?Eureka服務(wù)治理服務(wù)注冊(cè)和服務(wù)發(fā)現(xiàn)概念詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08SpringBoot2實(shí)現(xiàn)MessageQueue消息隊(duì)列
本文主要介紹了 SpringBoot2實(shí)現(xiàn)MessageQueue消息隊(duì)列,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04Spring入門配置和DL依賴注入實(shí)現(xiàn)圖解
這篇文章主要介紹了Spring入門配置和DL依賴注入實(shí)現(xiàn)圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10IntelliJ IDEA優(yōu)化配置的實(shí)現(xiàn)
這篇文章主要介紹了IntelliJ IDEA優(yōu)化配置的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07SpringAop實(shí)現(xiàn)原理及代理模式詳解
Spring的AOP就是通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)的,使用了兩個(gè)動(dòng)態(tài)代理,分別是JDK的動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理,本文重點(diǎn)給大家介紹下SpringAop實(shí)現(xiàn)原理及代理模式,感興趣的朋友一起看看吧2022-04-04