基于 spring-session 解决分布式 session 共享问题

摘要:本文主要研究 基于 spring-seesion 解决分布式 session 的共享问题。首先讲述 session 共享问题的产生背景以及常见的解决方案;而后讲解本文主要研究的 spring-session 的概念和功能;接着演示了 spring-session 的两种管理 sessionid 的实现方案,属于实战内容,需重点掌握;再接着对后台保存数据到 redis 上的数据结构进行了分析;而后对 spring-session 的核心源代码进行了解读,方便理解 spring-session 框架的实现原理;最后列举了在使用 spring-session 的实践过程当中可能遇到的问题或坑,重点去理解一下。html

直奔主题

HttpSession 是经过 Servlet 容器建立和管理的,像 Tomcat/Jetty 都是保存在内存中的。而若是咱们把 web 服务器搭建成分布式的集群,而后利用 LVS 或 Nginx 作负载均衡,那么来自同一用户的 Http 请求将有可能被分发到两个不一样的 web 站点中去。那么问题就来了,如何保证不一样的 web 站点可以共享同一份 session 数据呢?html5

最简单的想法将 session 管理从容器中独立出来。而实现方案有不少种,下面简单介绍下:java

  • 第一种是使用容器扩展来实现,你们比较容易接受的是经过容器插件来实现,好比基于 Tomcat 的 tomcat-redis-session-manager ,基于 Jetty 的 jetty-session-redis 等等。好处是对项目来讲是透明的,无需改动代码。不过前者目前还不支持 Tomcat 8 ,或者说不太完善。可是因为过于依赖容器,一旦容器升级或者更换意味着又得重新来过。而且代码不在项目中,对开发者来讲维护也是个问题。
  • 第二种是本身写一套会话管理的工具类,包括 Session 管理和 Cookie 管理,在须要使用会话的时候都从本身的工具类中获取,而工具类后端存储能够放到 Redis 中。很显然这个方案灵活性最大,但开发须要一些额外的时间。而且系统中存在两套 Session 方案,很容易弄错而致使取不到数据。
  • 第三种是使用框架的会话管理工具,也就是以下介绍的 spring-session ,能够理解是替换了 Servlet 那一套会话管理,接管建立和管理 Session 数据的工做。既不依赖容器,又不须要改动代码,而且是用了 spring-data-redis 那一套链接池,能够说是最完美的解决方案。

解决方案之 spring-session

介绍

Spring Session 是 Spring 的项目之一,GitHub地址:https://github.com/spring-pro...git

Spring Session 提供了一套建立和管理 Servlet HttpSession 的完美方案。github

功能

spring Session 提供了 API 和实现,用于管理用户的 Session 信息。除此以外,它还提供了以下特性:web

  • 将 session 所保存的状态卸载到特定的外部 session 存储汇总,如 Redis 中,他们可以以独立于应用服务器的方式提供高质量的集群。
  • 控制 sessionid 如何在客户端和服务器之间进行交换,这样的话就能很容易地编写 Restful API ,由于它能够从 HTTP 头信息中获取 sessionid ,而没必要再依赖于 cookie。
  • 在非 Web 请求的处理代码中,可以访问 session 数据,好比在 JMS 消息的处理代码中。
  • 支持每一个浏览器上使用多个 session,从而可以很容易地构建更加丰富的终端用户体验。
  • 当用户使用 WebSocket 发送请求的时候,可以保持 HttpSession 处于活跃状态。

方案一由 cookie 管理 sessionid

在 maven 中添加以下依赖

<!-- spring-session-data-redis 是一个空的包,仅仅只有一个 META-INF 文件夹。它的做用就在于引入以下四个          
包 spring-data-redis,jedis,spring-session,commons-pool2
-->
  <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
      <version>1.0.1.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.6.1.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.5.2</version>
  </dependency> 
  <dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
    <version>1.1.0.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.2</version>
  </dependency>

在 spring 配置文件中添加以下配置

