SpringSession的源碼解析(生成session,保存session,寫入cookie全流程分析)

文章目錄

前言

上一篇文章主要介紹了如何使用SpringSession,其實(shí)SpringSession的使用并不是很難,無(wú)非就是引入依賴,加下配置。但是,這僅僅只是知其然,要知其所以然,我們還是需要深入源碼去理解。在看本文先我們先想想,下面這些問(wèn)題Session是啥時(shí)候創(chuàng)建的呢?通過(guò)什么來(lái)創(chuàng)建的呢?創(chuàng)建之后如何保存到Redis?又是如何把SessionId設(shè)置到Cookie中的呢?帶著這一系列的問(wèn)題,今天就讓我們來(lái)揭開(kāi)SpringSession的神秘面紗,如果讀者朋友們看完本文之后能夠輕松的回答上面的問(wèn)題,那本文的作用也就達(dá)到了。當(dāng)然,如果您已經(jīng)對(duì)這些知識(shí)了若指掌,那么就不需要看本文了。
看源碼的過(guò)程真的是一個(gè)很枯燥乏味的過(guò)程,但是弄清楚了其調(diào)用過(guò)程之后,又是很讓人興奮的,話不多說(shuō),直接進(jìn)入正題。

基礎(chǔ)介紹

默認(rèn)參數(shù)的設(shè)置

首先,我們從添加的SpringSession的配置類來(lái)看起,如下,是一段很基礎(chǔ)的配置代碼,就添加了@Configuration注解和@EnableRedisHttpSession注解。其中@Configuration注解標(biāo)注在類上,相當(dāng)于把該類作為spring的xml配置文件中的<beans>,作用為:配置spring容器(應(yīng)用上下文),@EnableRedisHttpSession注解的作用是使SpringSession生效。

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1)
public class SessionConfig {
}

點(diǎn)到EnableRedisHttpSession注解中,我們可以看到里面定義了RedisHttpSessionConfiguration的設(shè)置類,以及一些基礎(chǔ)參數(shù)的設(shè)置,例如:session默認(rèn)的失效時(shí)間,存入到redis的key的前綴名,這些我們參數(shù)我們?cè)谑褂米⒔鈺r(shí)都可以重新設(shè)置。例如:maxInactiveIntervalInSeconds設(shè)置為-1表示用不失效。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
			//默認(rèn)最大的失效時(shí)間是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類是一個(gè)設(shè)置類,內(nèi)部的作用主要是實(shí)例化RedisOperationsSessionRepository對(duì)象和RedisMessageListenerContainer對(duì)象等以及設(shè)置操作redis的工具類。

主要類的說(shuō)明
















操作session(生成session,保存session等過(guò)程)的時(shí)序圖

首先,我們先看一下生成Session的調(diào)用時(shí)序圖。
在這里插入圖片描述

1. 調(diào)用的入口還是SessionRepositoryFilter類(PS:Spring是通過(guò)責(zé)任鏈的模式來(lái)執(zhí)行每個(gè)過(guò)濾器的)的doFilterInternal方法。

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		//省略部分代碼
			SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
				request, response, this.servletContext);
		try {
		  //執(zhí)行其他過(guò)濾器
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
		//wrappedRequest是SessionRepositoryRequestWrapper類的一個(gè)實(shí)例
			wrappedRequest.commitSession();
		}
	}

2. SessionRepositoryRequestWrapper類的getSession(true)方法

經(jīng)過(guò)斷點(diǎn)調(diào)試,并查看調(diào)用棧,發(fā)現(xiàn)調(diào)用這個(gè)filterChain.doFilter(wrappedRequest, wrappedResponse);方法之后,最終會(huì)調(diào)用到SessionRepositoryRequestWrapper類的getSession(true)方法。其中,SessionRepositoryRequestWrapper類是SessionRepositoryFilter類的一個(gè)私有的不可被繼承,被重寫的內(nèi)部類。

public HttpSessionWrapper getSession(boolean create) {
			//1. 獲取HttpSessionWrapper實(shí)例,如果可以獲取到,則說(shuō)明session已經(jīng)生成了。就直接返回
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}
			//如果可以獲取到session
			S requestedSession = getRequestedSession();
			//如果HttpSessionWrapper實(shí)例為空,則需要將session對(duì)象封裝到HttpSessionWrapper實(shí)例中,并設(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,則進(jìn)入下面分支,創(chuàng)建session
			else {
			//省略部分代碼
			//如果create為false,直接返回null
			if (!create) {
				return null;
			}
		    //省略部分代碼
			//如果create為true,則調(diào)用RedisOperationsSessionRepository類的createSession方法創(chuàng)建session實(shí)例
			S session = SessionRepositoryFilter.this.sessionRepository.createSession();
			session.setLastAccessedTime(Instant.now());
			currentSession = new HttpSessionWrapper(session, getServletContext());
			setCurrentSession(currentSession);
			return currentSession;
		}

