這篇文章主要介紹了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
的源碼。最近也在學習鏈路跟蹤相關的技術,也準備寫一寫,有興趣的同學可以一起討論。 希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。