<!-- redis 的 bean 配置以下 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"/>

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="127.0.0.1" />
    <property name="port" value="6379" />
    <property name="password" value="" />
    <property name="timeout" value="3600" />
    <property name="poolConfig" ref="jedisPoolConfig" />
    <property name="usePool" value="true" />
    <property name="database" value="0"/> <!-- 默认存放在0号库中 -->
</bean>

<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>

<!-- 将 session 放入 redis, spring-session 会使用此 bean -->
<bean id="redisHttpSessionConfiguration"                     class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">                                                                    
    <property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>

​这里前面几个 bean 都是操做 redis 时候使用的,最后一个 bean 才是 spring-session 须要用到的,其中的 id 能够不写或者保持不变,这也是一个约定优先配置的体现。这个 bean 中又会自动产生多个 bean ,用于相关操做,极大的简化了咱们的配置项。其中有个比较重要的是 springSessionRepositoryFilter ,它将在下面的代理 filter 中被调用到。maxInactiveIntervalInSeconds 表示超时时间,默认是 1800 秒。上述配置能够采用 xml 来定义,官方文档中有采用注解来声明一个配置类。redis

在 web.xml 中配置过滤器

  接下来在 web.xml 中添加一个 session 代理 filter ,经过这个 filter 来包装 Servlet 的 getSession() 。须要注意的是这个 filter 须要放在全部 filter 链最前面,从而保证彻底替换掉 tomcat 的 session。这个是约定。算法

<!-- delegatingFilterProxy -->
<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

验证

第一步:编写 Controller 代码spring

@RequestMapping(value = "user", method = RequestMethod.POST)
public void setUser(HttpSession session) {
    User user = new User();
    user.setName("lyf");
    user.setPassword("123");
    session.setAttribute("user", user);
}
@RequestMapping(value = "user", method = RequestMethod.GET)
public String getUser(HttpSession session) {
    User user = (User) session.getAttribute("user");
    String name = user.getName();
    return "用户名称:" + name;
}

第二步:浏览器中访问 Controller数据库

  • post请求:localhost:8080/training/user

响应头部以下:Response Headers:

Set-Cookie:SESSION=a2c10601-3204-454e-b545-85e84f587045; Path=/training/; HttpOnly
...

会发现浏览器 Cookie 中的 jsessionid 已经替换为 session**

此时使用 redis-cli 到 redis 库中查询以下:

springsession:0>keys *
1) spring:session:sessions:a2c10601-3204-454e-b545-85e84f587045
2) spring:session:expirations:1502595600000
  • get请求:localhost:8080/training/user

请求头部以下:Request Headers:

Cookie:SESSION=a2c10601-3204-454e-b545-85e84f587045;

服务器经过 Cookie 中的 session 识别码从 redis 库中找到了须要的 session 对象并返回,浏览器显示以下:

用户名称:lyf
  • 总结

    经过如上 spring-session 配置便可将其集成到项目中,以后使用的全部有关 session 的操做,都会由 spring-session 来接管建立和信息存取。官方默认 spring-session 中的 session 信息都保存在 redis 数据库中。

    此实现方式弊端:若是浏览器禁用掉了 cookie 或者是非 web 请求时根本没有 cookie 的时候,那么如上经过cookie 管理 sessionid 的实现方式将不可以实现 session 共享。

方案二由 httpheader 管理 sessionid

在 maven 中添加以下依赖

同3.1

在 spring 配置文件中添加以下配置

<!-- redis 的 bean 配置以下 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"/>

<!-- 替代默认使用 cookie ,这里使用的是 httpheader -->
<bean id="httpSessonStrategy"          class="org.springframework.session.web.http.HeaderHttpSessionStrategy"/>

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
      <property name="hostName" value="127.0.0.1" />
      <property name="port" value="6379" />
      <property name="password" value="" />
      <property name="timeout" value="3600" />
      <property name="poolConfig" ref="jedisPoolConfig" />
      <property name="usePool" value="true" />
