SpringSession的源码解析(生成session,保存session,写入cookie全流程分析)
前言
上一篇我们介绍了SpringSession中Session的保存过程,今天我们接着来看看Session的读取过程。相对保存过程,读取过程相对比较简单。
本文想从源码的角度,详细介绍一下Session的读取过程。
读取过程的时序图
如上,是读取Session的时序图,首先代码入口还是SessionRepositoryFilter过滤器的doFilterInternal方法。这个方法里还是会调用到SessionRepositoryRequestWrapper类的getSession()
方法,这个getSession
方法是读取Session的开始,这个方法内部会调用getSession(true)
方法。那我们就从SessionRepositoryRequestWrapper类的getSession(true)
方法开始说起。
getSession(true)方法。
@Override
public HttpSessionWrapper getSession(boolean create) {
//获取HttpSessionWrapper类,这个类会包装HttpSession
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//获取RedisSession
S requestedSession = getRequestedSession();
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;
}
}
//省略部分代码
}
这个方法首先获取HttpSessionWrapper对象,这个对象的作用是用于封装session,返回给其上一层,如果可以获取到则说明Session信息已经拿到了,就直接返回。
如果获取不到则调用getRequestedSession()
方法。这个方法就是获取session的主方法。接着让我们来看看这个方法吧。
getRequestedSession()方法
private S getRequestedSession() {
if (!this.requestedSessionCached) {
//从cookie中获取sessionid集合
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
.resolveSessionIds(this);
//遍历sessionid集合,分别获取HttpSession
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
//根据sessionid去redis中获取session
S session = SessionRepositoryFilter.this.sessionRepository
.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
如上,这个方法主要有两步:
- 从cookie中获取sessionid的集合,可能cookie中存在多个sessionid。
- 循环sessionid的集合,分别根据sessionid到redis中获取session。
获取sessionid是通过HttpSessionIdResolver接口的resolveSessionIds
方法来实现的,SessionRepositoryFilter中定义了HttpSessionIdResolver接口的实例,其实现类是CookieHttpSessionIdResolver类。
private HttpSessionIdResolver httpSessionIdResolver = new CookieHttpSessionIdResolver();
所以,SessionRepositoryFilter.this.httpSessionIdResolver
的实例是一个CookieHttpSessionIdResolver
对象。
而SessionRepositoryFilter.this.sessionRepository
的实例是一个RedisOperationsSessionRepository
对象。
那么接下来我们就分别来看看这个两个类的相关方法。
resolveSessionIds方法
接下来,我们就来到了CookieHttpSessionIdResolver
类的resolveSessionIds
方法,这个方法主要的作用就是从cookie中获取sessionid。
@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
return this.cookieSerializer.readCookieValues(request);
}
看到这个方法之后,我们发现这个方法只是一个中转方法,内部直接把请求交给了readCookieValues
方法。同样的在CookieHttpSessionIdResolver类内部也定义了cookieSerializer这个属性,
它的实例对象是DefaultCookieSerializer。所以,真正的操作逻辑还是在DefaultCookieSerializer类中完成的。
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();
接下来,我们就来看看DefaultCookieSerializer这个类的的readCookieValues
方法。
readCookieValues方法
@Override
public List<String> readCookieValues(HttpServletRequest request) {
//从请求头中获取cookies
Cookie[] cookies = request.getCookies();
List<String> matchingCookieValues = new ArrayList<>();
if (cookies != null) {
for (Cookie cookie : cookies) {
//获取存放sessionid的那个cookie,cookieName默认是SESSION
if (this.cookieName.equals(cookie.getName())) {
//默认的话sessionid是加密的
String sessionId = (this.useBase64Encoding
? base64Decode(cookie.getValue())
: cookie.getValue());
if (sessionId == null) {
continue;
}
if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
sessionId = sessionId.substring(0,
sessionId.length() - this.jvmRoute.length());
}
matchingCookieValues.add(sessionId);
}
}
}
return matchingCookieValues;
}
如上,这个从cookie中获取sessionid的方法也很简单,无非就是从当前的HttpServletRequest对象中获取所有的cookie,然后,提取name等于cookieName的cookie值。
这个cookie值就是sessionid。
findById方法
从cookie中那个sessionid之后会调用RedisOperationsSessionRepository类的findById
方法,这个方法的作用就是从redis中获取保存的session信息。
public RedisSession findById(String id) {
//直接调用getSession方法
return getSession(id, false);
}
private RedisSession getSession(String id, boolean allowExpired) {
//获取当前session在redis保存的所有数据
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
//传入数据并组装成MapSession
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
//将MapSession在转成RedisSession,并最终返回
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
如上,我们可以看到findById
方法内部直接调用了getSession
方法,所以,所有的逻辑都在这个方法,而这个方法的逻辑分为三步:
- 根据sessionid获取当前session在redis保存的所有数据
- 传入数据并组装成MapSession
- 将MapSession在转成RedisSession,并最终返回
我们一步步的看
首先,第一步根据sessionid获取当前session在redis保存的所有数据
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(
String sessionId) {
//拿到key
String key = getSessionKey(sessionId);
//根据key获取值
return this.sessionRedisOperations.boundHashOps(key);
}
//key是spring:session sessions:+sessionid
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
需要注意的是,session保存到redis中的值不是字符类型的。而是通过对象保存的,是hash类型。
总结
至此,从Cookie中读取SessionId,然后,根据SessionId查询保存到Redis中的数据的全过程,希望对大家有所帮助。
作者:码农飞哥
微信公众号:码农飞哥