「尺有所短,寸有所长;不忘初心,方得始终」。
项目中有用到Spring Session作为分布式集群中Session的共享机制,它的原理很简单,创建session之后Spring Session会自动将其存在Redis中,但是对其底层源码具体的实现却不是很清楚,所以简单的跟了一下源码,了解了一下具体的实现。
一、SpringSession的作用
-
Spring Session 是 Spring 的项目之一。Spring Session 提供了「一套创建和管理 Servlet HttpSession 的方案」,默认采用外置的Redis来存储Session数据,以此来解决 Session 共享的问题。
-
SpringSession通过「Filter」 对请求进行拦截,重新封装 【Request】 和 【Response】 ,这样客户端在调用的【request.getSession()】方法时,获取到的Session就是重新封装过的,Redis存储Session实现分布式共享,也是基于对此方法的重写封装。
二、集成Spring Session
-
引入maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency> -
配置Redis
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password= -
配置类
使用@EnableRedisHttpSession启用RedisHttpSession功能,同时将SessionConfig注册到到容器中
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
}@EnableRedisHttpSession注解也可以直接加在启动类上。
三、Spring Session原理
3.1 @EnableRedisHttpSession
-
@EnableRedisHttpSession用来启用RedisHttpSession功能,实现分布式session。在@EnableRedisHttpSession注解中通过Import引入了「RedisHttpSessionConfiguration」配置类。
-
「RedisHttpSessionConfiguration」
RedisHttpSessionConfiguration向Spring容器中注册了一些Bean,其中有一个RedisIndexedSessionRepository
主要是用来连接Redis,在方法的第一行需要依赖一个RedisTemplate,而RedisTemplate是通过createRedisTemplate()方法创建的,createRedisTemplate()也在这个类中
「RedisTemplate依赖一个redisConnectionFactory是需要我们自己配置的,在配置文件中加入spring.redis.cluster.nodes即可配置一个redis集群JedisConnectionFactory」。
-
「SpringHttpSessionConfiguration」
RedisHttpSessionConfiguration中创建的RedisIndexedSessionRepository在哪里用??
RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration,在SpringHttpSessionConfiguration中注册了一个「SessionRepositoryFilter」。
从代码中可以看出,在注册SessionRepositoryFilter是需要传入一个参数(sessionRepository),实际上这个参数就是在RedisHttpSessionConfiguration中注册的RedisIndexedSessionRepository对象。
RedisIndexedSessionRepositor是SessionRepository的子类
RedisIndexedSessionRepository implements FindByIndexNameSessionRepository extends SessionRepository

到此处就引出了SessionRepositoryFilter,也就是本次的核心。
3.2 SessionRepositoryFilter
在开头提到SpringSession通过「Filter」 对请求进行拦截,重新封装 【Request】 和 【Response】,这个Filter其实就是「SessionRepositoryFilter过滤器」。
「SessionRepositoryFilter主要作用是过滤所有的请求,接管创建和管理Session数据」。
「SessionRepositoryFilter是一个优先级最高的 javax.servlet.Filter」