</bean>

<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
       <property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>

<!-- 将 session 放入 redis -->
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
       <property name="maxInactiveIntervalInSeconds" value="1800" />
       <property name="httpSessionStrategy" ref="httpSessonStrategy"/>
</bean>

在 web.xml 中配置过滤器

同3.3

验证

第一步:编写 Controller代码

@RequestMapping(value = "user", method = RequestMethod.POST)
public void setUser(HttpSession session) {
    User user = new User();
    user.setName("lyf");
    user.setPassword("123");
    session.setAttribute("user", user);
}
@RequestMapping(value = "user", method = RequestMethod.GET)
public String getUser(HttpSession session) {
    User user = (User) session.getAttribute("user");
    String name = user.getName();
    return "用户名称:" + name;
}

第二步:浏览器中访问 Controller

  • post 请求:localhost:8080/training/user

响应头部以下:Response Headers:

x-auth-token:256064c7-b583-460f-bbd2-1f6dab3fd418
...

区别 Cookie 的地方在于,这种方式在响应头信息中添加了惟一标识字段 x-auth-token

此时使用 redis-cli 到 redis 库中查询以下:

springsession:0>keys *
1) spring:session:expirations:1502597280000
2) spring:session:sessions:256064c7-b583-460f-bbd2-1f6dab3fd418
  • get 请求:localhost:8080/training/user

    请求头部以下:Response Headers:

    x-auth-token:00ee4b6a-0aeb-42b1-a2bd-eae6f370c677

会发现此时在响应头信息中又从新建立了一个 x-auth-token ,由于 spring-seesion 的底层实现是在请求的时候服务端若是没有拿到这个惟一标识,就会从新建立一个新的 x-auth-token,
并保存到 redis 库中。

此时使用 redis-cli 到 redis 库中查询以下:

springsession:0>keys *
1) spring:session:sessions:00ee4b6a-0aeb-42b1-a2bd-eae6f370c677
2) spring:session:expirations:1502597280000
3) spring:session:sessions:256064c7-b583-460f-bbd2-1f6dab3fd418
4) spring:session:expirations:1502597460000
  • 总结

所以要想获取到 session 中的用户信息,须要将服务端返回的 x-auth-token 惟一标识符附加到 Headers上,而后服务器根据这个惟一标识符才能找到对应的用户信息
在此过程的 get 请求的 Headers 中添加以下键值对:

x-auth-token:256064c7-b583-460f-bbd2-1f6dab3fd418

服务器经过 Headers 中的 x-auth-token 从 redis 库中找到了须要的 session 对象并返回,浏览器显示以下:

用户名称:lyf

所以:

Spring-session 能够控制客户端和服务器端之间如何进行 sessionid 的交换,这样更加易于编写 Restful API,由于它能够从 HTTP 头信息中获取 sessionid ,而没必要再依赖于 cookie 。

spring-session redis 数据结构

建立 spring session

RedisSession 在建立时设置 3 个变量 creationTime ,maxInactiveInterval ,lastAccessedTime 。maxInactiveInterval 默认值为 1800 ,表示 1800s 以内该 session 没有被再次使用,则代表该 session 已过时。每次 session 被访问都会更新 lastAccessedTime 的值, session 的过时计算公式:当前时间-lastAccessedTime > maxInactiveInterval.

/**
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
**/
RedisSession() {
    this(new MapSession());
    this.delta.put(CREATION_TIME_ATTR, getCreationTime());
    this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
    this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
    this.isNew = true;
    this.flushImmediateIfNecessary();
}
public MapSession() {
    this(UUID.randomUUID().toString());
}

flushImmediateIfNecessary 判断 session 是否须要当即写入后端存储。

获取 session

