SpringMVC在多線程下請(qǐng)求頭獲取失敗問題的解決方案
前言
在日常的SpringMVC
開發(fā)中,我們通常會(huì)在請(qǐng)求頭中自定義一些參數(shù)信息,之后借助SpringMVC
提供的RequestContextHolder
來完成當(dāng)前請(qǐng)求的獲取,此時(shí)代碼邏輯大致如下:
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; }
上述代碼中,我們首先通過RequestContextHolder
提供的getRequestAttributes
方法獲取到一個(gè) ServletRequestAttributes
對(duì)象。而ServletRequestAttributes
在Spring MVC
中主要用于訪問和管理與當(dāng)前HTTP
請(qǐng)求相關(guān)的屬性, 并且提供了對(duì)HttpServletRequest
和HttpServletResponse
對(duì)象的訪問的API
。
進(jìn)一步,當(dāng)獲取到ServletRequestAttributes
對(duì)象后,我們就可以通過其提供的getRequest
來獲取到當(dāng)前請(qǐng)求的Reqeust
對(duì)象。而當(dāng)獲取到當(dāng)請(qǐng)求的Reqeust
對(duì)象后,我們即可讀取請(qǐng)求頭,從而獲取到請(qǐng)求頭中自定義的key-value
鍵值對(duì)。
請(qǐng)求頭丟失的問題
如果是在單線程情況下,上述邏輯不存在任何問題。但如果是多線程環(huán)境下,你會(huì)發(fā)現(xiàn)程序會(huì)莫名其妙出現(xiàn)空指針異常。此時(shí)出現(xiàn)的問題具體如下:
Controller測(cè)試接口
@RestController @RequestMapping("/test") @Slf4j public class TestController { @GetMapping("/missing-request-header") public String getMissingRequestHeader() { // 主線程獲取請(qǐng)求頭信息 String mainThreadLanguages = ServletUtils.getLanguagesExistProblem(); log.info("主線程獲取請(qǐng)求頭信息:{}", mainThreadLanguages); new Thread(() -> { // 子線程獲取請(qǐng)求頭信息 String subThreadLanguages = ServletUtils.getLanguagesExistProblem(); log.info("子線程獲取請(qǐng)求頭信息:{}", subThreadLanguages); }).start(); return "success"; } }
ServletUtils.getLanguagesExistProblem()具體邏輯
@Slf4j public class ServletUtils { private final static String X_CLIENT_LANG = "X-CLIENT-LANG"; 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"; } }
在上述代碼中,我們?cè)?code>TestController中啟用了一個(gè)新的線程,嘗試去通過getLanguagesExistProblem
讀取請(qǐng)求頭中我們自定義的"X-CLIENT-LANG
頭信息。然而,當(dāng)運(yùn)行代碼后你會(huì)發(fā)現(xiàn)出現(xiàn)代碼無法通過Assert.notNull(request);
這個(gè)斷言信息。即當(dāng)子線程嘗試去讀取請(qǐng)求中的"X-CLIENT-LANG
信息時(shí),其在子線程中無法獲取到當(dāng)前請(qǐng)求中的Request
對(duì)象,從而出現(xiàn)了空指針的異常。
而這恰恰也是我們開發(fā)中常見的在多線程環(huán)境下請(qǐng)求頭丟失的問題。簡(jiǎn)單來看,對(duì)于SpringMVC
而言,每個(gè)請(qǐng)求request
信息是存儲(chǔ)在ThreadLocal
中,而對(duì)于ThreadLocal
而言,其key
為當(dāng)前線程,因此每個(gè)線程一個(gè)存儲(chǔ)份Request
對(duì)象,因此Request
對(duì)象只與當(dāng)前線程關(guān)聯(lián)
。如果,我們嘗試在當(dāng)前線程中,再啟動(dòng)一個(gè)子線程去獲取Reqeust
其必然是無法獲取到主線程的Request
對(duì)象。
進(jìn)一步,針對(duì)多線程環(huán)境下無法獲取請(qǐng)求的這一問題,筆者在此提供兩個(gè)解決思路。希望對(duì)你能有所啟發(fā)。
解決方案
在這里我們先對(duì)網(wǎng)上一種錯(cuò)誤的方案進(jìn)行糾正。對(duì)于多線程環(huán)境下無法獲取請(qǐng)求頭的這一問題,網(wǎng)上其實(shí)很早就有人給出了解決方案,其大致思路是調(diào)用RequestContextHolder
的setRequestAttributes
將inheritable
屬性置為true
,從而實(shí)現(xiàn)父子線程對(duì)于Request
對(duì)象的共享。之所以這么做的原因在于SpringMVC
中有如下的代碼:
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(); } } }
在SpringMVC
內(nèi)對(duì),對(duì)于RequestContextHolder
而言,當(dāng)我們指定其requestAttributes
為true
時(shí),其會(huì)將相關(guān)的請(qǐng)求信息放入到InheritableThreadLocal
中。而InheritableThreadLocal
是 ThreadLocal
的子類,其可以實(shí)現(xiàn)父線程和子線程之間數(shù)據(jù)的共享。因此當(dāng)使用 InheritableThreadLocal
保存數(shù)據(jù)時(shí),子線程在創(chuàng)建時(shí)會(huì)繼承父線程中的 ThreadLocal
變量值。通過這樣的方式從而實(shí)現(xiàn)多線程環(huán)境下請(qǐng)求的獲取。
但這樣做的前提在于其必須確保子線程一定在父線程后執(zhí)行完畢,而如果子線程執(zhí)行慢,父線程執(zhí)行較快,已經(jīng)會(huì)存在子線程中數(shù)據(jù)獲取的問題!這么說可能比較晦澀,接下來我們不妨通過一個(gè)簡(jiǎn)單的例子來分析這一方法存在的問題
@GetMapping("/get-request-header-in-thread") public String getRequestHeaderInThread() { // 主線程獲取請(qǐng)求頭信息 String mainThreadLanguages = ServletUtils.getLanguages(); log.info("主線程獲取請(qǐng)求頭信息:{}", mainThreadLanguages); new Thread(() -> { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } // 子線程獲取請(qǐng)求頭信息 String subThreadLanguages = ServletUtils.getLanguages(); log.info("子線程獲取請(qǐng)求頭信息:{}", subThreadLanguages); }).start(); return "success"; }
(注:此處的ServletUtils.getLanguages()
邏輯可參考之前代碼)
在上述代碼中,我們?cè)?code>getRequestHeaderInThread方法中重新一個(gè)子線程去嘗試獲取請(qǐng)求中的語(yǔ)言信息。而我們的請(qǐng)求如下:
在請(qǐng)求頭中,我們?cè)O(shè)定的本次請(qǐng)求的語(yǔ)言頭為X-CLIENT-LANG
為en
,當(dāng)請(qǐng)求get-request-header-in-thread
這一路徑后,執(zhí)行結(jié)果如下:
可以看到,兩行日志打印時(shí)間間隔相差5秒
中,而這5秒
恰好正是我們代碼中Sleep
的時(shí)間。進(jìn)一步,子線程打印出的內(nèi)容zh-en
。即在子線程中其在獲取請(qǐng)求頭時(shí),本質(zhì)是獲取到了我們?cè)?code>getLanguages定義的默認(rèn)內(nèi)容,而非我們請(qǐng)求頭中X-CLIENT-LANG
對(duì)應(yīng)的en
。換言之,網(wǎng)上流傳的將RequestContextHolder
而言,當(dāng)我們指定其requestAttributes
為true
能有效解決多線程下SpringMVC
中獲取請(qǐng)求的方案完全是有問題的。那如何能解決這一問題呢?其實(shí)也很簡(jiǎn)單,如果能確保只開啟有限線程的話,完全可以借助CountDownLatch
來實(shí)現(xiàn)多線程間的協(xié)調(diào)工作。改造后的代碼如下:
@GetMapping("/get-request-header-in-thread") public String getRequestHeaderInThread() { // 主線程獲取請(qǐng)求頭信息 String mainThreadLanguages = ServletUtils.getLanguages(); CountDownLatch latch = new CountDownLatch(1); log.info("主線程獲取請(qǐng)求頭信息:{}", mainThreadLanguages); new Thread(() -> { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } // 子線程獲取請(qǐng)求頭信息 String subThreadLanguages = ServletUtils.getLanguages(); log.info("子線程獲取請(qǐng)求頭信息:{}", subThreadLanguages); latch.countDown(); }).start(); // 等待計(jì)數(shù)器變?yōu)榱? try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } log.info("確保父子線程全部執(zhí)行完畢"); return "success"; }
但在開發(fā)中,如果遇到子線程比較耗時(shí)的操作,上述代碼的性能又成為了效率的瓶頸。這與我們使用多線程開發(fā)的初衷相悖。事實(shí)上上,除了上述的方案外,我們還可以采用緩存當(dāng)前Request
的操作來實(shí)現(xiàn)請(qǐng)求的共享。其具體邏輯如下:
@GetMapping("/get-request-header-in-async-thread/{isJoin}") public String getRequestHeaderInThread() { // 主線程獲取請(qǐng)求頭信息 String mainThreadLanguages = ServletUtils.getLanguages(); log.info("主線程獲取請(qǐng)求頭信息:{}", mainThreadLanguages); // 獲取當(dāng)前servletRequestAttributes對(duì)象 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); new Thread(() -> { // 將servletRequestAttributes設(shè)定到子線程中 RequestContextHolder.setRequestAttributes(servletRequestAttributes); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } // 子線程獲取請(qǐng)求頭信息 String subThreadLanguages = ServletUtils.getLanguages(); log.info("子線程獲取請(qǐng)求頭信息:{}", subThreadLanguages); }).start(); return "success"; }
在上述代碼中,我們手動(dòng)獲取到當(dāng)前線程servletRequestAttributes
對(duì)象,然后將子線程代碼執(zhí)行前, 手動(dòng)給主線程中的ServletRequestAttributes
設(shè)置到子線程中,從而是確保實(shí)現(xiàn)子線程也能獲取到相關(guān)的請(qǐng)求對(duì)象。
總結(jié)
至此,我們就對(duì)多線程環(huán)境下使用SpringMVC
中RequestContextHolder
無法獲取請(qǐng)求的問題進(jìn)行了深入的分析,并針對(duì)相關(guān)問題給出了相應(yīng)的解決方案。具體來看,造成多線程環(huán)境下請(qǐng)求無法獲取的原因在于在默認(rèn)情況下SpringMVC
內(nèi)部對(duì)于請(qǐng)求頭的存放于在ThnreadLocal
。而如果手動(dòng)對(duì)RequestContextHolder
中的inheritable
設(shè)定為True
,其會(huì)將請(qǐng)求頭存放于InheritableThreadLocal
,從而實(shí)現(xiàn)父子線程請(qǐng)求頭的共享。
但當(dāng)請(qǐng)求頭存放于InheritableThreadLocal
時(shí),如果父線程先銷毀,則子線程依舊存在無法獲取請(qǐng)求頭的問題。 針對(duì)這一問題,我們給出了線程同步的解決方案。同時(shí),還給出了更加通用的方案以徹底解決多線程環(huán)境下請(qǐng)求頭丟失的問題。
以上就是SpringMVC在多線程下請(qǐng)求頭獲取失敗問題的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringMVC請(qǐng)求頭獲取失敗的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot自定義starter啟動(dòng)器的具體使用實(shí)踐
本文主要介紹了springboot自定義starter啟動(dòng)器的具體使用實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09java實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)單鏈表示例(java單鏈表)
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)單鏈表示例,需要的朋友可以參考下2014-03-03J2EE驗(yàn)證碼圖片如何生成和點(diǎn)擊刷新驗(yàn)證碼
這篇文章主要介紹了J2EE如何生成驗(yàn)證碼圖片如何生成,如何點(diǎn)擊刷新驗(yàn)證碼的相關(guān)方法,感興趣的小伙伴們可以參考一下2016-04-04Java實(shí)現(xiàn)Windows計(jì)算器界面
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)Windows計(jì)算器界面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06SpringBoot攔截器excludePathPatterns方法不生效的解決方案
這篇文章主要介紹了SpringBoot攔截器excludePathPatterns方法不生效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07