spring-session剖析

1、使用场景java

1)一台服务器上的软负载均衡应用mysql

2)分布式应用web

2、实现方式redis

1session数据存cookiespring

将session存储至cookie中,每次请求从cookie中读取session,缺点:不安全,大小有限制sql

2粘性sessionexpress

粘性session是指Ngnix每次都将同一用户的全部请求转发至同一台服务器上,即将用户与服务器绑定,缺点:某台服务器不可用时,获取不到session数据浏览器

3session复制安全

每次session发生变化时,建立或者修改,就广播给全部集群中的服务器,使全部的服务器上的session相同,缺点:副本数据都同样,数据冗余,占用空间服务器

4session共享

使用redismysql等存储session

3、使用配置

1)pom.xml引入jar包

2)web.xml配置filter

3)application.xml启用spring-session

4、流程图

步骤

1请求被filter过滤器拦截,其实是被SessionRepositoryFilter拦截器处理

2)生成requestresponse包装类,后续操做中跟requestresponse相关的操做都是调用包装类的方法

3)业务代码中调用request.getSession()时,实际调用的是SessionRepositoryRequestWrapper类的方法

4SessionRepositoryRequestWrappergetSession()方法会获取当前域中的cookie获取sessionID

5根据sessionIDredis中查找与之对应的RedisSession对象

6)当无RedisSession返回时,建立RedisSession对象,以后调用setAttributegetAttribute方法时,分别是往对象中到map存放和获取值

7)将RedisSession对象放入request中,供后续使用,如SessionRepositoryFilter$SessionRepositoryRequestWrapper. commitSession()

8)将数据保存至redis实际上保存到是RedisSession对象中的delta属性,该属性的数据类型为Map对应的redis数据结构为hash

9)将cookie写入浏览器cookie包括sessionID

5、源码解析

1)加载SessionRepositoryFilter过滤器

web.xml的过滤器为DelegatingFilterProxy过滤器实现了InitializingBean接口,故会调用afterPropertiesSet()方法,最终对应的是SessionRepositoryFilter

为何最后对应的filterSessionRepositoryFilter?因为DelegatingFilterProxy类中的targetBeanName值为springSessionRepositoryFilterinitDelegate()方法是在spring容器中找到idspringSessionRepositoryFilter的对象即为filter的具体实现类。回到application.xml文件,该文件中有一行配置:

<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>

查看RedisHttpSessionConfiguration源码发现其父类有个方法,方法中使用了@Bean注解,该注解相似<bean />,id默认为方法名称,方法参数默认依赖spring容器中id为参数名称的对象,故该代码最后会往spring容器中注入SessionRepositoryFilter对象,id=springSessionRepositoryFilter注入的SessionRepositoryFilter对象且参数sessionRepository依赖spring容器中的id=sessionRepository对象,具体代码以下:

@Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
    sessionRepositoryFilter.setServletContext(this.servletContext);
    if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
         sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy) this.httpSessionStrategy);
    }
    else {
        sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
    }
    return sessionRepositoryFilter;
}

2)加载RedisOperationsSessionRepository

application.xml中有以下配置,查看RedisHttpSessionConfiguration源码,发现Spring容器中id=springSessionRepositoryFilter的类,即为SessionRepositoryFilter且默认的session持久化容器为redis

图中的springSessionRepositoryFilter()方法表示初始化SessionRepositoryFilter对象,注入到spring容器中,且id=springSessionRepositoryFilter其中方法参数sessionRepository表示依赖spring容器中id=sessionRepository的对象。从图中sessionRepository()方法可知,注入spring容器的id=sessionRepository的对象为RedisOperationsSessionRepository即默认的session持久化到redis

3)生成request&response包装对象

spring-session 的核心思想是对HttpServletRequestHttpServletResonse进行包装,后续全部操做requestresponse的方法均调用包装对象的方法,生成包装对象是在SessionRepositoryFilter中进行,通过滤器处理后,controller方法中的HttpServletRequestHttpServletResponse对象均为SessionRepositoryRequestWrapper和MultiSessionHttpServletResponse,具体代码以下:

4)request.getSession()解析

1。获取sessionID