spring session在 redis 里面保存的数据包括:

  • SET 类型的spring:session:expireations:[min]

    min 表示从 1970 年 1 月 1 日 0 点 0 分通过的分钟数, SET 集合的 member 为 expires:[sessionId] ,表示 members 会在 min 分钟后过时。

  • String 类型的spring:session:sessions:expires:[sessionId]

    该数据的 TTL 表示 sessionId 过时的剩余时间,即 maxInactiveInterval。

  • Hash 类型的spring:session:sessions:[sessionId]

    session 保存的数据,记录了 creationTime,maxInactiveInterval,lastAccessedTime,attribute。前两个数据是用于 session 过时管理的辅助数据结构。

获取 session 流程:

​ 应用经过 getSession(boolean create) 方法来获取 session 数据,参数 create 表示 session 不存在时是否建立新的 session 。 getSession 方法首先从请求的 “.CURRENT_SESSION” 属性来获取 currentSession ,没有 currentSession ,则从 request 取出 sessionId ,而后读取 spring:session:sessions:[sessionId] 的值,同时根据 lastAccessedTime 和 MaxInactiveIntervalInSeconds 来判断这个 session 是否过时。若是 request 中没有 sessionId ,说明该用户是第一次访问,会根据不一样的实现,如 RedisSession ,MongoExpiringSession ,GemFireSession 等来建立一个新的 session 。

​ 另外, 从 request 取 sessionId 依赖具体的 HttpSessionStrategy 的实现,spring session 给了两个默认的实现 CookieHttpSessionStrategy 和 HeaderHttpSessionStrategy ,即从 cookie 和 header 中取出 sessionId 。

具体的代码实如今第 4 章已经演示了。

session 有效期与删除

spring session 的有效期指的是访问有效期,每一次访问都会更新 lastAccessedTime 的值,过时时间为lastAccessedTime + maxInactiveInterval ,也即在有效期内每访问一次,有效期就向后延长 maxInactiveInterval。

对于过时数据,通常有三种删除策略:

1)定时删除,即在设置键的过时时间的同时,建立一个定时器, 当键的过时时间到来时,当即删除。

2)惰性删除,即在访问键的时候,判断键是否过时,过时则删除,不然返回该键值。

3)按期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过时键。至于要删除多少过时键,以及要检查多少个数据库,则由算法决定。

​ redis 删除过时数据采用的是懒性删除+按期删除组合策略,也就是数据过时了并不会及时被删除。为了实现 session 过时的及时性,spring session 采用了定时删除的策略,但它并非如上描述在设置键的同时设置定时器,而是采用固定频率(1分钟)轮询删除过时值,这里的删除是惰性删除

​ 轮询操做并无去扫描全部的 spring:session:sessions:[sessionId] 的过时时间,而是在当前分钟数检查前一分钟应该过时的数据,即 spring:session:expirations:[min] 的 members ,而后 delete 掉 spring:session:expirations:[min] ,惰性删除 spring:session:sessions:expires:[sessionId] 。

​ 还有一点是,查看三个数据结构的TTL时间,spring:session:sessions:[sessionId] 和 spring:session:expirations:[min] 比真正的有效期大 5 分钟,目的是确保当 expire key 数据过时后,监听事件还能获取到 session 保存的原始数据。

@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
    this.expirationPolicy.cleanExpiredSessions();
}
public void cleanExpiredSessions() {
    long now = System.currentTimeMillis();
    long prevMin = roundDownMinute(now);
    // preMin 时间到,将 spring:session:expirations:[min], 
    // set 集合中 members 包括了这一分钟以内须要过时的全部
    // expire key 删掉, member 元素为 expires:[sessionId]
    String expirationKey = getExpirationKey(prevMin);
    Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
    this.redis.delete(expirationKey);
    for (Object session : sessionsToExpire) {
        // sessionKey 为 spring:session:sessions:expires:[sessionId]
        String sessionKey = getSessionKey((String) session);
        // 利用 redis 的惰性删除策略
        touch(sessionKey);
    }
}

​ spring session 在 redis 中保存了三个 key ,为何? sessions key 记录 session 自己的数据,expires key标记 session 的准确过时时间,expiration key 保证 session 可以被及时删除,spring 监听事件可以被及时处理。

