SpringSession 請求與響應(yīng)重寫的實現(xiàn)
我們知道, HttpServletRequset 和 HttpServletResponse 是 Servlet 標(biāo)準(zhǔn)所指定的 Java 語言與 Web 容器進行交互的接口。接口本身只規(guī)定 java 語言對 web 容器進行訪問的行為方式,而具體的實現(xiàn)是由不同的 web 容器在其內(nèi)部實現(xiàn)的。
那么在運行期,當(dāng)我們需要對 HttpServletRequset 和 HttpServletResponse 的默認(rèn)實例進行擴展時,我們就可以繼承 HttpServletRequestWrapper 和 HttpServletResponseWrapper 來實現(xiàn)。
在 SpringSession 中因為我們要實現(xiàn)不依賴容器本身的 getSession 實現(xiàn),因此需要擴展 HttpServletRequset ,通過重寫 getSession 來實現(xiàn)分布式 session 的能力。下面就來看下 SpringSession 中對于 HttpServletRequset 的擴展。
1、請求重寫
SpringSession 中對于請求重寫,在能力上主要體現(xiàn)在存儲方面,也就是 getSession 方法上。在 SessionRepositoryFilter 這個類中,是通過內(nèi)部類的方式實現(xiàn)了對 HttpServletRequset 和 HttpServletResponse 的擴展。
1.1 HttpServletRequset 擴展實現(xiàn)
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
// HttpServletResponse 實例
private final HttpServletResponse response;
// ServletContext 實例
private final ServletContext servletContext;
// requestedSession session對象
private S requestedSession;
// 是否緩存 session
private boolean requestedSessionCached;
// sessionId
private String requestedSessionId;
// sessionId 是否有效
private Boolean requestedSessionIdValid;
// sessionId 是否失效
private boolean requestedSessionInvalidated;
// 省略方法
}
1.2 構(gòu)造方法
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
構(gòu)造方法里面將 HttpServletRequest 、 HttpServletResponse 以及 ServletContext 實例傳遞進來,以便于后續(xù)擴展使用。
1.3 getSession 方法
@Override
public HttpSessionWrapper getSession(boolean create) {
// 從當(dāng)前請求線程中獲取 session
HttpSessionWrapper currentSession = getCurrentSession();
// 如果有直接返回
if (currentSession != null) {
return currentSession;
}
// 從請求中獲取 session,這里面會涉及到從緩存中拿session的過程
S requestedSession = getRequestedSession();
if (requestedSession != null) {
// 無效的會話id(不支持的會話存儲庫)請求屬性名稱。
// 這里看下當(dāng)前的sessionId是否有效
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
// 設(shè)置當(dāng)前session的最后訪問時間,用于延遲session的有效期
requestedSession.setLastAccessedTime(Instant.now());
// 將requestedSessionIdValid置為true
this.requestedSessionIdValid = true;
// 包裝session
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
// 不是新的session,如果是新的session則需要改變sessionId
currentSession.setNew(false);
// 將session設(shè)置到當(dāng)前請求上下文
setCurrentSession(currentSession);
// 返回session
return currentSession;
}
}
else {
// 這里處理的是無效的sessionId的情況,但是當(dāng)前請求線程 session有效
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
// 將invalidSessionId置為true
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
// 是否需要創(chuàng)建新的session
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
// 創(chuàng)建新的session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
// 設(shè)置最后訪問時間,也就是指定了當(dāng)前session的有效期限
session.setLastAccessedTime(Instant.now());
// 包裝下當(dāng)前session
currentSession = new HttpSessionWrapper(session, getServletContext());
//設(shè)置到當(dāng)前請求線程
setCurrentSession(currentSession);
return currentSession;
}
上面這段代碼有幾個點,這里單獨來解釋下。
getCurrentSession
這是為了在同一個請求過程中不需要重復(fù)的去從存儲中獲取session,在一個新的進來時,將當(dāng)前的 session 設(shè)置到當(dāng)前請求中,在后續(xù)處理過程如果需要getSession就不需要再去存儲介質(zhì)中再拿一次。
getRequestedSession
這個是根據(jù)請求信息去取 session ,這里面就包括了 sessionId 解析,從存儲獲取 session 對象等過程。
是否創(chuàng)建新的 session 對象
在當(dāng)前請求中和存儲中都沒有獲取到 session 信息的情況下,這里會根據(jù) create 參數(shù)來判斷是否創(chuàng)建新的 session 。這里一般用戶首次登錄時或者 session 失效時會走到。
1.4 getRequestedSession
根據(jù)請求信息來獲取 session 對象
private S getRequestedSession() {
// 緩存的請求session是否存在
if (!this.requestedSessionCached) {
// 獲取 sessionId
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
.resolveSessionIds(this);
// 通過sessionId來從存儲中獲取session
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
S session = SessionRepositoryFilter.this.sessionRepository
.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
這段代碼還是很有意思的,這里獲取 sessionId 返回的是個列表。當(dāng)然這里是 SpringSession 的實現(xiàn)策略,因為支持 session ,所以這里以列表的形式返回的。OK,繼續(xù)來看如何解析 sessionId 的:

這里可以看到 SpringSession 對于 sessionId 獲取的兩種策略,一種是基于 cookie ,一種是基于 header ;分別來看下具體實現(xiàn)。
1.4.1 CookieHttpSessionIdResolver 獲取 sessionId
CookieHttpSessionIdResolver 中獲取 sessionId 的核心代碼如下:
其實這里沒啥好說的,就是讀 cookie 。從 request 將 cookie 信息拿出來,然后遍歷找當(dāng)前 sessionId 對應(yīng)的 cookie ,這里的判斷也很簡單, 如果是以 SESSION 開頭,則表示是 SessionId ,畢竟 cookie 是共享的,不只有 sessionId,還有可能存儲其他內(nèi)容。
另外這里面有個 jvmRoute,這個東西實際上很少能夠用到,因為大多數(shù)情況下這個值都是null。這個我們在分析 CookieSerializer 時再來解釋。
1.4.2 HeaderHttpSessionIdResolver 獲取 sessionId
這個獲取更直接粗暴,就是根據(jù) headerName 從 header中取值。
回到 getRequestedSession ,剩下的代碼中核心的都是和 sessionRepository 這個有關(guān)系,這部分就會涉及到存儲部分。不在本篇的分析范圍之內(nèi),會在存儲實現(xiàn)部分來分析。
1.5 HttpSessionWrapper

上面的代碼中當(dāng)我們拿到 session 實例是通常會包裝下,那么用到的就是這個 HttpSessionWrapper 。
HttpSessionWrapper 繼承了 HttpSessionAdapter ,這個 HttpSessionAdapter 就是將SpringSession 轉(zhuǎn)換成一個標(biāo)準(zhǔn) HttpSession 的適配類。 HttpSessionAdapter 實現(xiàn)了標(biāo)準(zhǔn) servlet 規(guī)范的 HttpSession 接口。
1.5.1 HttpSessionWrapper
HttpSessionWrapper 重寫了 invalidate 方法。從代碼來看,調(diào)用該方法產(chǎn)生的影響是:
requestedSessionInvalidated置為true,標(biāo)識當(dāng)前session失效。- 將當(dāng)前請求中的
session設(shè)置為null,那么在請求的后續(xù)調(diào)用中通過getCurrentSession將拿不到session信息。 - 當(dāng)前緩存的 session 清楚,包括sessionId,session實例等。
- 刪除存儲介質(zhì)中的session對象。
1.5.2 HttpSessionAdapter
SpringSession 和標(biāo)準(zhǔn) HttpSession 的配置器類。這個怎么理解呢,來看下一段代碼:
@Override
public Object getAttribute(String name) {
checkState();
return this.session.getAttribute(name);
}
對于基于容器本身實現(xiàn)的 HttpSession 來說, getAttribute 的實現(xiàn)也是有容器本身決定。但是這里做了轉(zhuǎn)換之后, getAttribute 將會通過 SpringSession 中實現(xiàn)的方案來獲取。其他的 API 適配也是基于此實現(xiàn)。
SessionCommittingRequestDispatcher
實現(xiàn)了 RequestDispatcher 接口。關(guān)于 RequestDispatcher 可以參考這篇文章【Servlet】關(guān)于RequestDispatcher的原理 。 SessionCommittingRequestDispatcher 對 forward 的行為并沒有改變。 對于 include 則是在 include 之前提交 session 。為什么這么做呢?
因為 include 方法使原先的 Servlet 和轉(zhuǎn)發(fā)到的 Servlet 都可以輸出響應(yīng)信息,即原先的 Servlet 還可以繼續(xù)輸出響應(yīng)信息;即請求轉(zhuǎn)發(fā)后,原先的 Servlet 還可以繼續(xù)輸出響應(yīng)信息,轉(zhuǎn)發(fā)到的 Servlet 對請求做出的響應(yīng)將并入原先 Servlet 的響應(yīng)對象中。
所以這個在 include 調(diào)用之前調(diào)用 commit ,這樣可以確保被包含的 Servlet 程序不能改變響應(yīng)消息的狀態(tài)碼和響應(yīng)頭。
2 響應(yīng)重寫
響應(yīng)重寫的目的是確保在請求提交時能夠把session保存起來。來看下 SessionRepositoryResponseWrapper 類的實現(xiàn):
這里面實現(xiàn)還就是重寫 onResponseCommitted ,也就是上面說的,在請求提交時能夠通過這個回調(diào)函數(shù)將 session
保存到存儲容器中。
2.1 session 提交
最后來看下 commitSession

這個過程不會再去存儲容器中拿 session 信息,而是直接從當(dāng)前請求中拿。如果拿不到,則在回寫 cookie 時會將當(dāng)前 session 對應(yīng)的 cookie 值設(shè)置為空,這樣下次請求過來時攜帶的 sessionCookie 就是空,這樣就會重新觸發(fā)登陸。
如果拿到,則清空當(dāng)前請求中的 session 信息,然后將 session 保存到存儲容器中,并且將 sessionId 回寫到 cookie 中。
小結(jié)
本篇主要對 SpringSession 中重寫 Request 和 Response 進行了分析。通過重寫 Request 請求來將 session 的存儲與存儲容器關(guān)聯(lián)起來,通過重寫 Response 來處理 session 提交,將 session 保存到存儲容器中。
后面我們會繼續(xù)來分析 SpringSession 的源碼。最近也在學(xué)習(xí)鏈路跟蹤相關(guān)的技術(shù),也準(zhǔn)備寫一寫,有興趣的同學(xué)可以一起討論。 希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java通過數(shù)據(jù)庫表生成實體類詳細(xì)過程
這篇文章主要介紹了Java通過數(shù)據(jù)庫表生成實體類,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02
Java?中很好用的數(shù)據(jù)結(jié)構(gòu)EnumSet
這篇文章主要介紹了Java?中很好用的數(shù)據(jù)結(jié)構(gòu)EnumSet,EnumMap即屬于一個Map,下文圍繞主題展開詳細(xì)內(nèi)容,需要的小伙伴可以參考參考一下2022-05-05
java:無法訪問org.springframework.boot.SpringApplication
本文主要介紹了java:無法訪問org.springframework.boot.SpringApplication,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
如何在 Java 中利用 redis 實現(xiàn) LBS 服務(wù)
基于位置的服務(wù),是指通過電信移動運營商的無線電通訊網(wǎng)絡(luò)或外部定位方式,獲取移動終端用戶的位置信息,在GIS平臺的支持下,為用戶提供相應(yīng)服務(wù)的一種增值業(yè)務(wù)。下面我們來一起學(xué)習(xí)一下吧2019-06-06
springboot中通過jwt令牌校驗及前端token請求頭進行登錄攔截實戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于springboot中如何通過jwt令牌校驗及前端token請求頭進行登錄攔截的相關(guān)資料,需要的朋友可以參考下2024-08-08
Springboot2.x 使用 Log4j2 異步打印日志的實現(xiàn)
這篇文章主要介紹了Springboot2.x 使用 Log4j2 異步打印日志的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
Java中關(guān)于ThreadLocal的隱式引用詳解
這篇文章主要介紹了Java中關(guān)于ThreadLocal的隱式引用,從線程的角度看,每個線程都保持一個對其線程局部變量副本的隱式引用,只要線程是活動的,ThreadLocal實例就是可訪問的,下面我們來具體看看2024-03-03
Java利用docx4j+Freemarker生成word文檔
這篇文章主要為大家詳細(xì)介紹了Java如何利用docx4j+Freemarker生成word文檔,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-04-04

