SpringBoot中短時間連續(xù)請求時出現(xiàn)Cookie獲取異常問題的解決方案
一、問題描述:異步線程操作導(dǎo)致請求復(fù)用時 Cookie 解析失敗
在 Spring Boot Web 應(yīng)用中,每個請求都會攜帶 HttpServletRequest,其中包含 Cookie 等關(guān)鍵信息。然而,由于 Tomcat 對 HttpServletRequest 的復(fù)用機(jī)制,如果某個請求對象的 cookieParsed 標(biāo)記在異步線程中被錯誤修改,可能會導(dǎo)致 短時間內(nèi)的后續(xù)請求無法正確解析 Cookie。
1. 場景背景
在一個 Web 應(yīng)用中,通常每個請求都會有一個 HttpServletRequest 對象來保存該請求的上下文信息。例如,HttpServletRequest 存儲了請求中的 Cookie 信息。為了提高性能和減少內(nèi)存使用,Web 容器(例如 Tomcat)會對 HttpServletRequest 對象進(jìn)行復(fù)用。也就是說,當(dāng)一個請求完成后,Tomcat 會將 HttpServletRequest 對象放回池中,供下一次請求使用。
為了避免每次請求都重復(fù)解析某些信息(例如 Cookie),開發(fā)人員可能會在主線程中解析并標(biāo)記請求對象的狀態(tài),例如通過設(shè)置一個 cookieParsed 標(biāo)志位,表明 Cookie 已經(jīng)解析過。這一過程本來是為了避免重復(fù)的解析操作,但如果在異步線程中修改了請求的標(biāo)志位,可能會影響到請求復(fù)用時的行為,導(dǎo)致下一個請求復(fù)用時出現(xiàn)問題。
2. 問題根源
異步線程操作請求對象: 當(dāng)主線程解析完
HttpServletRequest
中的Cookie
信息后,標(biāo)記cookieParsed
為“已解析”,然后啟動一個異步線程執(zhí)行一些長時間的任務(wù),然后主線程執(zhí)行完畢,進(jìn)行HttpServletRequest
回收操作(例如:清空上下文信息,cookieParsed
置為未解析狀態(tài))。由于HttpServletRequest
是一個共享對象(在主線程和異步線程之間共享),異步線程可能會修改該請求對象的狀態(tài),例如將cookieParsed
設(shè)置為“已解析”。請求復(fù)用機(jī)制: 當(dāng)前請求完成后,
HttpServletRequest
會被回收并返回到請求池中,準(zhǔn)備供下一個請求復(fù)用。在復(fù)用時,Tomcat 會檢查當(dāng)前請求對象的狀態(tài)。如果上一個請求對象的cookieParsed
被標(biāo)記為“已解析”,則下一個請求在復(fù)用這個請求對象時會跳過 Cookie 的解析步驟,從而導(dǎo)致下一個請求無法正確獲取 Cookie 信息。標(biāo)志位未重置: 由于在主線程結(jié)束后,
cookieParsed
標(biāo)志位被設(shè)置為“已解析”,但異步線程沒有在任務(wù)完成后重置該標(biāo)志位,導(dǎo)致請求對象在復(fù)用時被錯誤地標(biāo)記為已經(jīng)解析過 Cookie。這會直接影響到下一個請求的處理,導(dǎo)致 Cookie 解析失敗,直到該Request再次被回收,再次進(jìn)行Request回收操作,才會正常
。
二、問題詳細(xì)分析
1. 場景重現(xiàn)
主線程獲取
HttpServletRequest
的Cookie
:主線程在處理 HTTP 請求時,首先從HttpServletRequest
中解析出Cookie
信息,并標(biāo)記其解析狀態(tài)。通常,Tomcat 會在請求完成后將請求對象回收。異步線程啟動:主線程結(jié)束后,將繼續(xù)執(zhí)行異步任務(wù)(例如,長時間的導(dǎo)出任務(wù)),在此過程中,異步線程會繼續(xù)訪問同一個
HttpServletRequest
對象。請求復(fù)用:由于 Tomcat 對請求對象進(jìn)行復(fù)用,當(dāng)一個請求處理完后,它會將請求對象歸還到池中,以便下一個請求復(fù)用。如果異步線程修改了請求的某些狀態(tài)標(biāo)志(例如標(biāo)記
Cookie
已經(jīng)解析),下一個請求可能會復(fù)用已經(jīng)被修改過的HttpServletRequest
對象。數(shù)據(jù)污染問題:由于復(fù)用的請求對象已經(jīng)被標(biāo)記為“Cookie 已解析”,這個狀態(tài)可能會被復(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; } } } // 主線程完成后啟動異步線程 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í)行時,
request
已經(jīng)被回收,request.getCookies()
返回的Cookie
可能會是一個 空數(shù)組 或者是 錯誤的 Cookie。這時,即使請求中存在有效的Cookie
,異步線程依然無法獲取到正確的值。同時被回收的
request
已經(jīng)被異步線程標(biāo)記為“Cookie 已解析”,導(dǎo)致下一次復(fù)用該request的請求跳過了Cookie
的解析邏輯,造成下一次請求的獲取Cookie
為空。
2. 問題分析
- Tomcat 請求復(fù)用機(jī)制:
Tomcat 在請求處理結(jié)束后并不會立即銷毀
HttpServletRequest
對象,而是將其初始化后放入對象池中以供下一個請求復(fù)用。當(dāng)請求完成后,如果異步線程訪問了HttpServletRequest
,會繼續(xù)使用主線程的請求對象。如果主線程處理完請求后,已經(jīng)對
HttpServletRequest
標(biāo)記了“Cookie 已解析”,這個狀態(tài)可能會被復(fù)用,導(dǎo)致下一次請求跳過Cookie
的解析。
- 異步線程與請求對象狀態(tài)沖突:
異步線程和主線程雖然共享同一個
HttpServletRequest
對象,但異步線程修改了請求的狀態(tài)(例如cookieParsed
標(biāo)志),就會影響其他線程訪問請求數(shù)據(jù)的能力。這種情況下,下一個請求使用了已經(jīng)標(biāo)記為“Cookie 解析完畢”的請求對象,導(dǎo)致解析失敗。
- 請求上下文傳遞失敗:
- 在異步線程中,由于線程隔離,主線程中的
HttpServletRequest
無法自動傳遞到異步線程中。即使使用AsyncContext
來延遲清理請求,HttpServletRequest
中的數(shù)據(jù)也可能無法正確傳遞給異步線程。
- 請求標(biāo)志和清理機(jī)制:
Tomcat 使用請求標(biāo)志(如
cookieParsed
或者requestCompleted
)來追蹤請求的狀態(tài),并在請求處理完成后清理請求資源。異步線程和主線程共享同一個請求對象時,可能會意外地修改這些標(biāo)志,影響復(fù)用請求的正確性。一旦請求進(jìn)入異步模式,Tomcat 會將其狀態(tài)標(biāo)記為“處理完成”,并通過
asyncContext.complete()
延遲清理請求對象。這種延遲清理機(jī)制會讓異步線程繼續(xù)持有原始的請求對象,造成請求標(biāo)志的沖突和數(shù)據(jù)污染。
三、如何避免影響下一次請求?
為了避免 HttpServletRequest
的狀態(tài)被修改,并正確地將請求上下文傳遞給異步線程,以下是推薦的幾種解決方案。
方式 1:在主線程提前復(fù)制 Cookie(推薦)
避免異步線程訪問 request,在主線程獲取 Cookie 副本并傳遞給異步線程:
Cookie[] cookiesCopy = Arrays.copyOf(request.getCookies(), request.getCookies().length); AsyncContext asyncContext = request.startAsync(); new Thread(() -> { try { // 訪問副本,避免修改原 request String cookieValue = cookiesCopy[0].getValue(); System.out.println("異步線程的 Cookie:" + cookieValue); } finally { asyncContext.complete(); } }).start();
優(yōu)點(diǎn):
- 在 主線程 獲取 Cookie,不會影響 request 內(nèi)部狀態(tài)。
- 避免了 cookieParsed 被提前設(shè)置為 true。
方式 2:使用 HttpServletRequestWrapper 包裝 request(避免修改原始 request)
如果你需要保持 request 可用性,可以使用 HttpServletRequestWrapper 攔截 getCookies(),防止它影響 request 的 cookieParsed 狀態(tài):
class SafeRequestWrapper extends HttpServletRequestWrapper { private final Cookie[] cookiesCopy; public SafeRequestWrapper(HttpServletRequest request) { super(request); // 提前復(fù)制 cookie,避免影響原始 request this.cookiesCopy = request.getCookies() != null ? Arrays.copyOf(request.getCookies(), request.getCookies().length) : new Cookie[0]; } @Override public Cookie[] getCookies() { return cookiesCopy; } } public String handleRequest(HttpServletRequest request, HttpServletResponse response) { HttpServletRequest safeRequest = new SafeRequestWrapper(request); AsyncContext asyncContext = request.startAsync(); new Thread(() -> { try { String cookieValue = safeRequest.getCookies()[0].getValue(); System.out.println("異步線程的 Cookie:" + cookieValue); } finally { asyncContext.complete(); } }).start(); return "success"; }
優(yōu)點(diǎn):
SafeRequestWrapper
攔截getCookies()
,防止cookieParsed
狀態(tài)變化。- 異步線程仍然可以像正常
request
一樣獲取Cookie
,但不會污染主request
。
方式 3:使用 ThreadLocal 傳遞 Cookie(適用于復(fù)雜場景)
如果異步線程可能會在多個地方訪問 request
,可以使用 ThreadLocal
預(yù)先緩存 Cookie
:
private static final ThreadLocal<Cookie[]> threadLocalCookies = new ThreadLocal<>(); public String handleRequest(HttpServletRequest request, HttpServletResponse response) { threadLocalCookies.set(request.getCookies()); // 復(fù)制 Cookie AsyncContext asyncContext = request.startAsync(); new Thread(() -> { try { Cookie[] cookies = threadLocalCookies.get(); if (cookies != null) { String cookieValue = cookies[0].getValue(); System.out.println("異步線程的 Cookie:" + cookieValue); } } finally { threadLocalCookies.remove(); // 避免內(nèi)存泄漏 asyncContext.complete(); } }).start(); return "success"; }
優(yōu)點(diǎn):
- 避免異步線程訪問
request
,但仍然可以獲取Cookie
副本。 - 避免
cookieParsed
狀態(tài)修改,不會污染后續(xù)請求。 - 適用于 異步任務(wù)復(fù)雜且可能跨多個方法調(diào)用的情況。
四、總結(jié)
在處理異步線程時,特別是涉及到 HttpServletRequest
等請求對象時,可能會遇到請求復(fù)用和上下文傳遞問題。通過合理地使用在主線程提前復(fù)制 Cookie
、使用 HttpServletRequestWrapper
包裝 request
、使用 ThreadLocal 傳遞 Cookie或者直接傳遞參數(shù)等方法,可以有效避免數(shù)據(jù)污染和請求對象復(fù)用問題,從而確保異步任務(wù)中的請求數(shù)據(jù)正確性。
核心問題:
請求復(fù)用:Tomcat 會復(fù)用請求對象,導(dǎo)致異步線程訪問到已經(jīng)修改過的請求。
異步線程訪問不到請求數(shù)據(jù):由于請求對象在異步線程執(zhí)行時可能已經(jīng)被清理或標(biāo)記為“完成”,導(dǎo)致訪問不到請求數(shù)據(jù)。
解決方案:
方案 | 適用場景 | 優(yōu)勢 | 可能的缺點(diǎn) |
---|---|---|---|
提前復(fù)制 Cookie(推薦) | 簡單場景 | 線程安全、性能好 | 適用于 Cookie 訪問較少的場景 |
HttpServletRequestWrapper | 需要完整 request 功能 | 透明使用 request | 需要額外封裝 |
ThreadLocal 傳遞 Cookie | 復(fù)雜異步任務(wù) | 適用于跨線程、跨方法 | 需要手動清理 ThreadLocal |
最佳實(shí)踐:
- 如果只是讀取
Cookie
,建議在主線程復(fù)制數(shù)據(jù)后傳遞(方式 1)。 - 如果異步線程需要多個
request
方法,建議用HttpServletRequestWrapper
(方式 2)。 - 如果異步任務(wù)復(fù)雜,可以用
ThreadLocal
維護(hù)副本(方式 3)。
這樣就可以保證異步線程訪問 Cookie
而不會影響 request
的復(fù)用!
以上就是SpringBoot中短時間連續(xù)請求時出現(xiàn)Cookie獲取異常問題的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Cookie獲取異常的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在js與java中判斷json數(shù)據(jù)中是否含有某字段的案例
這篇文章主要介紹了在js與java中判斷json數(shù)據(jù)中是否含有某字段的案例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12手把手教你如何利用SpringBoot實(shí)現(xiàn)審核功能
審核功能經(jīng)過幾個小時的奮戰(zhàn)終于完成了,現(xiàn)在我就與廣大網(wǎng)友分享我的成果,這篇文章主要給大家介紹了關(guān)于如何利用SpringBoot實(shí)現(xiàn)審核功能的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05Mybatis多參數(shù)及實(shí)體對象傳遞實(shí)例講解
在使用Mybatis的時候,經(jīng)常會有各種各樣的參數(shù)傳遞,不同類型,不同個數(shù)的參數(shù),下面小編通過例子給大家講解下Mybatis多參數(shù)及實(shí)體對象傳遞,一起看看吧2016-12-12Java的PriorityBlockingQueue優(yōu)先級阻塞隊(duì)列代碼實(shí)例
這篇文章主要介紹了Java的PriorityBlockingQueue優(yōu)先級阻塞隊(duì)列代碼實(shí)例,PriorityBlockingQueue顧名思義是帶有優(yōu)先級的阻塞隊(duì)列,為了實(shí)現(xiàn)按優(yōu)先級彈出數(shù)據(jù),存入其中的對象必須實(shí)現(xiàn)comparable接口自定義排序方法,需要的朋友可以參考下2023-12-12如何解決springcloud feign 首次調(diào)用100%失敗的問題
這篇文章主要介紹了如何解決springcloud feign 首次調(diào)用100%失敗的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06