SpringMVC在多線程下請求頭獲取失敗問題的解決方案
前言
在日常的SpringMVC開發(fā)中,我們通常會在請求頭中自定義一些參數(shù)信息,之后借助SpringMVC提供的RequestContextHolder來完成當(dāng)前請求的獲取,此時代碼邏輯大致如下:
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時發(fā)生異常:", e);
}
// 返回獲取到的請求對象,如果失敗則返回null
return httpServletRequest;
}
上述代碼中,我們首先通過RequestContextHolder提供的getRequestAttributes方法獲取到一個 ServletRequestAttributes 對象。而ServletRequestAttributes 在Spring MVC中主要用于訪問和管理與當(dāng)前HTTP請求相關(guān)的屬性, 并且提供了對HttpServletRequest和HttpServletResponse對象的訪問的API。
進一步,當(dāng)獲取到ServletRequestAttributes對象后,我們就可以通過其提供的getRequest來獲取到當(dāng)前請求的Reqeust對象。而當(dāng)獲取到當(dāng)請求的Reqeust對象后,我們即可讀取請求頭,從而獲取到請求頭中自定義的key-value鍵值對。
請求頭丟失的問題
如果是在單線程情況下,上述邏輯不存在任何問題。但如果是多線程環(huán)境下,你會發(fā)現(xiàn)程序會莫名其妙出現(xiàn)空指針異常。此時出現(xiàn)的問題具體如下:
Controller測試接口
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
@GetMapping("/missing-request-header")
public String getMissingRequestHeader() {
// 主線程獲取請求頭信息
String mainThreadLanguages = ServletUtils.getLanguagesExistProblem();
log.info("主線程獲取請求頭信息:{}", mainThreadLanguages);
new Thread(() -> {
// 子線程獲取請求頭信息
String subThreadLanguages = ServletUtils.getLanguagesExistProblem();
log.info("子線程獲取請求頭信息:{}", 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";
}
}
在上述代碼中,我們在TestController中啟用了一個新的線程,嘗試去通過getLanguagesExistProblem讀取請求頭中我們自定義的"X-CLIENT-LANG頭信息。然而,當(dāng)運行代碼后你會發(fā)現(xiàn)出現(xiàn)代碼無法通過Assert.notNull(request);這個斷言信息。即當(dāng)子線程嘗試去讀取請求中的"X-CLIENT-LANG信息時,其在子線程中無法獲取到當(dāng)前請求中的Request對象,從而出現(xiàn)了空指針的異常。
而這恰恰也是我們開發(fā)中常見的在多線程環(huán)境下請求頭丟失的問題。簡單來看,對于SpringMVC而言,每個請求request信息是存儲在ThreadLocal中,而對于ThreadLocal而言,其key為當(dāng)前線程,因此每個線程一個存儲份Request對象,因此Request對象只與當(dāng)前線程關(guān)聯(lián)。如果,我們嘗試在當(dāng)前線程中,再啟動一個子線程去獲取Reqeust其必然是無法獲取到主線程的Request對象。
進一步,針對多線程環(huán)境下無法獲取請求的這一問題,筆者在此提供兩個解決思路。希望對你能有所啟發(fā)。
解決方案
在這里我們先對網(wǎng)上一種錯誤的方案進行糾正。對于多線程環(huán)境下無法獲取請求頭的這一問題,網(wǎng)上其實很早就有人給出了解決方案,其大致思路是調(diào)用RequestContextHolder的setRequestAttributes將inheritable屬性置為true,從而實現(xiàn)父子線程對于Request對象的共享。之所以這么做的原因在于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)對,對于RequestContextHolder而言,當(dāng)我們指定其requestAttributes為true時,其會將相關(guān)的請求信息放入到InheritableThreadLocal中。而InheritableThreadLocal 是 ThreadLocal 的子類,其可以實現(xiàn)父線程和子線程之間數(shù)據(jù)的共享。因此當(dāng)使用 InheritableThreadLocal 保存數(shù)據(jù)時,子線程在創(chuàng)建時會繼承父線程中的 ThreadLocal 變量值。通過這樣的方式從而實現(xiàn)多線程環(huán)境下請求的獲取。
但這樣做的前提在于其必須確保子線程一定在父線程后執(zhí)行完畢,而如果子線程執(zhí)行慢,父線程執(zhí)行較快,已經(jīng)會存在子線程中數(shù)據(jù)獲取的問題!這么說可能比較晦澀,接下來我們不妨通過一個簡單的例子來分析這一方法存在的問題
@GetMapping("/get-request-header-in-thread")
public String getRequestHeaderInThread() {
// 主線程獲取請求頭信息
String mainThreadLanguages = ServletUtils.getLanguages();
log.info("主線程獲取請求頭信息:{}", mainThreadLanguages);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 子線程獲取請求頭信息
String subThreadLanguages = ServletUtils.getLanguages();
log.info("子線程獲取請求頭信息:{}", subThreadLanguages);
}).start();
return "success";
}
(注:此處的ServletUtils.getLanguages()邏輯可參考之前代碼)
在上述代碼中,我們在getRequestHeaderInThread方法中重新一個子線程去嘗試獲取請求中的語言信息。而我們的請求如下:

