在 Spring Boot 中使用異步線程時(shí)的 HttpServletRequest 復(fù)用問題記錄
一、問題描述:異步線程操作導(dǎo)致請求復(fù)用時(shí) Cookie 解析失敗
1. 場景背景
在一個(gè) Web 應(yīng)用中,通常每個(gè)請求都會(huì)有一個(gè) HttpServletRequest
對象來保存該請求的上下文信息。例如,HttpServletRequest
存儲(chǔ)了請求中的 Cookie
信息。為了提高性能和減少內(nèi)存使用,Web 容器(例如 Tomcat)會(huì)對 HttpServletRequest
對象進(jìn)行復(fù)用。也就是說,當(dāng)一個(gè)請求完成后,Tomcat 會(huì)將 HttpServletRequest
對象放回池中,供下一次請求使用。
為了避免每次請求都重復(fù)解析某些信息(例如 Cookie
),開發(fā)人員可能會(huì)在主線程中解析并標(biāo)記請求對象的狀態(tài),例如通過設(shè)置一個(gè) cookieParsed
標(biāo)志位,表明 Cookie
已經(jīng)解析過。這一過程本來是為了避免重復(fù)的解析操作,但如果在異步線程中修改了請求的標(biāo)志位,可能會(huì)影響到請求復(fù)用時(shí)的行為,導(dǎo)致下一個(gè)請求復(fù)用時(shí)出現(xiàn)問題。
2. 問題根源
- 異步線程操作請求對象: 當(dāng)主線程解析完
HttpServletRequest
中的Cookie
信息后,標(biāo)記cookieParsed
為“已解析”,然后啟動(dòng)一個(gè)異步線程執(zhí)行一些長時(shí)間的任務(wù),然后主線程執(zhí)行完畢,進(jìn)行Request回收操作(例如:清空上下文信息,cookieParsed
置為未解析狀態(tài))。由于HttpServletRequest
是一個(gè)共享對象(在主線程和異步線程之間共享),異步線程可能會(huì)修改該請求對象的狀態(tài),例如將cookieParsed
設(shè)置為“已解析”。 - 請求復(fù)用機(jī)制: 當(dāng)前請求完成后,
HttpServletRequest
會(huì)被回收并返回到請求池中,準(zhǔn)備供下一個(gè)請求復(fù)用。在復(fù)用時(shí),Tomcat 會(huì)檢查當(dāng)前請求對象的狀態(tài)。如果上一個(gè)請求對象的cookieParsed
被標(biāo)記為“已解析”,則下一個(gè)請求在復(fù)用這個(gè)請求對象時(shí)會(huì)跳過 Cookie 的解析步驟,從而導(dǎo)致下一個(gè)請求無法正確獲取 Cookie 信息。 - 標(biāo)志位未重置: 由于在主線程結(jié)束后,
cookieParsed
標(biāo)志位被設(shè)置為“已解析”,但異步線程沒有在任務(wù)完成后重置該標(biāo)志位,導(dǎo)致請求對象在復(fù)用時(shí)被錯(cuò)誤地標(biāo)記為已經(jīng)解析過 Cookie。這會(huì)直接影響到下一個(gè)請求的處理,導(dǎo)致 Cookie 解析失敗,直到該Request再次被回收,再次進(jìn)行Request回收操作,才會(huì)正常
。
二、問題詳細(xì)分析
1. 場景重現(xiàn)
- 主線程獲取
HttpServletRequest
的Cookie
:主線程在處理 HTTP 請求時(shí),首先從HttpServletRequest
中解析出Cookie
信息,并標(biāo)記其解析狀態(tài)。通常,Tomcat 會(huì)在請求完成后將請求對象回收。 - 異步線程啟動(dòng):主線程結(jié)束后,將繼續(xù)執(zhí)行異步任務(wù)(例如,長時(shí)間的導(dǎo)出任務(wù)),在此過程中,異步線程會(huì)繼續(xù)訪問同一個(gè)
HttpServletRequest
對象。 - 請求復(fù)用:由于 Tomcat 對請求對象進(jìn)行復(fù)用,當(dāng)一個(gè)請求處理完后,它會(huì)將請求對象歸還到池中,以便下一個(gè)請求復(fù)用。如果異步線程修改了請求的某些狀態(tài)標(biāo)志(例如標(biāo)記
Cookie
已經(jīng)解析),下一個(gè)請求可能會(huì)復(fù)用已經(jīng)被修改過的HttpServletRequest
對象。 - 數(shù)據(jù)污染問題:由于復(fù)用的請求對象已經(jīng)被標(biāo)記為“Cookie 已解析”,這個(gè)狀態(tài)可能會(huì)被復(fù)用,導(dǎo)致下一次請求跳過
Cookie
的解析邏輯,導(dǎo)致獲取到的Cookie
為null
,進(jìn)而影響請求的數(shù)據(jù)處理。
代碼示例:
public String handleRequest(HttpServletRequest request, HttpServletResponse response) { // 主線程開始執(zhí)行,解析 Cookie 信息 String cookieValue = null; Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("UID".equals(cookie.getName())) { cookieValue = cookie.getValue(); break; } } } // 主線程完成后啟動(dòng)異步線程 AsyncContext asyncContext = request.startAsync(request, response); new Thread(() -> { try { // 模擬延遲任務(wù) Thread.sleep(5000); // 異步線程嘗試再次讀取 Cookie,將回收后的request中的 `cookieParsed` 設(shè)置為“已解析” String cookieValueFromAsync = request.getCookies()[0].getValue(); System.out.println("異步線程中的 cookie: " + cookieValueFromAsync); asyncContext.complete(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); return "success"; }
問題:
- 當(dāng)異步線程執(zhí)行時(shí),
request
已經(jīng)被回收,request.getCookies()
返回的Cookie
可能會(huì)是一個(gè) 空數(shù)組 或者是 錯(cuò)誤的 Cookie。這時(shí),即使請求中存在有效的Cookie
,異步線程依然無法獲取到正確的值。 - 同時(shí)被回收的
request
已經(jīng)被異步線程標(biāo)記為“Cookie 已解析”,導(dǎo)致下一次復(fù)用該request的請求跳過了Cookie
的解析邏輯,造成下一次請求的獲取Cookie
為空。
2. 問題分析
Tomcat 請求復(fù)用機(jī)制:
- Tomcat 在請求處理結(jié)束后并不會(huì)立即銷毀
HttpServletRequest
對象,而是將其放入對象池中以供下一個(gè)請求復(fù)用。當(dāng)請求完成后,如果異步線程訪問了HttpServletRequest
,會(huì)繼續(xù)使用主線程的請求對象。 - 如果主線程處理完請求后,已經(jīng)對
HttpServletRequest
標(biāo)記了“Cookie 已解析”,這個(gè)狀態(tài)可能會(huì)被復(fù)用,導(dǎo)致下一次請求跳過Cookie
的解析。
異步線程與請求對象狀態(tài)沖突:
- 異步線程和主線程雖然共享同一個(gè)
HttpServletRequest
對象,但異步線程修改了請求的狀態(tài)(例如cookieParsed
標(biāo)志),就會(huì)影響其他線程訪問請求數(shù)據(jù)的能力。 - 這種情況下,下一個(gè)請求使用了已經(jīng)標(biāo)記為“Cookie 解析完畢”的請求對象,導(dǎo)致解析失敗。
請求上下文傳遞失敗:
- 在異步線程中,由于線程隔離,主線程中的
HttpServletRequest
無法自動(dòng)傳遞到異步線程中。即使使用AsyncContext
來延遲清理請求,HttpServletRequest
中的數(shù)據(jù)也可能無法正確傳遞給異步線程。
請求標(biāo)志和清理機(jī)制:
- Tomcat 使用請求標(biāo)志(如
cookieParsed
或者requestCompleted
)來追蹤請求的狀態(tài),并在請求處理完成后清理請求資源。異步線程和主線程共享同一個(gè)請求對象時(shí),可能會(huì)意外地修改這些標(biāo)志,影響復(fù)用請求的正確性。 - 一旦請求進(jìn)入異步模式,Tomcat 會(huì)將其狀態(tài)標(biāo)記為“處理完成”,并通過
asyncContext.complete()
延遲清理請求對象。這種延遲清理機(jī)制會(huì)讓異步線程繼續(xù)持有原始的請求對象,造成請求標(biāo)志的沖突和數(shù)據(jù)污染。
三、解決方案
為了避免 HttpServletRequest
的狀態(tài)被修改,并正確地將請求上下文傳遞給異步線程,以下是推薦的幾種解決方案。
使用 HttpServletRequestWrapper
創(chuàng)建請求副本
在異步線程中創(chuàng)建請求副本,避免直接操作原始請求對象,從而解決請求復(fù)用問題。
public String handleRequest(HttpServletRequest request, HttpServletResponse response) { // 創(chuàng)建請求副本 HttpServletRequest requestCopy = new HttpServletRequestWrapper(request) { @Override public Cookie[] getCookies() { Cookie[] cookies = super.getCookies(); // 解析 cookie 或者創(chuàng)建副本 return cookies; } }; AsyncContext asyncContext = request.startAsync(request, response); new Thread(() -> { try { // 在異步線程中使用副本 String cookieValueFromAsync = requestCopy.getCookies()[0].getValue(); System.out.println("異步線程中的 cookie: " + cookieValueFromAsync); asyncContext.complete(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); return "success"; }
優(yōu)點(diǎn):通過 HttpServletRequestWrapper
創(chuàng)建的副本確保了異步線程不會(huì)直接修改原始請求對象,從而避免了請求復(fù)用時(shí)出現(xiàn)數(shù)據(jù)污染。
手動(dòng)傳遞請求上下文
通過 RequestContextHolder
手動(dòng)傳遞請求上下文到異步線程,確保異步線程可以訪問主線程的請求數(shù)據(jù)。
public String handleRequest(HttpServletRequest request, HttpServletResponse response) { AsyncContext asyncContext = request.startAsync(request, response); // 手動(dòng)傳遞請求上下文到異步線程 new Thread(() -> { try { // 設(shè)置當(dāng)前請求上下文 ServletRequestAttributes attributes = new ServletRequestAttributes(request, response); RequestContextHolder.setRequestAttributes(attributes, true); // 在異步線程中獲取請求參數(shù) String cookieValueFromAsync = request.getCookies()[0].getValue(); System.out.println("異步線程中的 cookie: " + cookieValueFromAsync); asyncContext.complete(); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 清理請求上下文 RequestContextHolder.resetRequestAttributes(); } }).start(); return "success"; }
優(yōu)點(diǎn):手動(dòng)傳遞請求上下文使得異步線程能夠訪問主線程的請求信息,避免了異步線程和主線程的上下文隔離問題。
延遲請求對象的清理
通過 AsyncContext.complete()
延遲請求的清理,避免請求對象在異步線程執(zhí)行期間被回收,從而保持請求數(shù)據(jù)的有效性。
public String handleRequest(HttpServletRequest request, HttpServletResponse response) { AsyncContext asyncContext = request.startAsync(request, response); new Thread(() -> { try { // 執(zhí)行異步任務(wù) Thread.sleep(5000); // 模擬長時(shí)間任務(wù) asyncContext.complete(); // 延遲請求清理 } catch (InterruptedException e) { e.printStackTrace(); } }).start(); return "success"; }
優(yōu)點(diǎn):通過延遲清理請求對象,確保異步線程可以訪問到有效的請求數(shù)據(jù),避免了請求數(shù)據(jù)在異步任務(wù)執(zhí)行期間被誤清理。
四、總結(jié)
在處理異步線程時(shí),特別是涉及到 HttpServletRequest
等請求對象時(shí),可能會(huì)遇到請求復(fù)用和上下文傳遞問題。通過合理地使用請求副本、手動(dòng)傳遞請求上下文和延遲請求清理等方法,可以有效避免數(shù)據(jù)污染和請求對象復(fù)用問題,從而確保異步任務(wù)中的請求數(shù)據(jù)正確性。
核心問題:
- 請求復(fù)用:Tomcat 會(huì)復(fù)用請求對象,導(dǎo)致異步線程訪問到已經(jīng)修改過的請求。
- 異步線程訪問不到請求數(shù)據(jù):由于請求對象在異步線程執(zhí)行時(shí)可能已經(jīng)被清理或標(biāo)記為“完成”,導(dǎo)致訪問不到請求數(shù)據(jù)。
解決方案:
- 使用
HttpServletRequestWrapper
創(chuàng)建請求副本。 - 手動(dòng)傳遞請求上下文到異步線程。
- 延遲請求對象的清理,確保異步線程在執(zhí)行期間能夠訪問到請求數(shù)據(jù)。
到此這篇關(guān)于在 Spring Boot 中使用異步線程時(shí)的 HttpServletRequest 復(fù)用問題的文章就介紹到這了,更多相關(guān)Spring Boot 異步線程HttpServletRequest 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot異步線程父子線程數(shù)據(jù)傳遞的5種方式
- Spring?Boot異步線程間數(shù)據(jù)傳遞的四種方式
- springboot?正確的在異步線程中使用request的示例代碼
- SpringBoot?異步線程間傳遞上下文方式
- SpringBoot獲取HttpServletRequest的3種方式總結(jié)
- SpringBoot詳細(xì)講解異步任務(wù)如何獲取HttpServletRequest
- SpringBoot實(shí)現(xiàn)任意位置獲取HttpServletRequest對象
- Spring?Boot?中正確地在異步線程中使用?HttpServletRequest的方法
相關(guān)文章
idea resources目錄下的application.properties不能自動(dòng)提示問題
這篇文章主要介紹了idea resources目錄下的application.properties不能自動(dòng)提示問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11Spring Cloud Data Flow初體驗(yàn)以Local模式運(yùn)行
這篇文章主要介紹了Spring Cloud Data Flow初體驗(yàn)以Local模式運(yùn)行,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08MyBatis類型轉(zhuǎn)換模塊的實(shí)現(xiàn)
MyBatis是一個(gè)持久層框架ORM框架,實(shí)現(xiàn)數(shù)據(jù)庫中數(shù)據(jù)和Java對象中的屬性的雙向映射,那么不可避免的就會(huì)碰到類型轉(zhuǎn)換的問題,本文主要介紹了MyBatis類型轉(zhuǎn)換模塊的實(shí)現(xiàn),感興趣的可以了解一下2023-09-09Java實(shí)現(xiàn)四則混合運(yùn)算代碼示例
這篇文章主要介紹了Java實(shí)現(xiàn)四則混合運(yùn)算代碼示例,文中展示了詳細(xì)代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10