分布式集群: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配置类。

  • 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的创建获取主要涉及以下几个类与方法

类名方法名作用说明
RedisHttpSessionConfigurationsessionRepository()创建RedisIndexedSessionRepository对象
RedisIndexedSessionRepositorycreateSession(),save()SessionRepository的子类,调用Redis,保存/创建session
SpringHttpSessionConfigurationspringSessionRepositoryFilter()创建SessionRepositoryFilter对象
SessionRepositoryFilterdoFilterInternal()属于一个过滤器,优先级最高
SessionRepositoryRequestWrappercommitSession(),getSession()SessionRepositoryFilter的内部类,封装请求的Request。调用RedisIndexedSessionRepository的方法
SessionRepositoryResponseWrapperonResponseCommitted()SessionRepositoryFilter的内部类,封装请求的Response
CookieHttpSessionIdResolversetSessionId()调用DefaultCookieSerializer的writeCookieValue(),设置session
DefaultCookieSerializerwriteCookieValue()创建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方法的增强,也就是装饰器模式的使用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值