如上代碼所示:getSession(boolean create) 方法主要有兩塊,1. 獲取session實(shí)例,如果請(qǐng)求頭中帶著sessionid,則表示不是第一次請(qǐng)求,是可以獲取到session的。2. 如果瀏覽器是第一次請(qǐng)求應(yīng)用(沒(méi)有sessionid)則獲取不到session實(shí)例,需要?jiǎng)?chuàng)建session實(shí)例。在拿到生成的Session對(duì)象之后,緊接著會(huì)創(chuàng)建一個(gè)HttpSessionWrapper實(shí)例,并將前面生成的session傳入其中,方便后面取用,然后將HttpSessionWrapper實(shí)例放入當(dāng)前請(qǐng)求會(huì)話HttpServletRequest中,(Key是.CURRENT_SESSION,value是HttpSessionWrapper的實(shí)例)。

3. RedisOperationsSessionRepository類的createSession()方法

從前面的代碼分析我們可以知道如果獲取不到session實(shí)例,則會(huì)調(diào)用createSession()方法進(jìn)行創(chuàng)建。這個(gè)方法是在RedisOperationsSessionRepository類中,該方法比較簡(jiǎn)單,主要就是實(shí)例化RedisSession對(duì)象。其中RedisSession對(duì)象中包括了sessionid,creationTime,maxInactiveInterval和lastAccessedTime等屬性。其中原始的sessionid是一段唯一的UUID字符串。

	@Override
	public RedisSession createSession() {
		//實(shí)例化RedisSession對(duì)象
		RedisSession redisSession = new RedisSession();
		if (this.defaultMaxInactiveInterval != null) {
			//設(shè)置session的失效時(shí)間
			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代碼塊中會(huì)調(diào)用SessionRepositoryRequestWrapper類內(nèi)部的commitSession()方法,而commitSession()方法會(huì)保存session信息到Redis中,并將sessionid寫到cookie中。我們接著來(lái)看看commitSession()方法。

private void commitSession() {
			//當(dāng)前請(qǐng)求會(huì)話中獲取HttpSessionWrapper對(duì)象的實(shí)例
			HttpSessionWrapper wrappedSession = getCurrentSession();
			//如果wrappedSession為空則調(diào)用expireSession寫入一個(gè)空值的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);
				}
			}
		}

第一步就是從當(dāng)前請(qǐng)求會(huì)話中獲取HttpSessionWrapper對(duì)象的實(shí)例,如果實(shí)例獲取不到則向Cookie中寫入一個(gè)空值。如果可以獲取到實(shí)例的話,則從實(shí)例中獲取Session對(duì)象。獲取到Session對(duì)象之后則調(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等于請(qǐng)求頭中的sessionid,則直接返回
		if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
			return;
		}
		//將sessionid設(shè)置到請(qǐng)求頭中
		request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
		//將sessionid寫入cookie中
		this.cookieSerializer
				.writeCookieValue(new CookieValue(request, response, sessionId));
	}

從上代碼我們可以看出,setSessionId方法主要就是將生成的sessionid設(shè)置到請(qǐng)求會(huì)話中,然后調(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的名稱,默認(rèn)是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的失效時(shí)間
		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屬性,默認(rèn)就是當(dāng)前請(qǐng)求的域名,或者ip
		if (domain != null && domain.length() > 0) {
			validateDomain(domain);
			sb.append("; Domain=").append(domain);
		}
		//設(shè)置Path屬性,默認(rèn)是當(dāng)前項(xiàng)目名(例如:/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());
	}

分析到這兒整個(gè)session生成的過(guò)程,保存到session的過(guò)程,寫入到cookie的過(guò)程就分析完了。如果下次遇到session共享的問(wèn)題我們處理起來(lái)也就得心應(yīng)手了。
例如:如果要實(shí)現(xiàn)同域名下不同項(xiàng)目的項(xiàng)目之間session共享,我們只需要改變Path屬性即可。

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1)
public class SessionConfig {
    @Bean
    public DefaultCookieSerializer defaultCookieSerializer() {
        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setCookiePath("/");
        return defaultCookieSerializer;
    }
}

如果要指定域名的話,我們只需要設(shè)置DomainName屬性即可。其他的也是同理,在此就不在贅述了。

總結(jié)

本文按照代碼運(yùn)行的順序,一步步分析了session的創(chuàng)建,保存到redis,將sessionid交由cookie托管的過(guò)程。分析完源碼之后,我們知道了session的創(chuàng)建和保存到redis主要是由RedisOperationsSessionRepository類來(lái)完成。將sessionid交由cookie托管主要是由DefaultCookieSerializer類來(lái)完成,下一篇我們將介紹讀取session的過(guò)程。

源代碼

https://github.com/XWxiaowei/spring-boot-session-demo




作者:碼農(nóng)飛哥

微信公眾號(hào):碼農(nóng)飛哥