前言
上一篇文章主要介绍了如何使用SpringSession,其实SpringSession的使用并不是很难,无非就是引入依赖,加下配置。但是,这仅仅只是知其然,要知其所以然,我们还是需要深入源码去理解。在看本文先我们先想想,下面这些问题Session是啥时候创建的呢?通过什么来创建的呢?创建之后如何保存到Redis?又是如何把SessionId设置到Cookie中的呢?带着这一系列的问题,今天就让我们来揭开SpringSession的神秘面纱,如果读者朋友们看完本文之后能够轻松的回答上面的问题,那本文的作用也就达到了。当然,如果您已经对这些知识了若指掌,那么就不需要看本文了。
看源码的过程真的是一个很枯燥乏味的过程,但是弄清楚了其调用过程之后,又是很让人兴奋的,话不多说,直接进入正题。
基础介绍
默认参数的设置
首先,我们从添加的SpringSession的配置类来看起,如下,是一段很基础的配置代码,就添加了@Configuration注解和@EnableRedisHttpSession注解。其中@Configuration注解标注在类上,相当于把该类作为spring的xml配置文件中的<beans>,作用为:配置spring容器(应用上下文),@EnableRedisHttpSession注解的作用是使SpringSession生效。
@Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1) public class SessionConfig {
点到EnableRedisHttpSession注解中,我们可以看到里面定义了RedisHttpSessionConfiguration的设置类,以及一些基础参数的设置,例如:session默认的失效时间,存入到redis的key的前缀名,这些我们参数我们在使用注解时都可以重新设置。例如:maxInactiveIntervalInSeconds设置为-1表示用不失效。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(RedisHttpSessionConfiguration.class) @Configuration public @interface EnableRedisHttpSession { //默认最大的失效时间是30分钟 int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS(1800秒); public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800; //存入到redis的key的前缀名 public static final String DEFAULT_NAMESPACE = "spring:session"; String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE; }
RedisHttpSessionConfiguration类是一个设置类,内部的作用主要是实例化RedisOperationsSessionRepository对象和RedisMessageListenerContainer对象等以及设置操作redis的工具类。
主要类的说明
类名 | 作用 |
RedisHttpSessionConfiguration | 定义RedisOperationsSessionRepository等类的对象 |
SessionRepositoryFilter | 过滤器,操作session的入口类 |
SessionRepositoryRequestWrapper | 是SessionRepositoryFilter内部类,包装HttpRequest请求,调用RedisOperationsSessionRepository类相关的方法都是通过其完成 |
CookieHttpSessionIdResolver | 这个类主要是调用DefaultCookieSerializer类的方法将sessionid存入cookie中,或者从cookie中读取sessionid,并返回给他的上一层 |
DefaultCookieSerializer | 这个类是真正的操作cookie的类,设置cookie的相关属性,只需要重新实例化这个类即可 |
RedisOperationsSessionRepository | 这个类的作用是生成session,并将session保存到redis中,另外就是根据sessionid查找session |
RedisSession | 这个类就是Spring Session的真正的实例对象,这是原始的session |
操作session(生成session,保存session等过程)的时序图
首先,我们先看一下生成Session的调用时序图。
1. 调用的入口还是SessionRepositoryFilter类(PS:Spring是通过责任链的模式来执行每个过滤器的)的doFilterInternal方法。
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //省略部分代码 SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); try { //执行其他过滤器 filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { //wrappedRequest是SessionRepositoryRequestWrapper类的一个实例 wrappedRequest.commitSession(); } }
2. SessionRepositoryRequestWrapper类的getSession(true)方法
经过断点调试,并查看调用栈,发现调用这个filterChain.doFilter(wrappedRequest, wrappedResponse);方法之后,最终会调用到SessionRepositoryRequestWrapper类的getSession(true)方法。其中,SessionRepositoryRequestWrapper类是SessionRepositoryFilter类的一个私有的不可被继承,被重写的内部类。
public HttpSessionWrapper getSession(boolean create) { //1. 获取HttpSessionWrapper实例,如果可以获取到,则说明session已经生成了。就直接返回 HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } //如果可以获取到session S requestedSession = getRequestedSession(); //如果HttpSessionWrapper实例为空,则需要将session对象封装到HttpSessionWrapper实例中,并设置到HttpRequestSerlvet中 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; } } //如果获取不到session,则进入下面分支,创建session else { //省略部分代码 //如果create为false,直接返回null if (!create) { return null; } //省略部分代码 //如果create为true,则调用RedisOperationsSessionRepository类的createSession方法创建session实例 S session = SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(Instant.now()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession; }
如上代码所示:getSession(boolean create) 方法主要有两块,1. 获取session实例,如果请求头中带着sessionid,则表示不是第一次请求,是可以获取到session的。2. 如果浏览器是第一次请求应用(没有sessionid)则获取不到session实例,需要创建session实例。在拿到生成的Session对象之后,紧接着会创建一个HttpSessionWrapper实例,并将前面生成的session传入其中,方便后面取用,然后将HttpSessionWrapper实例放入当前请求会话HttpServletRequest中,(Key是.CURRENT_SESSION,value是HttpSessionWrapper的实例)。
3. RedisOperationsSessionRepository类的createSession()方法
从前面的代码分析我们可以知道如果获取不到session实例,则会调用createSession()方法进行创建。这个方法是在RedisOperationsSessionRepository类中,该方法比较简单,主要就是实例化RedisSession对象。其中RedisSession对象中包括了sessionid,creationTime,maxInactiveInterval和lastAccessedTime等属性。其中原始的sessionid是一段唯一的UUID字符串。
@Override public RedisSession createSession() { //实例化RedisSession对象 RedisSession redisSession = new RedisSession(); if (this.defaultMaxInactiveInterval != null) { //设置session的失效时间 redisSession.setMaxInactiveInterval( Duration.ofSeconds(this.defaultMaxInactiveInterval)); } return redisSession; } RedisSession() { this(new MapSession()); this.delta.put(CREATION_TIME_KEY, getCreationTime().toEpochMilli()); this.delta.put(MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds()); this.delta.put(LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli()); this.isNew = true; this.flushImmediateIfNecessary(); }
另外,doFilterInternal方法在调用完其他方法之后,在finally代码块中会调用SessionRepositoryRequestWrapper类内部的commitSession()方法,而commitSession()方法会保存session信息到Redis中,并将sessionid写到cookie中。我们接着来看看commitSession()方法。
private void commitSession() { //当前请求会话中获取HttpSessionWrapper对象的实例 HttpSessionWrapper wrappedSession = getCurrentSession(); //如果wrappedSession为空则调用expireSession写入一个空值的cookie if (wrappedSession == null) { if (isInvalidateClientSession()) { SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response); } } else { //获取session S session = wrappedSession.getSession(); clearRequestedSessionCache(); SessionRepositoryFilter.this.sessionRepository.save(session); String sessionId = session.getId(); if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) { SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId); } } }
第一步就是从当前请求会话中获取HttpSessionWrapper对象的实例,如果实例获取不到则向Cookie中写入一个空值。如果可以获取到实例的话,则从实例中获取Session对象。获取到Session对象之后则调用RedisOperationsSessionRepository类的save(session)方法将session信息保存到Redis中,其中redis的名称前缀是spring:session。将数据保存到Redis之后
紧接着获取sessionid,最后调用CookieHttpSessionIdResolver类的setSessionId方法将sessionid设置到Cookie中。
4. CookieHttpSessionIdResolver类的setSessionId方法
@Override public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) { //如果sessionid等于请求头中的sessionid,则直接返回 if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) { return; } //将sessionid设置到请求头中 request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId); //将sessionid写入cookie中 this.cookieSerializer .writeCookieValue(new CookieValue(request, response, sessionId)); }
从上代码我们可以看出,setSessionId方法主要就是将生成的sessionid设置到请求会话中,然后调用DefaultCookieSerializer类的writeCookieValue方法将sessionid设置到cookie中。
5. DefaultCookieSerializer类的writeCookieValue方法
@Override public void writeCookieValue(CookieValue cookieValue) { HttpServletRequest request = cookieValue.getRequest(); HttpServletResponse response = cookieValue.getResponse(); StringBuilder sb = new StringBuilder(); //设置cookie的名称,默认是SESSION sb.append(this.cookieName).append('='); //设置cookie的值,就是传入的sessionid String value = getValue(cookieValue); if (value != null && value.length() > 0) { validateValue(value); sb.append(value); } //设置cookie的失效时间 int maxAge = getMaxAge(cookieValue); if (maxAge > -1) { sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge()); OffsetDateTime expires = (maxAge != 0) ? OffsetDateTime.now().plusSeconds(maxAge) : Instant.EPOCH.atOffset(ZoneOffset.UTC); sb.append("; Expires=") .append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); } String domain = getDomainName(request); //设置Domain属性,默认就是当前请求的域名,或者ip if (domain != null && domain.length() > 0) { validateDomain(domain); sb.append("; Domain=").append(domain); } //设置Path属性,默认是当前项目名(例如:/spring-boot-session),可重设 String path = getCookiePath(request); if (path != null && path.length() > 0) { validatePath(path); sb.append("; Path=").append(path); } if (isSecureCookie(request)) { sb.append("; Secure"); } //设置在HttpOnly是否只读属性。 if (this.useHttpOnlyCookie) { sb.append("; HttpOnly"); } if (this.sameSite != null) { sb.append("; SameSite=").append(this.sameSite); } //将设置好的cookie放入响应头中 response.addHeader("Set-Cookie", sb.toString()); }
分析到这儿整个session生成的过程,保存到session的过程,写入到cookie的过程就分析完了。如果下次遇到session共享的问题我们处理起来也就得心应手了。
例如:如果要实现同域名下不同项目的项目之间session共享,我们只需要改变Path属性即可。
@Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1) public class SessionConfig { @Bean public DefaultCookieSerializer defaultCookieSerializer() { DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer(); defaultCookieSerializer.setCookiePath("/"); return defaultCookieSerializer; } }
如果要指定域名的话,我们只需要设置DomainName属性即可。其他的也是同理,在此就不在赘述了。
总结
本文按照代码运行的顺序,一步步分析了session的创建,保存到redis,将sessionid交由cookie托管的过程。分析完源码之后,我们知道了session的创建和保存到redis主要是由RedisOperationsSessionRepository类来完成。将sessionid交由cookie托管主要是由DefaultCookieSerializer类来完成,下一篇我们将介绍读取session的过程。