它有两个内部类
-
「SessionRepositoryRequestWrapper」 -
「SessionRepositoryResponseWrapper」
每当有请求进入时,过滤器会执行doFilterInternal方法将Request 和Response 这两个对象转换成包装类SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper对象。

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
/** 包装 request */
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
/** 包装 response */
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
/** 执行后续过滤器链 */
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
/** 后续过滤器链执行完毕,提交 session,用于存储 session 信息并返回set-cookie信息 */
wrappedRequest.commitSession();
}
}
3.3 SessionRepositoryRequestWrapper
这个类的方法很多,很多都是跟 「Session」有关系的,比如「getSession,commitSession」
-
「commitSession」
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
// 如果wrappedSession为null,并且已经被标记为失效时,调用 expireSession 进行通知处理
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
}else {// 如果wrappedSession不为null 就更新属性值
S session = wrappedSession.getSession();
clearRequestedSessionCache();
// 将session保存在Redis中
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
// 如果请求的sessionId跟当前的session的id不同,或者请求的sessionId无效,则重新setSessionId
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}setSessionId,expireSession都是 「Strategy」 的方法,根据不一样的策略采取不一样的处理。 「Strategy」 有:
-
「CookieHttpSessionIdResolver(Cookie)」 -
「HeaderHttpSessionIdResolver(请求头)」 -
「getSession」
@Override
public HttpSessionWrapper getSession(boolean create) {
// HttpSessionWrapper 是否存在 request 的 attribute 中
HttpSessionWrapper currentSession = getCurrentSession();
// 存在即返回
if (currentSession != null) {
return currentSession;
}
// 获取请求的 sessionId, Cookie策略的话从cookie里拿, header策略的话在 Http Head 中获取
String requestedSessionId = getRequestedSessionId();
// 如果获取到,并且没有‘sessionId失效’标识
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
// 这里是从 repository 中读取,如 RedisRepository
S session = getSession(requestedSessionId);
// 读取到了就恢复出session
if (session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
// 没有读取到(过期了), 设置‘失效’标识, 下次不用再去 repository 中读取
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
// true 创建一个 false 不创建
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)"));
}
// 都没有那么就创建一个新的
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
3.4 SessionRepositoryResponseWrapper
response是服务端对客户端请求的一个响应,其中封装了响应头、状态码、内容(最终要在浏览器上显示的HTML代码或者其他数据格式)等。
「服务端在把response提交到客户端之前,会使用一个缓冲区,并向该缓冲区内写入响应头和状态码,然后将所有内容flush(flush包含两个步骤:先将缓冲区内容发送至客户端,然后将缓冲区清空)」。
这就标志着该次响应已经committed(提交)。
继承自 「OnCommittedResponseWrapper」 主要作用就一个,当 Response 输出完毕后调用 commit,确保如果响应被提交session能够被保存。

可以看到在onResponseCommitted方法中调了一下request.commitSession(),而这个方法在SessionRepositoryFilter.doFilterInternal已经调用过了,并且是在Finally中调用的,这里为什么又调用了一次?
这个问题在GitHub上有人提了相同的疑问,spring session 作者对此作出了回答


「原因是:我们需要确保在response提交响应之前创建session。如果response已经提交,则无法跟踪session(即无法将 cookie 写入response跟踪哪个Session ID)」。
-
「OnCommittedResponseWrapper」
SessionRepositoryResponseWrapper继承父类OnCommittedResponseWrapper,在父类中重写了HttpServletResponse的flushBuffer方法
「由于flushBuffer()会强行把Buffer的内容写到客户端浏览器,所以要重写flushBuffer方法,在调用HttpServletResponse的flushBuffer方法之前,将session写入response和持久化session,避免无法追踪session」。
不加flushBuffer的话,当程序运行到最后的右大括号的时候,Tomcat也会把Response的Buffer的东西,一次性发给客户 端。
3.5 CookieHttpSessionIdResolver
在SessionRepositoryRequestWrapper的commitSession方法中调用了setSessionId,expireSession,这两个方法的现实是一个「Strategy」 的方法。
CookieHttpSessionIdResolver是默认的策略实现

@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));
}
从代码上可以看出,setSessionId方法调用writeCookieValue方法,创建了一个Cookie,然后将SessionId放在Cookie中,再将Cookie放在response里面返回给客户端。
-
DefaultCookieSerializer.writeCookieValue
@Override
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
StringBuilder sb = new StringBuilder();
sb.append(this.cookieName).append('=');
String value = getValue(cookieValue);
if (value != null && value.length() > 0) {
validateValue(value);
sb.append(value);
}
int maxAge = getMaxAge(cookieValue);
if (maxAge > -1) {
sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge)
: Instant.EPOCH.atZone(ZoneOffset.UTC);
sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
String domain = getDomainName(request);
if (domain != null && domain.length() > 0) {
validateDomain(domain);
sb.append("; Domain=").append(domain);
}
String path = getCookiePath(request);
if (path != null && path.length() > 0) {
validatePath(path);
sb.append("; Path=").append(path);
}
if (isSecureCookie(request)) {
sb.append("; Secure");
}
if (this.useHttpOnlyCookie) {
sb.append("; HttpOnly");
}
if (this.sameSite != null) {
sb.append("; SameSite=").append(this.sameSite);
}
response.addHeader("Set-Cookie", sb.toString());
}
3.6 HeaderHttpSessionIdResolver
顾名思义,HeaderHttpSessionIdResolver是将SessionId写在请求头中,通过response返回客户端。