上面的代码展现了 session expires key 如何被删除,那 session 每次都是怎样更新过时时间的呢? 每一次 http 请求,在通过全部的 filter 处理事后,spring session 都会经过 onExpirationUpdated() 方法来更新 session 的过时时间, 具体的操做看下面源码的注释。

public void onExpirationUpdated(Long originalExpirationTimeInMilli,
            ExpiringSession session) {
    String keyToExpire = "expires:" + session.getId();
    long toExpire = roundUpToNextMinute(expiresInMillis(session));
    if (originalExpirationTimeInMilli != null) {
        long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
        // 更新 expirations:[min] ,两个分钟数以内都有这个 session ,将前一个 set 中的成员删除
        if (toExpire != originalRoundedUp) {
            String expireKey = getExpirationKey(originalRoundedUp);
            this.redis.boundSetOps(expireKey).remove(keyToExpire);
        }
    }
    long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds();
    String sessionKey = getSessionKey(keyToExpire);
    if (sessionExpireInSeconds < 0) {
        this.redis.boundValueOps(sessionKey).append("");
        this.redis.boundValueOps(sessionKey).persist();
        this.redis.boundHashOps(getSessionKey(session.getId())).persist();
        return;
    }
    String expireKey = getExpirationKey(toExpire);
    BoundSetOperations<Object, Object> expireOperations = this.redis
            .boundSetOps(expireKey);
    expireOperations.add(keyToExpire);
    long fiveMinutesAfterExpires = sessionExpireInSeconds
            + TimeUnit.MINUTES.toSeconds(5);
    // expirations:[min] key 的过时时间加 5 分钟
    expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    if (sessionExpireInSeconds == 0) {
        this.redis.delete(sessionKey);
    }
    else {
        // expires:[sessionId] 值为“”,过时时间为 MaxInactiveIntervalInSeconds
        this.redis.boundValueOps(sessionKey).append("");
        this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
                TimeUnit.SECONDS);
    }
    // sessions:[sessionId] 的过时时间加 5 分钟
    this.redis.boundHashOps(getSessionKey(session.getId()))
            .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}

源码解读

源码架构分析

使用 spring-session 须要解决两个核心问题:

问题一:如何建立集群环境下高可用的 session,要求可以可靠并高效地存储数据

解决:在高可用可扩展的集群中存储数据已经经过各类数据存储方案获得了解决,如 Redis、GemFire 以及 Apache Geode 等等

问题二:如何保证无论请求是 HTTP、WebSocket 等其余协议,服务端都可以获取到 sessionid 来找到对应的资源

解决:Spring Session 认为将请求与特定的 session 实例关联起来的问题是与协议相关的,由于在请求/响应周期中,客户端和服务器之间须要协商赞成一种传递 sessionid 的方式。例如,若是请求是经过 HTTP 传递进来的,那么 session 能够经过 HTTP cookie 或 HTTP Header 信息与请求进行关联。若是使用 HTTPS 的话,那么能够借助SSL sessionid 实现请求与 session 的关联。若是使用 JMS 的话,那么 JMS 的 Header 信息可以用来存储请求和响应之间的 sessionid 。

HTTP 支持

Spring Session 对 HTTP 的支持是经过标准的 servlet filter 来实现的,这个 filter 必需要配置为拦截全部的 web 应用请求,而且它应该是 filter 链中的第一个 filter 。Spring Session filter 会确保随后调用javax.servlet.http.HttpServletRequestgetSession()方法时,都会返回 Spring Session 的HttpSession实例,而不是应用服务器默认的 HttpSession。

首先,咱们了解一下标准 servlet 扩展点的一些背景知识:

