SpringSession 請(qǐng)求與響應(yīng)重寫的實(shí)現(xiàn)
我們知道, HttpServletRequset 和 HttpServletResponse 是 Servlet 標(biāo)準(zhǔn)所指定的 Java 語(yǔ)言與 Web 容器進(jìn)行交互的接口。接口本身只規(guī)定 java 語(yǔ)言對(duì) web 容器進(jìn)行訪問的行為方式,而具體的實(shí)現(xiàn)是由不同的 web 容器在其內(nèi)部實(shí)現(xiàn)的。
那么在運(yùn)行期,當(dāng)我們需要對(duì) HttpServletRequset 和 HttpServletResponse 的默認(rèn)實(shí)例進(jìn)行擴(kuò)展時(shí),我們就可以繼承 HttpServletRequestWrapper 和 HttpServletResponseWrapper 來(lái)實(shí)現(xiàn)。
在 SpringSession 中因?yàn)槲覀円獙?shí)現(xiàn)不依賴容器本身的 getSession 實(shí)現(xiàn),因此需要擴(kuò)展 HttpServletRequset ,通過重寫 getSession 來(lái)實(shí)現(xiàn)分布式 session 的能力。下面就來(lái)看下 SpringSession 中對(duì)于 HttpServletRequset 的擴(kuò)展。
1、請(qǐng)求重寫
SpringSession 中對(duì)于請(qǐng)求重寫,在能力上主要體現(xiàn)在存儲(chǔ)方面,也就是 getSession 方法上。在 SessionRepositoryFilter 這個(gè)類中,是通過內(nèi)部類的方式實(shí)現(xiàn)了對(duì) HttpServletRequset 和 HttpServletResponse 的擴(kuò)展。
1.1 HttpServletRequset 擴(kuò)展實(shí)現(xiàn)
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
// HttpServletResponse 實(shí)例
private final HttpServletResponse response;
// ServletContext 實(shí)例
private final ServletContext servletContext;
// requestedSession session對(duì)象
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 實(shí)例傳遞進(jìn)來(lái),以便于后續(xù)擴(kuò)展使用。
1.3 getSession 方法
@Override
public HttpSessionWrapper getSession(boolean create) {
// 從當(dāng)前請(qǐng)求線程中獲取 session
HttpSessionWrapper currentSession = getCurrentSession();
// 如果有直接返回
if (currentSession != null) {
return currentSession;
}
// 從請(qǐng)求中獲取 session,這里面會(huì)涉及到從緩存中拿session的過程
S requestedSession = getRequestedSession();
if (requestedSession != null) {
// 無(wú)效的會(huì)話id(不支持的會(huì)話存儲(chǔ)庫(kù))請(qǐng)求屬性名稱。
// 這里看下當(dāng)前的sessionId是否有效
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
// 設(shè)置當(dāng)前session的最后訪問時(shí)間,用于延遲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)前請(qǐng)求上下文
setCurrentSession(currentSession);
// 返回session
return currentSession;
}
}
else {
// 這里處理的是無(wú)效的sessionId的情況,但是當(dāng)前請(qǐ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");
}
// 是否需要?jiǎng)?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è)置最后訪問時(shí)間,也就是指定了當(dāng)前session的有效期限
session.setLastAccessedTime(Instant.now());
// 包裝下當(dāng)前session
currentSession = new HttpSessionWrapper(session, getServletContext());
//設(shè)置到當(dāng)前請(qǐng)求線程
setCurrentSession(currentSession);
return currentSession;
}
上面這段代碼有幾個(gè)點(diǎn),這里單獨(dú)來(lái)解釋下。
getCurrentSession
這是為了在同一個(gè)請(qǐng)求過程中不需要重復(fù)的去從存儲(chǔ)中獲取session,在一個(gè)新的進(jìn)來(lái)時(shí),將當(dāng)前的 session 設(shè)置到當(dāng)前請(qǐng)求中,在后續(xù)處理過程如果需要getSession就不需要再去存儲(chǔ)介質(zhì)中再拿一次。
getRequestedSession
這個(gè)是根據(jù)請(qǐng)求信息去取 session ,這里面就包括了 sessionId 解析,從存儲(chǔ)獲取 session 對(duì)象等過程。
是否創(chuàng)建新的 session 對(duì)象
在當(dāng)前請(qǐng)求中和存儲(chǔ)中都沒有獲取到 session 信息的情況下,這里會(huì)根據(jù) create 參數(shù)來(lái)判斷是否創(chuàng)建新的 session 。這里一般用戶首次登錄時(shí)或者 session 失效時(shí)會(huì)走到。
1.4 getRequestedSession
根據(jù)請(qǐng)求信息來(lái)獲取 session 對(duì)象
private S getRequestedSession() {
// 緩存的請(qǐng)求session是否存在
if (!this.requestedSessionCached) {
// 獲取 sessionId
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
.resolveSessionIds(this);
// 通過sessionId來(lái)從存儲(chǔ)中獲取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 返回的是個(gè)列表。當(dāng)然這里是 SpringSession 的實(shí)現(xiàn)策略,因?yàn)橹С?session ,所以這里以列表的形式返回的。OK,繼續(xù)來(lái)看如何解析 sessionId 的:

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

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

這個(gè)過程不會(huì)再去存儲(chǔ)容器中拿 session 信息,而是直接從當(dāng)前請(qǐng)求中拿。如果拿不到,則在回寫 cookie 時(shí)會(huì)將當(dāng)前 session 對(duì)應(yīng)的 cookie 值設(shè)置為空,這樣下次請(qǐng)求過來(lái)時(shí)攜帶的 sessionCookie 就是空,這樣就會(huì)重新觸發(fā)登陸。
如果拿到,則清空當(dāng)前請(qǐng)求中的 session 信息,然后將 session 保存到存儲(chǔ)容器中,并且將 sessionId 回寫到 cookie 中。
小結(jié)
本篇主要對(duì) SpringSession 中重寫 Request 和 Response 進(jìn)行了分析。通過重寫 Request 請(qǐng)求來(lái)將 session 的存儲(chǔ)與存儲(chǔ)容器關(guān)聯(lián)起來(lái),通過重寫 Response 來(lái)處理 session 提交,將 session 保存到存儲(chǔ)容器中。
后面我們會(huì)繼續(xù)來(lái)分析 SpringSession 的源碼。最近也在學(xué)習(xí)鏈路跟蹤相關(guān)的技術(shù),也準(zhǔn)備寫一寫,有興趣的同學(xué)可以一起討論。 希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java使用poi導(dǎo)出ppt文件的實(shí)現(xiàn)代碼
Apache POI 是用Java編寫的免費(fèi)開源的跨平臺(tái)的 Java API,Apache POI提供API給Java對(duì)Microsoft Office格式檔案讀和寫的功能。本文給大家介紹Java使用poi導(dǎo)出ppt文件的實(shí)現(xiàn)代碼,需要的朋友參考下吧2021-06-06
SpringSecurity 自定義表單登錄的實(shí)現(xiàn)
這篇文章主要介紹了SpringSecurity 自定義表單登錄的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
Java并發(fā)應(yīng)用之任務(wù)執(zhí)行分析
這篇文章主要為大家詳細(xì)介紹了JavaJava并發(fā)應(yīng)用編程中任務(wù)執(zhí)行分析的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-07-07
java中BCryptPasswordEncoder密碼的加密與驗(yàn)證方式
這篇文章主要介紹了java中BCryptPasswordEncoder密碼的加密與驗(yàn)證方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
Java RandomAccessFile 指定位置實(shí)現(xiàn)文件讀取與寫入
這篇文章主要介紹了Java RandomAccessFile 指定位置實(shí)現(xiàn)文件讀取與寫入的相關(guān)資料,需要的朋友可以參考下2017-01-01
超級(jí)好用的輕量級(jí)JSON處理命令jq(最新推薦)
jq是一個(gè)輕量級(jí)的命令行工具,讓你可以非常方便地處理JSON數(shù)據(jù),如切分、過濾、映射、轉(zhuǎn)化等,就像sed、awk、grep文本處理三劍客一樣,這篇文章主要介紹了超級(jí)好用的輕量級(jí)JSON處理命令jq,需要的朋友可以參考下2023-01-01
mybatisplus邏輯刪除基本實(shí)現(xiàn)和坑點(diǎn)解決
這篇文章主要介紹了mybatisplus邏輯刪除基本實(shí)現(xiàn)和坑點(diǎn)解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
一文詳解如何通過Java實(shí)現(xiàn)SSL交互功能
這篇文章主要為大家詳細(xì)介紹了如何通過Java實(shí)現(xiàn)SSL交互功能,文中的示例代碼講解詳細(xì),對(duì)我們的學(xué)習(xí)或工作有一定的幫助,需要的可以參考一下2023-04-04
Java程序的初始化順序,static{}靜態(tài)代碼塊和實(shí)例語(yǔ)句塊的使用方式
這篇文章主要介紹了Java程序的初始化順序,static{}靜態(tài)代碼塊和實(shí)例語(yǔ)句塊的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01