在請求頭中,我們設(shè)定的本次請求的語言頭為X-CLIENT-LANG為en,當(dāng)請求get-request-header-in-thread這一路徑后,執(zhí)行結(jié)果如下:

可以看到,兩行日志打印時間間隔相差5秒中,而這5秒恰好正是我們代碼中Sleep的時間。進一步,子線程打印出的內(nèi)容zh-en。即在子線程中其在獲取請求頭時,本質(zhì)是獲取到了我們在getLanguages定義的默認(rèn)內(nèi)容,而非我們請求頭中X-CLIENT-LANG對應(yīng)的en。換言之,網(wǎng)上流傳的將RequestContextHolder而言,當(dāng)我們指定其requestAttributes為true能有效解決多線程下SpringMVC中獲取請求的方案完全是有問題的。那如何能解決這一問題呢?其實也很簡單,如果能確保只開啟有限線程的話,完全可以借助CountDownLatch來實現(xiàn)多線程間的協(xié)調(diào)工作。改造后的代碼如下:
@GetMapping("/get-request-header-in-thread")
public String getRequestHeaderInThread() {
// 主線程獲取請求頭信息
String mainThreadLanguages = ServletUtils.getLanguages();
CountDownLatch latch = new CountDownLatch(1);
log.info("主線程獲取請求頭信息:{}", mainThreadLanguages);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 子線程獲取請求頭信息
String subThreadLanguages = ServletUtils.getLanguages();
log.info("子線程獲取請求頭信息:{}", subThreadLanguages);
latch.countDown();
}).start();
// 等待計數(shù)器變?yōu)榱?
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("確保父子線程全部執(zhí)行完畢");
return "success";
}
但在開發(fā)中,如果遇到子線程比較耗時的操作,上述代碼的性能又成為了效率的瓶頸。這與我們使用多線程開發(fā)的初衷相悖。事實上上,除了上述的方案外,我們還可以采用緩存當(dāng)前Request的操作來實現(xiàn)請求的共享。其具體邏輯如下:
@GetMapping("/get-request-header-in-async-thread/{isJoin}")
public String getRequestHeaderInThread() {
// 主線程獲取請求頭信息
String mainThreadLanguages = ServletUtils.getLanguages();
log.info("主線程獲取請求頭信息:{}", mainThreadLanguages);
// 獲取當(dāng)前servletRequestAttributes對象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
new Thread(() -> {
// 將servletRequestAttributes設(shè)定到子線程中
RequestContextHolder.setRequestAttributes(servletRequestAttributes);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 子線程獲取請求頭信息
String subThreadLanguages = ServletUtils.getLanguages();
log.info("子線程獲取請求頭信息:{}", subThreadLanguages);
}).start();
return "success";
}
在上述代碼中,我們手動獲取到當(dāng)前線程servletRequestAttributes對象,然后將子線程代碼執(zhí)行前, 手動給主線程中的ServletRequestAttributes設(shè)置到子線程中,從而是確保實現(xiàn)子線程也能獲取到相關(guān)的請求對象。
總結(jié)
至此,我們就對多線程環(huán)境下使用SpringMVC中RequestContextHolder無法獲取請求的問題進行了深入的分析,并針對相關(guān)問題給出了相應(yīng)的解決方案。具體來看,造成多線程環(huán)境下請求無法獲取的原因在于在默認(rèn)情況下SpringMVC內(nèi)部對于請求頭的存放于在ThnreadLocal。而如果手動對RequestContextHolder中的inheritable設(shè)定為True,其會將請求頭存放于InheritableThreadLocal,從而實現(xiàn)父子線程請求頭的共享。
但當(dāng)請求頭存放于InheritableThreadLocal時,如果父線程先銷毀,則子線程依舊存在無法獲取請求頭的問題。 針對這一問題,我們給出了線程同步的解決方案。同時,還給出了更加通用的方案以徹底解決多線程環(huán)境下請求頭丟失的問題。
以上就是SpringMVC在多線程下請求頭獲取失敗問題的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringMVC請求頭獲取失敗的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot自定義starter啟動器的具體使用實踐
本文主要介紹了springboot自定義starter啟動器的具體使用實踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09
java實現(xiàn)數(shù)據(jù)結(jié)構(gòu)單鏈表示例(java單鏈表)
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)實現(xiàn)單鏈表示例,需要的朋友可以參考下2014-03-03
SpringBoot攔截器excludePathPatterns方法不生效的解決方案
這篇文章主要介紹了SpringBoot攔截器excludePathPatterns方法不生效的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07

