spring-session剖析

news/2025/2/24 7:53:29

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

一、使用场景

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

2)分布式应用

二、实现方式

1session数据存cookie

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

2粘性session

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

3session复制

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

4session共享

使用redismysql等存储session

三、使用配置

1)pom.xml引入jar包

bfa597f81c3ce8e7955de2781fc4f336f04.jpg

2)web.xml配置filter

777c845650b64e01bd74321e10043c43b6d.jpg

3)application.xml启用spring-session

6c086364bfa99bc875a77f29076eaac1f35.jpg

四、流程图

4df6fda70b5b6dcc95121df13298917315c.jpg

步骤

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

五、源码解析

1)加载SessionRepositoryFilter过滤器

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

dd5e902700e5837f709239a2da2dbac4e43.jpg

为什么最后对应的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

99b72d207892c419cdb22cbdba83f45ac8f.jpg

图中的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,具体代码如下:3e2ea75384b376f435f0662d09fe6255ddc.jpg

4)request.getSession()解析

1。获取sessionID

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

d0c1840cc01aa1dae6ac2c2067b21209401.jpg

2。根据sessionID查找Session对象

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

ebfb7c66e92a40b4cec7dc1781ec454e6c9.jpg

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对象

640797909b6692eb39b0229bfb887ed0b32.jpg

当调用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中执行

722eac1c8be623ed374d4c7e992fbab028f.jpg

5、定时任务清理过期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);
}

六、客户端禁用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。

转载于:https://my.oschina.net/u/732520/blog/1827654


http://www.niftyadmin.cn/n/1895003.html

相关文章

css学习笔记(一)

position定位 CSS position属性用于指定一个元素在文档中的定位方式。top&#xff0c;right&#xff0c;bottom 和 left 属性则决定了该元素的最终位置。 定位类型 定位元素&#xff08;positioned element&#xff09;是其计算后位置属性为 relative, absolute, fixed 或 stic…

bitmap索引 MySQL_( 转 ) 数据库BTree索引、Hash索引、Bitmap位图索引的优缺点

测试于&#xff1a;MySQL 5.5.25当前测试的版本是Mysql 5.5.25只有BTree和Hash两种索引类型&#xff0c;默认为BTree。Oracle或其他类型数据库中会有Bitmap索引(位图索引)&#xff0c;这里作为比较也一起提供。BTree索引BTree(多路搜索树&#xff0c;并不是二叉的)是一种常见的…

python学习笔记--变量和运算符

一、变量命名规则 1.字母、数字、下划线组成 2.不以数字开头 3.关键字(也叫保留字)&#xff0c;不能用作变量名 4.遵循PEP8命名规范 二、变量赋值 1.赋值符号 2.多重赋值 xy123 3.多变量赋值 x,y,z123 4.交换两个变量的值 可以 a,bb,a 三、运算符 1.算数运算符 &#xff1a; …

mpvue重构小程序之坑点1

对于最近刚做的项目&#xff0c;想着用框架重新架构一遍&#xff0c;方便以后拓展,毕竟现在拓展方向非常大。 目前主要碰到两个略坑的问题&#xff1a; 1&#xff1a; 关于用户授权信息的按钮&#xff0c;原生是以下方式&#xff0c;使用bindgetuserinfo事件 <button stylew…

C/C++中typedef struct和struct的用法

由于对typedef理解不够&#xff0c;因此从网上摘录了一些资料&#xff0c;整理如下&#xff1a; C/C中typedef struct和struct的用法 struct _x1 { ...}x1; 和 typedef struct _x2{ ...} x2; 有什么不同&#xff1f; 其实, 前者是定义了类_x1和_x1的对象实例…

php mysql date 格式化时间_PHP+Mysql如何格式化日期时间?

写过PHPMySQL的程序员都知道有时间差&#xff0c;UNIX时间戳和格式化日期是我们常打交道的两个时间表示形式&#xff0c;Unix时间戳存储、处理方便&#xff0c;但是不直观&#xff0c;格式化日期直观&#xff0c;但是处理起来不如Unix时间戳那么自如。所以有的时候需要互相转换…

mysql中创建外键如何理解_mysql外键理解

一个班级的学生个人信息表&#xff1a;什么是外键在设计的时候&#xff0c;就给表1加入一个外键&#xff0c;这个外键就是表2中的学号字段&#xff0c;那么这样表1就是主表&#xff0c;表2就是子表。外键用来干什么为了一张表记录的数据不要太过冗余。这和软件project的模块化思…

Transaction, Lock, Isolation Level

http://www.cnblogs.com/RicCC/archive/2010/03/05/transaction-lock-isolation-level.html