1.传统session与spring session的比较
在传统session模式中,客户端发送请求,通过cookie上存储的sessionid在服务器上找到对应的session,然后进行其他操作。
而在springsession模式中,session就不是存储在服务器上的,而是存储在一个独立的存储服务器上,所有请求获取session都是从这个独立的存储服务器上获取。
在单机情况下两种模式都没什么问题,但是在集群环境下传统session的问题就很明显了,传统session模式下每台服务器只能获取自身存储的session而不能获取其他服务器的session。springsession模式下session都存储在一个独立存储服务器,任何一台服务器都可以获取到所有的session。
2.spring session源码解析
1.spring session基本流程
spring session目前支持MySql、Redis、Mongodb等多种session存储形式,这里以Redis为例分析
@EnableRedisHttpSession注解导入了RedisHttpSessionConfiguration配置,RedisHttpSessionConfiguration会注入以下两个组件:
1.RedisHttpSessionConfiguration给容器中添加了一个组件SessionRepository,其实现类为 【RedisOperationsSessionRepository】,所有redis的操作都由该组件完成。如session的增删改查操作。
sessionRepository注入代码片段如下:
@Bean
public RedisOperationsSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
...
return sessionRepository;
}
2.RedisHttpSessionConfiguration还会给容器注入SessionRepositoryFilter ==》Filter: session存储过滤器;
SessionRepositoryFilter注入代码片段如下:
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
每个请求过来都必须经过SessionRepositoryFilter ,其主要功能如下: 1.创建的时候,就自动从容器中获取到了SessionRepositroy; 2.原始的request,response都被包装成SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper 3.以后获取session。request.getSession();就是SessionRepositoryRequestWrapper.getSession();
而SessionRepositoryRequestWrapper会从SessionRepository中获取到的session,就实现了从独立的session存储服务器上获取session
2.spring session源码分析
1.RedisOperationsSessionRepository
@Bean
public RedisOperationsSessionRepository sessionRepository() {
// 创建redisTemplate
RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
// 设置默认redis序列化
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
// 设置session默认的最大有效时间
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
// 设置命名空间,默认为spring:session,在redis中存储session的key以命名空间为前缀
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setRedisFlushMode(this.redisFlushMode);
int database = this.resolveDatabase();
sessionRepository.setDatabase(database);
return sessionRepository;
}
springsession中对redis操作都是通过该类完成
session在redis中的存储命名如下:
127.0.0.1:6379> keys *
1) "spring:session:sessions:00d50fcb-b832-476f-a762-c414823d7de1"
2) "spring:session:expirations:1687857240000"
3) "spring:session:sessions:expires:00d50fcb-b832-476f-a762-c414823d7de1"
2.SessionRepositoryFilter
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
RedisHttpSessionConfiguration通过其父类SpringHttpSessionConfiguration完成SessionRepositoryFilter的注入。
SessionRepositoryFilter拦截方法如下:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 为请求属性上设置sessionRepository
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
// 包装原始请求为SessionRepositoryRequestWrapper
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
// 包装原始响应为SessionRepositoryResponseWrapper
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
// 提交session
wrappedRequest.commitSession();
}
}
SessionRepositoryFilter完成对请求和响应的包装,使用了装饰者模式。
此后的request操作其实都是对SessionRepositoryRequestWrapper进行操作的。
finally中提交session是对session数据持久化到redis的操作。
3.SessionRepositoryRequestWrapper
接下来就看SessionRepositoryRequestWrapper到底把原始request封装成什么样了。
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
// 原始响应
private final HttpServletResponse response;
private final ServletContext servletContext;
// 该请求的redisSession
private S requestedSession;
private boolean requestedSessionCached;
// 该请求sessionId
private String requestedSessionId;
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
...
SessionRepositoryRequestWrapper继承了HttpServletRequestWrapper,HttpServletRequestWrapper遵循HttpServletRequest接口规范,可以发现SessionRepositoryRequestWrapper只是重写了有关session的方法,其他的方法还是使用原请求方法。
其重写的getSession(boolean)方法如下:
public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
// 从当前请求的attribute中获取session,如果有直接返回
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
if (currentSession != null) {
return currentSession;
} else {
// 根据请求cookie的sessionId获取存在redis中session
S requestedSession = this.getRequestedSession();
if (requestedSession != null) {
if (this.getAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {
// 设置最后访问时间
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
// 对redisSession进行封装以符合HttpSession接口规范(使用了适配器模式)
currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, this.getServletContext());
currentSession.setNew(false);
// 设置session至Requset的attribute中,提高同一个request访问session的性能
this.setCurrentSession(currentSession);
return currentSession;
}
} else {
this.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");
}
// 当前没有session时是否创建session
if (!create) {
return null;
} else {
// 创建新session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
// 设置最后访问时间
session.setLastAccessedTime(Instant.now());
// 对redisSession进行封装以符合HttpSession接口规范
currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, this.getServletContext());
// 设置session至Requset的attribute中,提高同一个request访问session的性能
this.setCurrentSession(currentSession);
return currentSession;
}
}
}
getsession()会获取到一个springsession自己封装的session,可以看下该session的结构
以上主要属性是cached和delta,cached用于存储session的全量值,获取值从cached中获取提高效率。delta则用于存储新增、删除、修改的值用于持久化到redis中。
4.SessionRepositoryResponseWrapper
private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {
private final SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;
SessionRepositoryResponseWrapper(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {
super(response);
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
} else {
this.request = request;
}
}
protected void onResponseCommitted() {
// 提交session(持久化)
this.request.commitSession();
}
}
SessionRepositoryResponseWrapper就重写了onResponseCommitted()这一个方法,作用是持久化session,确保在response提交前session被持久化到redis。我测试过这个持久化要先于SessionRepositoryFilter.doFilterInternal中finally里的持久化。
接下来看一下如何持久化到redis的
private void commitSession() {
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();
if (wrappedSession == null) {
if (this.isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
} else {
// 取出redisSession
S session = wrappedSession.getSession();
this.clearRequestedSessionCache();
// 使用sessionRepository持久化到redis
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
// 如果是新创建spring session,sessionId添加到response的cookie
if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {
// 该方法里面sessionId会进行base64编码,我开始还奇怪为啥cookie存的和redis存的sessionId不一样呢
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
RedisOperationsSessionRepository.save()
public void save(RedisOperationsSessionRepository.RedisSession session) {
// 将RedisSession中delta存储的变化的数据持久化到redis
session.saveDelta();
if (session.isNew()) {
String sessionCreatedKey = this.getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
5.HttpSessionWrapper
接下来看下HttpSessionWrapper,在我们操作session.setAttribute()和session.getAttribute()它是怎么实现的,这里需要结合上面的session的结构图看。
public void setAttribute(String name, Object value) {
this.checkState();
Object oldValue = this.session.getAttribute(name);
// 设置属性
this.session.setAttribute(name, value);
if (value != oldValue) {
if (oldValue instanceof HttpSessionBindingListener) {
try {
((HttpSessionBindingListener)oldValue).valueUnbound(new HttpSessionBindingEvent(this, name, oldValue));
} catch (Throwable var6) {
logger.error("Error invoking session binding event listener", var6);
}
}
if (value instanceof HttpSessionBindingListener) {
try {
((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this, name, value));
} catch (Throwable var5) {
logger.error("Error invoking session binding event listener", var5);
}
}
}
}
RedisSession.setAttribute
public void setAttribute(String attributeName, Object attributeValue) {
// 设置属性到cached中,以后get可以直接从cached获取,提高效率
this.cached.setAttribute(attributeName, attributeValue);
// 方法里面就是属性值设置到delta中,跟踪变化值
this.putAndFlush(RedisOperationsSessionRepository.getSessionAttrNameKey(attributeName), attributeValue);
}
springsession源码解析就到这里了,由于本人技术有限,如有不对之处还望指出,本人定虚心修正。