SpringSession:請求與響應重寫
我們知道,HttpServletRequset
和HttpServletResponse
是Servlet
標準所指定的Java
語言與Web
容器進行互動的介面。介面本身只規定java
語言對web
容器進行訪問的行為方式,而具體的實現是由不同的web
容器在其內部實現的。
那麼在執行期,當我們需要對HttpServletRequset
和HttpServletResponse
的預設例項進行擴充套件時,我們就可以繼承HttpServletRequestWrapper
和HttpServletResponseWrapper
來實現。
在 SpringSession
中因為我們要實現不依賴容器本身的getSession
HttpServletRequset
,通過重寫getSession
來實現分散式session
的能力。下面就來看下SpringSession
中對於HttpServletRequset
的擴充套件。
1、請求重寫
SpringSession
中對於請求重寫,在能力上主要體現在儲存方面,也就是getSession
方法上。在 SessionRepositoryFilter
這個類中,是通過內部類的方式實現了對HttpServletRequset
和HttpServletResponse
的擴充套件。
1.1 HttpServletRequset 擴充套件實現
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 構造方法
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
複製程式碼
構造方法裡面將 HttpServletRequest
、HttpServletResponse
以及 ServletContext
例項傳遞進來,以便於後續擴充套件使用。
1.3 getSession 方法
@Override
public HttpSessionWrapper getSession(boolean create) {
// 從當前請求執行緒中獲取 session
HttpSessionWrapper currentSession = getCurrentSession();
// 如果有直接返回
if (currentSession != null) {
return currentSession;
}
// 從請求中獲取 session,這裡面會涉及到從快取中拿session的過程
S requestedSession = getRequestedSession();
if (requestedSession != null) {
// 無效的會話id(不支援的會話儲存庫)請求屬性名稱。
// 這裡看下當前的sessionId是否有效
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
// 設定當前session的最後訪問時間,用於延遲session的有效期
requestedSession.setLastAccessedTime(Instant.now());
// 將requestedSessionIdValid置為true
this.requestedSessionIdValid = true;
// 包裝session
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
// 不是新的session,如果是新的session則需要改變sessionId
currentSession.setNew(false);
// 將session設定到當前請求上下文
setCurrentSession(currentSession);
// 返回session
return currentSession;
}
}
else {
// 這裡處理的是無效的sessionId的情況,但是當前請求執行緒 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");
}
// 是否需要建立新的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)"));
}
// 建立新的session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
// 設定最後訪問時間,也就是指定了當前session的有效期限
session.setLastAccessedTime(Instant.now());
// 包裝下當前session
currentSession = new HttpSessionWrapper(session, getServletContext());
//設定到當前請求執行緒
setCurrentSession(currentSession);
return currentSession;
}
複製程式碼
上面這段程式碼有幾個點,這裡單獨來解釋下。
getCurrentSession
- 這是為了在同一個請求過程中不需要重複的去從儲存中獲取session,在一個新的進來時,將當前的 session 設定到當前請求中,在後續處理過程如果需要getSession就不需要再去儲存介質中再拿一次。
getRequestedSession
- 這個是根據請求資訊去取
session
,這裡面就包括了sessionId
解析,從儲存獲取session
物件等過程。
- 這個是根據請求資訊去取
- 是否建立新的
session
物件- 在當前請求中和儲存中都沒有獲取到
session
資訊的情況下,這裡會根據create
引數來判斷是否建立新的session
。這裡一般使用者首次登入時或者session
失效時會走到。
- 在當前請求中和儲存中都沒有獲取到
1.4 getRequestedSession
根據請求資訊來獲取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
返回的是個列表。當然這裡是SpringSession
的實現策略,因為支援session
,所以這裡以列表的形式返回的。OK,繼續來看如何解析sessionId
的:
這裡可以看到SpringSession
對於sessionId
獲取的兩種策略,一種是基於cookie
,一種是基於header
;分別來看下具體實現。
1.4.1 CookieHttpSessionIdResolver 獲取 sessionId
CookieHttpSessionIdResolver
中獲取sessionId
的核心程式碼如下:
cookie
。從
request
將
cookie
資訊拿出來,然後遍歷找當前
sessionId
對應的
cookie
,這裡的判斷也很簡單, 如果是以
SESSION
開頭,則表示是
SessionId
,畢竟
cookie
是共享的,不只有
sessionId
,還有可能儲存其他內容。
另外這裡面有個 jvmRoute,這個東西實際上很少能夠用到,因為大多數情況下這個值都是null。這個我們在分析CookieSerializer
時再來解釋。
1.4.2 HeaderHttpSessionIdResolver 獲取 sessionId
這個獲取更直接粗暴,就是根據headerName
從
header
中取值。
回到getRequestedSession
,剩下的程式碼中核心的都是和sessionRepository
這個有關係,這部分就會涉及到儲存部分。不在本篇的分析範圍之內,會在儲存實現部分來分析。
1.5 HttpSessionWrapper
上面的程式碼中當我們拿到session
例項是通常會包裝下,那麼用到的就是這個HttpSessionWrapper
。
HttpSessionWrapper
繼承了 HttpSessionAdapter
,這個HttpSessionAdapter
就是將SpringSession 轉換成一個標準HttpSession
的適配類。HttpSessionAdapter
實現了標準servlet
規範的HttpSession
介面。
1.5.1 HttpSessionWrapper
HttpSessionWrapper
重寫了 invalidate
方法。從程式碼來看,呼叫該方法產生的影響是:
requestedSessionInvalidated
置為true
,標識當前session
失效。- 將當前請求中的
session
設定為null
,那麼在請求的後續呼叫中通過getCurrentSession
將拿不到session
資訊。 - 當前快取的 session 清楚,包括sessionId,session例項等。
- 刪除儲存介質中的session物件。
1.5.2 HttpSessionAdapter
SpringSession
和標準HttpSession
的配置器類。這個怎麼理解呢,來看下一段程式碼:
@Override
public Object getAttribute(String name) {
checkState();
return this.session.getAttribute(name);
}
複製程式碼
對於基於容器本身實現的HttpSession
來說,getAttribute
的實現也是有容器本身決定。但是這裡做了轉換之後,getAttribute
將會通過SpringSession
中實現的方案來獲取。其他的API
適配也是基於此實現。
SessionCommittingRequestDispatcher
實現了 RequestDispatcher
介面。關於RequestDispatcher
可以參考這篇文章【Servlet】關於RequestDispatcher的原理。SessionCommittingRequestDispatcher
對forward
的行為並沒有改變。 對於include
則是在include
之前提交session
。為什麼這麼做呢?
因為include
方法使原先的Servlet
和轉發到的Servlet
都可以輸出響應資訊,即原先的Servlet
還可以繼續輸出響應資訊;即請求轉發後,原先的Servlet
還可以繼續輸出響應資訊,轉發到的Servlet
對請求做出的響應將併入原先Servlet
的響應物件中。
所以這個在include
呼叫之前呼叫commit
,這樣可以確保被包含的Servlet
程式不能改變響應訊息的狀態碼和響應頭。
2 響應重寫
響應重寫的目的是確保在請求提交時能夠把session儲存起來。來看下SessionRepositoryResponseWrapper
類的實現:
onResponseCommitted
,也就是上面說的,在請求提交時能夠通過這個回撥函式將
session
儲存到儲存容器中。
2.1 session 提交
最後來看下 commitSession
這個過程不會再去儲存容器中拿session
資訊,而是直接從當前請求中拿。如果拿不到,則在回寫cookie
時會將當前session
對應的cookie
值設定為空,這樣下次請求過來時攜帶的sessionCookie
就是空,這樣就會重新觸發登陸。
如果拿到,則清空當前請求中的session
資訊,然後將session
儲存到儲存容器中,並且將sessionId
回寫到cookie
中。
小結
本篇主要對SpringSession
中重寫Request
和Response
進行了分析。通過重寫Request
請求來將session
的儲存與儲存容器關聯起來,通過重寫Response
來處理session
提交,將session
儲存到儲存容器中。
後面我們會繼續來分析SpringSession
的原始碼。最近也在學習鏈路跟蹤相關的技術,也準備寫一寫,有興趣的同學可以一起討論。