SpringSession的源碼解析(生成session,保存session,寫入cookie全流程分析)
文章目錄
- 前言
- 基礎(chǔ)介紹
- 操作session(生成session,保存session等過程)的時序圖
- 1. 調(diào)用的入口還是SessionRepositoryFilter類(PS:Spring是通過責任鏈的模式來執(zhí)行每個過濾器的)的doFilterInternal方法。
- 2. SessionRepositoryRequestWrapper類的getSession(true)方法
- 3. RedisOperationsSessionRepository類的`createSession()`方法
- 4. CookieHttpSessionIdResolver類的`setSessionId`方法
- 5. DefaultCookieSerializer類的`writeCookieValue`方法
- 總結(jié)
- 源代碼
前言
上一篇文章主要介紹了如何使用SpringSession,其實SpringSession的使用并不是很難,無非就是引入依賴,加下配置。但是,這僅僅只是知其然,要知其所以然,我們還是需要深入源碼去理解。在看本文先我們先想想,下面這些問題Session是啥時候創(chuàng)建的呢?通過什么來創(chuàng)建的呢?創(chuàng)建之后如何保存到Redis?又是如何把SessionId設(shè)置到Cookie中的呢?帶著這一系列的問題,今天就讓我們來揭開SpringSession的神秘面紗,如果讀者朋友們看完本文之后能夠輕松的回答上面的問題,那本文的作用也就達到了。當然,如果您已經(jīng)對這些知識了若指掌,那么就不需要看本文了。
看源碼的過程真的是一個很枯燥乏味的過程,但是弄清楚了其調(diào)用過程之后,又是很讓人興奮的,話不多說,直接進入正題。
基礎(chǔ)介紹
默認參數(shù)的設(shè)置
首先,我們從添加的SpringSession的配置類來看起,如下,是一段很基礎(chǔ)的配置代碼,就添加了@Configuration
注解和@EnableRedisHttpSession
注解。其中@Configuration
注解標注在類上,相當于把該類作為spring的xml配置文件中的<beans>
,作用為:配置spring容器(應(yīng)用上下文),@EnableRedisHttpSession
注解的作用是使SpringSession生效。
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1)
public class SessionConfig {
}
點到EnableRedisHttpSession注解中,我們可以看到里面定義了RedisHttpSessionConfiguration的設(shè)置類,以及一些基礎(chǔ)參數(shù)的設(shè)置,例如:session默認的失效時間,存入到redis的key的前綴名,這些我們參數(shù)我們在使用注解時都可以重新設(shè)置。例如:maxInactiveIntervalInSeconds設(shè)置為-1表示用不失效。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
//默認最大的失效時間是30分鐘
int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS(1800秒);
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
//存入到redis的key的前綴名
public static final String DEFAULT_NAMESPACE = "spring:session";
String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
}
RedisHttpSessionConfiguration類是一個設(shè)置類,內(nèi)部的作用主要是實例化RedisOperationsSessionRepository對象和RedisMessageListenerContainer對象等以及設(shè)置操作redis的工具類。
主要類的說明
操作session(生成session,保存session等過程)的時序圖
首先,我們先看一下生成Session的調(diào)用時序圖。
1. 調(diào)用的入口還是SessionRepositoryFilter類(PS:Spring是通過責任鏈的模式來執(zhí)行每個過濾器的)的doFilterInternal方法。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//省略部分代碼
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
try {
//執(zhí)行其他過濾器
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
//wrappedRequest是SessionRepositoryRequestWrapper類的一個實例
wrappedRequest.commitSession();
}
}
2. SessionRepositoryRequestWrapper類的getSession(true)方法
經(jīng)過斷點調(diào)試,并查看調(diào)用棧,發(fā)現(xiàn)調(diào)用這個filterChain.doFilter(wrappedRequest, wrappedResponse);
方法之后,最終會調(diào)用到SessionRepositoryRequestWrapper類的getSession(true)
方法。其中,SessionRepositoryRequestWrapper類是SessionRepositoryFilter類的一個私有的不可被繼承,被重寫的內(nèi)部類。
public HttpSessionWrapper getSession(boolean create) {
//1. 獲取HttpSessionWrapper實例,如果可以獲取到,則說明session已經(jīng)生成了。就直接返回
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//如果可以獲取到session
S requestedSession = getRequestedSession();
//如果HttpSessionWrapper實例為空,則需要將session對象封裝到HttpSessionWrapper實例中,并設(shè)置到HttpRequestSerlvet中
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
}
//如果獲取不到session,則進入下面分支,創(chuàng)建session
else {
//省略部分代碼
//如果create為false,直接返回null
if (!create) {
return null;
}
//省略部分代碼
//如果create為true,則調(diào)用RedisOperationsSessionRepository類的createSession方法創(chuàng)建session實例
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
如上代碼所示:getSession(boolean create)
方法主要有兩塊,1. 獲取session實例,如果請求頭中帶著sessionid,則表示不是第一次請求,是可以獲取到session的。2. 如果瀏覽器是第一次請求應(yīng)用(沒有sessionid)則獲取不到session實例,需要創(chuàng)建session實例。在拿到生成的Session對象之后,緊接著會創(chuàng)建一個HttpSessionWrapper實例,并將前面生成的session傳入其中,方便后面取用,然后將HttpSessionWrapper實例放入當前請求會話HttpServletRequest中,(Key是.CURRENT_SESSION,value是HttpSessionWrapper的實例)。
3. RedisOperationsSessionRepository類的createSession()
方法
從前面的代碼分析我們可以知道如果獲取不到session實例,則會調(diào)用createSession()
方法進行創(chuàng)建。這個方法是在RedisOperationsSessionRepository類中,該方法比較簡單,主要就是實例化RedisSession對象。其中RedisSession對象中包括了sessionid,creationTime,maxInactiveInterval和lastAccessedTime等屬性。其中原始的sessionid是一段唯一的UUID字符串。
@Override
public RedisSession createSession() {
//實例化RedisSession對象
RedisSession redisSession = new RedisSession();
if (this.defaultMaxInactiveInterval != null) {
//設(shè)置session的失效時間
redisSession.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return redisSession;
}
RedisSession() {
this(new MapSession());
this.delta.put(CREATION_TIME_KEY, getCreationTime().toEpochMilli());
this.delta.put(MAX_INACTIVE_INTERVAL_KEY,
(int) getMaxInactiveInterval().getSeconds());
this.delta.put(LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.isNew = true;
this.flushImmediateIfNecessary();
}
另外,doFilterInternal方法在調(diào)用完其他方法之后,在finally代碼塊中會調(diào)用SessionRepositoryRequestWrapper類內(nèi)部的commitSession()方法,而commitSession()方法會保存session信息到Redis中,并將sessionid寫到cookie中。我們接著來看看commitSession()方法。
private void commitSession() {
//當前請求會話中獲取HttpSessionWrapper對象的實例
HttpSessionWrapper wrappedSession = getCurrentSession();
//如果wrappedSession為空則調(diào)用expireSession寫入一個空值的cookie
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
this.response);
}
}
else {
//獲取session
S session = wrappedSession.getSession();
clearRequestedSessionCache();
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid()
|| !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
this.response, sessionId);
}
}
}
第一步就是從當前請求會話中獲取HttpSessionWrapper對象的實例,如果實例獲取不到則向Cookie中寫入一個空值。如果可以獲取到實例的話,則從實例中獲取Session對象。獲取到Session對象之后則調(diào)用RedisOperationsSessionRepository類的save(session)
方法將session信息保存到Redis中,其中redis的名稱前綴是spring:session
。將數(shù)據(jù)保存到Redis之后
緊接著獲取sessionid,最后調(diào)用CookieHttpSessionIdResolver類的setSessionId
方法將sessionid設(shè)置到Cookie中。
4. CookieHttpSessionIdResolver類的setSessionId
方法
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId) {
//如果sessionid等于請求頭中的sessionid,則直接返回
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
//將sessionid設(shè)置到請求頭中
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
//將sessionid寫入cookie中
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, sessionId));
}
從上代碼我們可以看出,setSessionId
方法主要就是將生成的sessionid設(shè)置到請求會話中,然后調(diào)用DefaultCookieSerializer類的writeCookieValue
方法將sessionid設(shè)置到cookie中。
5. DefaultCookieSerializer類的writeCookieValue
方法
@Override
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
StringBuilder sb = new StringBuilder();
//設(shè)置cookie的名稱,默認是SESSION
sb.append(this.cookieName).append('=');
//設(shè)置cookie的值,就是傳入的sessionid
String value = getValue(cookieValue);
if (value != null && value.length() > 0) {
validateValue(value);
sb.append(value);
}
//設(shè)置cookie的失效時間
int maxAge = getMaxAge(cookieValue);
if (maxAge > -1) {
sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
OffsetDateTime expires = (maxAge != 0)
? OffsetDateTime.now().plusSeconds(maxAge)
: Instant.EPOCH.atOffset(ZoneOffset.UTC);
sb.append("; Expires=")
.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
String domain = getDomainName(request);
//設(shè)置Domain屬性,默認就是當前請求的域名,或者ip
if (domain != null && domain.length() > 0) {
validateDomain(domain);
sb.append("; Domain=").append(domain);
}
//設(shè)置Path屬性,默認是當前項目名(例如:/spring-boot-session),可重設(shè)
String path = getCookiePath(request);
if (path != null && path.length() > 0) {
validatePath(path);
sb.append("; Path=").append(path);
}
if (isSecureCookie(request)) {
sb.append("; Secure");
}
//設(shè)置在HttpOnly是否只讀屬性。
if (this.useHttpOnlyCookie) {
sb.append("; HttpOnly");
}
if (this.sameSite != null) {
sb.append("; SameSite=").append(this.sameSite);
}
//將設(shè)置好的cookie放入響應(yīng)頭中
response.addHeader("Set-Cookie", sb.toString());
}
分析到這兒整個session生成的過程,保存到session的過程,寫入到cookie的過程就分析完了。如果下次遇到session共享的問題我們處理起來也就得心應(yīng)手了。
例如:如果要實現(xiàn)同域名下不同項目的項目之間session共享,我們只需要改變Path屬性即可。
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1)
public class SessionConfig {
@Bean
public DefaultCookieSerializer defaultCookieSerializer() {
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setCookiePath("/");
return defaultCookieSerializer;
}
}
如果要指定域名的話,我們只需要設(shè)置DomainName屬性即可。其他的也是同理,在此就不在贅述了。
總結(jié)
本文按照代碼運行的順序,一步步分析了session的創(chuàng)建,保存到redis,將sessionid交由cookie托管的過程。分析完源碼之后,我們知道了session的創(chuàng)建和保存到redis主要是由RedisOperationsSessionRepository類來完成。將sessionid交由cookie托管主要是由DefaultCookieSerializer類來完成,下一篇我們將介紹讀取session的過程。
源代碼
https://github.com/XWxiaowei/spring-boot-session-demo
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