在2001年,Servlet 2.3规范引入了ServletRequestWrapper。官方API中解释,ServletRequestWrapper“提供了ServletRequest接口的便利实现,开发人员若是但愿将请求适配到 Servlet 的话,能够编写它的子类。这个类实现了包装(Wrapper)或者说是装饰(Decorator)模式。对方法的调用默认会经过包装的请求对象来执行”。以下的代码样例抽取自 Tomcat,展示了 ServletRequestWrapper 是如何实现的。

public class ServletRequestWrapper implements ServletRequest {

    private ServletRequest request;

    /**
     * 建立 ServletRequest 适配器,它包装了给定的请求对象。
     */
    public ServletRequestWrapper(ServletRequest request)  {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");   
        }
        this.request = request;
    }

    public ServletRequest getRequest() {
        return this.request;
    }
    
    public Object getAttribute(String name) {
        return this.request.getAttribute(name);
    } 
}

Servlet 2.3 规范还定义了HttpServletRequestWrapper,它是ServletRequestWrapper的子类,可以快速提供HttpServletRequest的自定义实现,以下的代码是从 Tomcat 抽取出来的,展示了HttpServletRequesWrapper类是如何运行的。

public class HttpServletRequestWrapper extends ServletRequestWrapper 
    implements HttpServletRequest {

    public HttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    private HttpServletRequest _getHttpServletRequest() {
        return (HttpServletRequest) super.getRequest();
    }
  
    public HttpSession getSession(boolean create) {
     return this._getHttpServletRequest().getSession(create);
    }
   
    public HttpSession getSession() {
      return this._getHttpServletRequest().getSession();
    } 
}

因此,借助这些包装类就能编写代码来扩展HttpServletRequest,重载返回HttpSession的方法,让它返回由外部存储所提供的实现。以下的代码是从 Spring Session 项目中提取出来的。

/*
 * 注意,Spring Session 项目定义了扩展自
 * 标准 HttpServletRequestWrapper 的类,用来重载
 * HttpServletRequestWrapper 中与 session 相关的方法。
 */
private final class SessionRepositoryRequestWrapper
   extends HttpServletRequestWrapper {

   private HttpSessionWrapper currentSession;
   private Boolean requestedSessionIdValid;
   private boolean requestedSessionInvalidated;
   private final HttpServletResponse response;
   private final ServletContext servletContext;

   /*
   * 注意,这个构造器很是简单,它接收稍后会用到的参数,
   * 而且委托给它所扩展的 HttpServletRequestWrapper
   */
   private SessionRepositoryRequestWrapper(
      HttpServletRequest request,
      HttpServletResponse response,
      ServletContext servletContext) {
     super(request);
     this.response = response;
     this.servletContext = servletContext;
   }

   /*
   * 在这里,Spring Session 项目再也不将调用委托给
   * 应用服务器,而是实现本身的逻辑,
   * 返回由外部数据存储做为支撑的 HttpSession 实例。
   *
   * @Param create 参数表示 session 不存在时是否建立新的 session
   */
   @Override
   public HttpSession getSession(boolean create) {
     // 检查是否存在 session ,若是存在,则直接返回
     if(currentSession != null) {
       return currentSession;
     }
     // 检查当前的请求中是否存在 sessionid
     String requestedSessionId = getRequestedSessionId();
     if(requestedSessionId != null) {
       // 若是存在 sessionid ,将会根据这个 sessionid,从它的 SessionRepository 中加载 session
       S session = sessionRepository.getSession(requestedSessionId);
       if(session != null) {
         // 封装 session 并返回
         this.requestedSessionIdValid = true;
         currentSession = new HttpSessionWrapper(session, getServletContext());
         currentSession.setNew(false);
         return currentSession;
       }
     }
     if(!create) {
       return null;
     }
     // session repository 中没有 session ,而且在当前请求中也没有与之关联的 sessoinid,
     // 那么就建立一个新的 session ,并将其持久化到 session repository 中
     S session = sessionRepository.createSession();
     currentSession = new HttpSessionWrapper(session, getServletContext());
     return currentSession;
   }

   @Override
   public HttpSession getSession() {
     return getSession(true);
   }
}