当调用request.getSession()方法时,会从cookie中获取sessionID代码以下:

2。根据sessionID查找Session对象

获取到sessionID且值不为空,则须要到redis中查找与之对应的session对象

loadSession方法定义在RedisOperationsSessionRepository类中,目的是为了将redis中键为spring:session:sessions:{sessionId}hash结构的属性值复制到MapSession,而生成的MapSession对象作为RedisSession类的构造函数的参数,也就是说在RedisSession对象中保存了MapSession对象的引用,能够直接操做RedisSession对象获取redis保存的属性值,具体代码以下:

private MapSession loadSession(String id, Map<Object, Object> entries) {
    MapSession loaded = new MapSession(id);
    for (Map.Entry<Object, Object> entry : entries.entrySet()) {
        String key = (String) entry.getKey();
        if (CREATION_TIME_ATTR.equals(key)) {
            loaded.setCreationTime((Long) entry.getValue());
        } else if (MAX_INACTIVE_ATTR.equals(key)) {
           loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue());
        } else if (LAST_ACCESSED_ATTR.equals(key)) {
           loaded.setLastAccessedTime((Long) entry.getValue());
        } else if (key.startsWith(SESSION_ATTR_PREFIX)) {
           loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()),
           entry.getValue());
        }
    }
    return loaded;
}

3。建立Session对象

获取不到sessionID或者值为空时,则须要建立Session对象

当调用request.getSession.setAttribute(name,value)时,实际是往RedisSession对象中的delta属性中设值,具体代码在RedisOperationsSessionRepository中,以下:

public void setAttribute(String attributeName, Object attributeValue) {
    this.cached.setAttribute(attributeName, attributeValue);
    this.putAndFlush(getSessionAttrNameKey(attributeName), attributeValue);
}

private void putAndFlush(String a, Object v) {
    this.delta.put(a, v);
    this.flushImmediateIfNecessary();
}

4。Session对象保存至Redis

经过以上步骤获取或建立Session对象后,以后就是将Session对象保存到redis中。而保存操做是在SessionRepositoryFilter类的doFilterInternal方法的finally中执行

五、定时任务清理过时key

虽然redis自带了过时key的清理,但采用可是按期删除+懒性删除方式,若是并发量比较大的时候,redis会存在不少无效key,形成内容浪费。鉴于redis清理key方式的弊端,spring-session开启了一个定时任务,定时清理redis中过时的key,其具体思路是取得当前时间的时间戳(精确到分)做为 key,去 redis 中定位到 spring:session:expirations:{当前时间戳} ,这个 set 里面存放的即是全部过时的 key 。具体实现以下:

org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanupExpiredSessions
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
   this.expirationPolicy.cleanExpiredSessions();
}
org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions
public void cleanExpiredSessions() {
   long now = System.currentTimeMillis();
   // 获取当前时间戳对应的分
   long prevMin = roundDownMinute(now);
   if (logger.isDebugEnabled()) {
      logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
   }
   // 获取到spring:session:expirations:{当前时间戳-精确到分}
   String expirationKey = getExpirationKey(prevMin);
   // 取出当前这一分钟应当过时的session
   Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
   // 删除spring:session:expirations:{当前时间戳-精确到分}键,不是删除session自己
   this.redis.delete(expirationKey);
   // 遍历这一分钟要过时的session
   for (Object session : sessionsToExpire) {
      String sessionKey = getSessionKey((String) session);
      // 访问session
      touch(sessionKey);
   }
}

private void touch(String key) {
   // 并非删除key,而只是访问key
   this.redis.hasKey(key);
}

6、客户端禁用cookie

当浏览器禁用cookie后,是没法获取到cookie数据的,也就是说没法获取到jsessionID,获取不到jessionID则没法得到对应的session对象。为了解决这个问题,能够对URL重写,使用response.encodeURL(url)便可。重写URL的目的是在url后面加上jsessonID,这样便能在请求中获取到jsessionID,进一步得到对应的session对象。测试了一下,当集成spring-session后,使用response.encodeURL(url)重写URL时,是不会在url后面加上jsessionID参数,这或许是设计spring-session时,就必需要求不能禁用cookie。

相关文章
相关标签/搜索