SpringSession系列-請求與響應重寫

glmapper發表於2018-11-24

我們知道,HttpServletRequsetHttpServletResponseServlet標準所指定的Java語言與Web容器進行互動的介面。介面本身只規定java語言對web容器進行訪問的行為方式,而具體的實現是由不同的web容器在其內部實現的。

那麼在執行期,當我們需要對HttpServletRequsetHttpServletResponse的預設例項進行擴充套件時,我們就可以繼承HttpServletRequestWrapperHttpServletResponseWrapper來實現。   

SpringSession中因為我們要實現不依賴容器本身的getSession 實現,因此需要擴充套件 HttpServletRequset,通過重寫getSession來實現分散式session的能力。下面就來看下SpringSession中對於HttpServletRequset的擴充套件。

1、請求重寫

SpringSession 中對於請求重寫,在能力上主要體現在儲存方面,也就是getSession方法上。在 SessionRepositoryFilter 這個類中,是通過內部類的方式實現了對HttpServletRequsetHttpServletResponse的擴充套件。

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;
}
複製程式碼

構造方法裡面將 HttpServletRequestHttpServletResponse 以及 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系列-請求與響應重寫

這裡可以看到SpringSession對於sessionId獲取的兩種策略,一種是基於cookie,一種是基於header;分別來看下具體實現。

1.4.1 CookieHttpSessionIdResolver 獲取 sessionId

CookieHttpSessionIdResolver 中獲取sessionId的核心程式碼如下:

SpringSession系列-請求與響應重寫
其實這裡沒啥好說的,就是讀cookie。從requestcookie資訊拿出來,然後遍歷找當前sessionId對應的cookie,這裡的判斷也很簡單, 如果是以SESSION開頭,則表示是 SessionId,畢竟cookie是共享的,不只有sessionId,還有可能儲存其他內容。

另外這裡面有個 jvmRoute,這個東西實際上很少能夠用到,因為大多數情況下這個值都是null。這個我們在分析CookieSerializer時再來解釋。

1.4.2 HeaderHttpSessionIdResolver 獲取 sessionId

SpringSession系列-請求與響應重寫
這個獲取更直接粗暴,就是根據 headerNameheader 中取值。

回到getRequestedSession,剩下的程式碼中核心的都是和sessionRepository這個有關係,這部分就會涉及到儲存部分。不在本篇的分析範圍之內,會在儲存實現部分來分析。

1.5 HttpSessionWrapper

SpringSession系列-請求與響應重寫

上面的程式碼中當我們拿到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的原理SessionCommittingRequestDispatcherforward的行為並沒有改變。 對於include則是在include之前提交session。為什麼這麼做呢?

因為include方法使原先的Servlet和轉發到的Servlet都可以輸出響應資訊,即原先的Servlet還可以繼續輸出響應資訊;即請求轉發後,原先的Servlet還可以繼續輸出響應資訊,轉發到的Servlet對請求做出的響應將併入原先Servlet的響應物件中。

所以這個在include呼叫之前呼叫commit,這樣可以確保被包含的Servlet程式不能改變響應訊息的狀態碼和響應頭。

2 響應重寫

響應重寫的目的是確保在請求提交時能夠把session儲存起來。來看下SessionRepositoryResponseWrapper類的實現:

SpringSession系列-請求與響應重寫
這裡面實現還就是重寫onResponseCommitted,也就是上面說的,在請求提交時能夠通過這個回撥函式將session儲存到儲存容器中。

2.1 session 提交

最後來看下 commitSession

SpringSession系列-請求與響應重寫

這個過程不會再去儲存容器中拿session資訊,而是直接從當前請求中拿。如果拿不到,則在回寫cookie時會將當前session對應的cookie值設定為空,這樣下次請求過來時攜帶的sessionCookie就是空,這樣就會重新觸發登陸。

如果拿到,則清空當前請求中的session資訊,然後將session儲存到儲存容器中,並且將sessionId回寫到cookie中。

小結

本篇主要對SpringSession中重寫RequestResponse進行了分析。通過重寫Request請求來將session的儲存與儲存容器關聯起來,通過重寫Response來處理session提交,將session儲存到儲存容器中。

後面我們會繼續來分析SpringSession的原始碼。最近也在學習鏈路跟蹤相關的技術,也準備寫一寫,有興趣的同學可以一起討論。

相關文章