Spring Session 定义了SessionRepositoryFilter,它实现了 Servlet Filter接口。以下是抽取了这个 filter的关键部分

/*
 * SessionRepositoryFilter 只是一个标准的 ServletFilter,
 * 它的实现扩展了一个 helper 基类。
 */
public class SessionRepositoryFilter < S extends ExpiringSession >
    extends OncePerRequestFilter {

    /*
     * 这个方法是魔力真正发挥做用的地方。这个方法至关于重写了doFilter,
     * 建立了咱们上文所述的封装请求对象和
     * 一个封装的响应对象,而后调用其他的 filter 链。
     * 这里,关键在于当这个 filter 后面的应用代码执行时,
     * 若是要得到 session 的话,获得的将会是 Spring Session 的
     * HttpServletSession 实例,它是由后端的外部数据存储做为支撑的。
     */
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

        request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);

        SessionRepositoryRequestWrapper wrappedRequest =
          new SessionRepositoryRequestWrapper(request,response,servletContext);

        SessionRepositoryResponseWrapper wrappedResponse =
          new SessionRepositoryResponseWrapper(wrappedRequest, response);

        HttpServletRequest strategyRequest =
             httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);

        HttpServletResponse strategyResponse =
             httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);

        try {
            filterChain.doFilter(strategyRequest, strategyResponse);
        } finally {
            wrappedRequest.commitSession();
        }
    }
}

总结:经过对 spring-session 核心源码的分析获得的关键信息是,Spring Session 对 HTTP 的支持所依靠的是一个简单老式的ServletFilter,借助 servlet 规范中标准的特性来实现 Spring Session 的功能。所以,咱们可以让已有的 war 文件使用 Spring Session 的功能,而无需修改已有的代码。

注意事项

  • 如上实现方式都是基于 xml 方式来配置的,官方也有经过注解方式来配置的
  • spring-session 要求 Redis 版本在2.8及以上
  • Spring Session 的核心项目并不依赖于Spring框架,因此,咱们甚至可以将其应用于不使用 Spring 框架的项目中,只是须要引入 spring 经常使用的包,包括 spring-beans, spring-core, spring-tx 等,版本需在 3.2.9 及以上。可是当咱们项目使用了 spring 的时候,版本需在 3.2.9 及以上。
  • 默认状况下,session 存储在 redis 的 key 是“spring:session::”,但若是有多个系统同时使用一个 redis,则会冲突,此时应该配置 redisNamespace 值,配置后,其 key 为 spring:session:devlops:keyName

    • 配置 redisNamesapce 的方式,在以前配置文件的 bean 中添加一个属性便可

      <!-- 将session放入redis -->
          <bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
              <property name="maxInactiveIntervalInSeconds" value="1800" />
              <property name="redisNamespace" value="${redisNamespace}"/>
          </bean>

      注意:spring-session 的版本在 1.1.0 及以上才支持命名空间

  • 若是想在 session 中保存一个对象,必须实现了 Serializable接口,这样 Spring-session 才能对保存的对象进行序列化,从而存储在 redis 里
  • session 的域不一样会生成新的 session 的。因此在项目中作了负载均衡的话,域就是同样的,因此能够实现session 共享
  • 若是选用 redis 云服务,使用过程当中会出现异常,异常缘由是:不少 Redis 云服务提供商考虑到安全因素,会禁用掉 Redis 的 config 命令,所以须要咱们手动在云服务后台管理系统手动配置,或者找云服务售后帮忙配置。而后咱们在配置文件 RedisHttpSessionConfiguration 的 bean 中添加以下配置,解决使用 redis 云服务异常问题

    <!-- 让Spring Session再也不执行config命令 -->
    <util:constant static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP">
    </util:constant>

注意:判断 config 命令是否被禁用,能够在 redis 的命令行去使用 config 命令,若是报没有找到该命令,说明 config 命令被禁用了。

参考

相关文章
相关标签/搜索