四、总结
4.1 Spring Session原理分析总结
通过上述的分析,Spring Session的创建获取主要涉及以下几个类与方法
类名 | 方法名 | 作用说明 |
---|---|---|
RedisHttpSessionConfiguration | sessionRepository() | 创建RedisIndexedSessionRepository对象 |
RedisIndexedSessionRepository | createSession(),save() | SessionRepository的子类,调用Redis,保存/创建session |
SpringHttpSessionConfiguration | springSessionRepositoryFilter() | 创建SessionRepositoryFilter对象 |
SessionRepositoryFilter | doFilterInternal() | 属于一个过滤器,优先级最高 |
SessionRepositoryRequestWrapper | commitSession(),getSession() | SessionRepositoryFilter的内部类,封装请求的Request。调用RedisIndexedSessionRepository的方法 |
SessionRepositoryResponseWrapper | onResponseCommitted() | SessionRepositoryFilter的内部类,封装请求的Response |
CookieHttpSessionIdResolver | setSessionId() | 调用DefaultCookieSerializer的writeCookieValue(),设置session |
DefaultCookieSerializer | writeCookieValue() | 创建cookies,设置Cookies的值 |
-
「调用时序图」

4.2 源码阅读理解
对于源码的学习,除了代码实现本身之外,更重要的是学习在源码中的设计思想,在上述源码的分析中,涉及到了两个设计模式:责任链模式,装饰者模式
4.2.1责任链模式
在SessionRepositoryFilter的doFilterInternal方法,调用了一个过滤器链,过滤器链使用的「责任链模式」

-
「责任链模式」
「为请求创建了一个处理者对象的链。允许请求沿着处理者链进行发送, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。属于行为型模式」。
对于责任链模式的具体介绍有兴趣可以参考我之前的文章《设计模式(16):责任链模式》
4.2.2装饰者模式
「装饰器模式是继承关系的一种组合代替继承的替代方案。它允许向一个现有的对象添加新的功能,同时又不改变其结构」。
对于装饰者模式的具体介绍有兴趣可以参考我之前的文章《设计模式(11):装饰模式》
在上述的源码中涉及到最重要的两个类就是SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。
在上述的源码中涉及到最重要的两个类就是SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。
SessionRepositoryRequestWrapper的继承关系图如下:

SessionRepositoryRequestWrapper继承HttpServletRequestWrapper,而HttpServletRequestWrapper是针对Servlet规范api,用于扩展HttpServletRequest提供扩展的扩张点。这里其实用到了「装饰器模式,通过重写session的相关api达到功能点的增强和自定义」。
-
SessionRepositoryRequestWrapper继承HttpServletRequestWrapper 可以对父类的方法进行重写。
-
HttpServletRequestWrapper继承了ServletRequestWrapper并实现HttpServletRequest接口,
持有一个HttpServletRequest对象,实现了HttpServletRequest接口的所有方法,所有方法的实现中都是调用HttpServletRequest对象原本相应的方法。
可以看到SessionRepositoryRequestWrapper的构造方法,以及它的父类HttpServletRequestWrapper的构造方法都调用了:
super(request)
而在他们的父类ServletRequestWrapper中完成了HttpServletRequest初始化赋值
因此在SessionRepositoryRequestWrapper中只需要重写session相关的方法,其他方法都是调用的HttpServletRequest的方法,以此到达对session方法的增强,也就是「装饰器模式」的使用。
原文始发于微信公众号(星河之码):分布式集群:Spring Session源码分析
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/26914.html