分布式集群:Spring Session源码分析

「尺有所短,寸有所长;不忘初心,方得始终」

项目中有用到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」配置类。

    分布式集群:Spring Session源码分析
  • 「RedisHttpSessionConfiguration」

    RedisHttpSessionConfiguration向Spring容器中注册了一些Bean,其中有一个RedisIndexedSessionRepository

    分布式集群:Spring Session源码分析

    主要是用来连接Redis,在方法的第一行需要依赖一个RedisTemplate,而RedisTemplate是通过createRedisTemplate()方法创建的,createRedisTemplate()也在这个类中

    分布式集群:Spring Session源码分析

    「RedisTemplate依赖一个redisConnectionFactory是需要我们自己配置的,在配置文件中加入spring.redis.cluster.nodes即可配置一个redis集群JedisConnectionFactory」

  • 「SpringHttpSessionConfiguration」

    RedisHttpSessionConfiguration中创建的RedisIndexedSessionRepository在哪里用??

    RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration,在SpringHttpSessionConfiguration中注册了一个「SessionRepositoryFilter」

    分布式集群:Spring Session源码分析

从代码中可以看出,在注册SessionRepositoryFilter是需要传入一个参数(sessionRepository),实际上这个参数就是在RedisHttpSessionConfiguration中注册的RedisIndexedSessionRepository对象。

RedisIndexedSessionRepositor是SessionRepository的子类

RedisIndexedSessionRepository  implements  FindByIndexNameSessionRepository  extends SessionRepository
分布式集群:Spring Session源码分析

到此处就引出了SessionRepositoryFilter,也就是本次的核心。

3.2 SessionRepositoryFilter

在开头提到SpringSession通过「Filter」 对请求进行拦截,重新封装 【Request】 和 【Response】,这个Filter其实就是「SessionRepositoryFilter过滤器」

「SessionRepositoryFilter主要作用是过滤所有的请求,接管创建和管理Session数据」

「SessionRepositoryFilter是一个优先级最高的 javax.servlet.Filter」

分布式集群:Spring Session源码分析

它有两个内部类

  • 「SessionRepositoryRequestWrapper」
  • 「SessionRepositoryResponseWrapper」

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

分布式集群:Spring Session源码分析
@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」

    分布式集群:Spring Session源码分析
    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」 有:

    分布式集群:Spring Session源码分析
    • 「CookieHttpSessionIdResolver(Cookie)」
    • 「HeaderHttpSessionIdResolver(请求头)」
  • 「getSession」

    分布式集群:Spring Session源码分析
@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能够被保存。

分布式集群:Spring Session源码分析

可以看到在onResponseCommitted方法中调了一下request.commitSession(),而这个方法在SessionRepositoryFilter.doFilterInternal已经调用过了,并且是在Finally中调用的,这里为什么又调用了一次?

这个问题在GitHub上有人提了相同的疑问,spring session 作者对此作出了回答

分布式集群:Spring Session源码分析
分布式集群:Spring Session源码分析

「原因是:我们需要确保在response提交响应之前创建session。如果response已经提交,则无法跟踪session(即无法将 cookie 写入response跟踪哪个Session ID)」

  • 「OnCommittedResponseWrapper」

    SessionRepositoryResponseWrapper继承父类OnCommittedResponseWrapper,在父类中重写了HttpServletResponse的flushBuffer方法

    分布式集群:Spring Session源码分析
    分布式集群:Spring Session源码分析

「由于flushBuffer()会强行把Buffer的内容写到客户端浏览器,所以要重写flushBuffer方法,在调用HttpServletResponse的flushBuffer方法之前,将session写入response和持久化session,避免无法追踪session」

不加flushBuffer的话,当程序运行到最后的右大括号的时候,Tomcat也会把Response的Buffer的东西,一次性发给客户 端。

3.5 CookieHttpSessionIdResolver

在SessionRepositoryRequestWrapper的commitSession方法中调用了setSessionId,expireSession,这两个方法的现实是一个「Strategy」 的方法。

CookieHttpSessionIdResolver是默认的策略实现

分布式集群:Spring Session源码分析
@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返回客户端。

分布式集群:Spring Session源码分析

四、总结

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的值
  • 「调用时序图」
分布式集群:Spring Session源码分析

4.2 源码阅读理解

对于源码的学习,除了代码实现本身之外,更重要的是学习在源码中的设计思想,在上述源码的分析中,涉及到了两个设计模式:责任链模式,装饰者模式

4.2.1责任链模式

在SessionRepositoryFilter的doFilterInternal方法,调用了一个过滤器链,过滤器链使用的「责任链模式」

分布式集群:Spring Session源码分析
  • 「责任链模式」

    「为请求创建了一个处理者对象的链。允许请求沿着处理者链进行发送, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。属于行为型模式」

    对于责任链模式的具体介绍有兴趣可以参考我之前的文章《设计模式(16):责任链模式》

4.2.2装饰者模式

「装饰器模式是继承关系的一种组合代替继承的替代方案。它允许向一个现有的对象添加新的功能,同时又不改变其结构」

对于装饰者模式的具体介绍有兴趣可以参考我之前的文章《设计模式(11):装饰模式》


在上述的源码中涉及到最重要的两个类就是SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。

在上述的源码中涉及到最重要的两个类就是SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。

SessionRepositoryRequestWrapper的继承关系图如下:

分布式集群:Spring Session源码分析

SessionRepositoryRequestWrapper继承HttpServletRequestWrapper,而HttpServletRequestWrapper是针对Servlet规范api,用于扩展HttpServletRequest提供扩展的扩张点。这里其实用到了「装饰器模式,通过重写session的相关api达到功能点的增强和自定义」

  • SessionRepositoryRequestWrapper继承HttpServletRequestWrapper 可以对父类的方法进行重写。

    分布式集群:Spring Session源码分析
  • HttpServletRequestWrapper继承了ServletRequestWrapper并实现HttpServletRequest接口,

    持有一个HttpServletRequest对象,实现了HttpServletRequest接口的所有方法,所有方法的实现中都是调用HttpServletRequest对象原本相应的方法。

    分布式集群:Spring Session源码分析

    可以看到SessionRepositoryRequestWrapper的构造方法,以及它的父类HttpServletRequestWrapper的构造方法都调用了:

    super(request)

    而在他们的父类ServletRequestWrapper中完成了HttpServletRequest初始化赋值

    分布式集群:Spring Session源码分析

    因此在SessionRepositoryRequestWrapper中只需要重写session相关的方法,其他方法都是调用的HttpServletRequest的方法,以此到达对session方法的增强,也就是「装饰器模式」的使用。


原文始发于微信公众号(星河之码):分布式集群:Spring Session源码分析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/26914.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!