首页/文章列表/文章详情

SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒!

编程知识1942024-08-05评论

开心一刻

快 8 点了,街边卖油条的还没来,我只能给他打电话

大哥在电话中说到:劳资卖了这么多年油条,从来都是自由自在,自从特么认识了你,居然让我有了上班的感觉!

你让我有了上班的感觉

Session 共享

SpringBoot session 共享配置,我相信你们都会,但出于负责的态度,我还是给你们演示一遍

  1. 添加依赖

    <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.qsl</groupId> <artifactId>spring-boot-session-demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <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> </dependencies></project>
  2. 添加配置

    文件配置application.yml

    spring: session: store-type: redis redis: timeout: 3000 password: 123456 host: 10.5.108.226 port: 6379

    注解配置

    @SpringBootApplication@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 900, redisNamespace ="session-demo")public class SessionApplication { public static void main(String[] args) { SpringApplication.run(SessionApplication.class, args); }}

    两个配置项需要说明下

    maxInactiveIntervalInSeconds:session 有效时长,单位是秒,示例中 session 有效时长是 900s

    redisNamespace:redis 命名空间,即将 session 信息存于 redis 的哪个命名空间下,没有会创建,示例中是 session-demo

  3. 操作 session

    为了简化,直接提供接口设置和访问 session

    @RestController@RequestMapping("hello")public class HelloController { @GetMapping("/set") public String set(HttpSession session) { session.setAttribute("user","qsl"); return"qsl"; } @GetMapping("/get") public String get(HttpSession session) { return session.getAttribute("user") +""; }}

至此,搭建就算完成了,启动后访问

http://localhost:8080/hello/set

然后去 redis 看 session 信息

redis_session

有效时长为什么是870而不是900,请把头伸过来,我悄悄告诉你

20230115143049

我就问你们,SpringBoot Session共享是不是很简单?但就是这么简单的内容,竟然有人往里面下毒,而我很不幸的成了那个中毒之人,如果不是我有绝招,说不定就噶过去了,具体细节且听我慢慢道来

配置不生效

实际项目中,我也是按如上配置的,可 redis 中的存放内容却是

异现象

从结果来看,session 确实是共享了,但为什么 maxInactiveIntervalInSecondsredisNamespace配置都未生效?我还特意去对比了另外一个项目,一样的配置流程,那个项目的命名空间有效时长都是正常生效的,而此项目却未生效,这就让我彻底懵圈了

懵

debug 源码

该尝试的都尝试了,maxInactiveIntervalInSecondsredisNamespace始终不生效,没有办法了,只能上绝招了

debug 调试源码

问题又来了:断点打在哪?有两个地方需要打断点

  1. RedisHttpSessionConfiguration#sessionRepository

    跟进到@EnableRedisHttpSession注解里面,会看到@Import(RedisHttpSessionConfiguration.class),跟进RedisHttpSessionConfiguration,会看到被@Bean修饰的sessionRepository方法,正常情况下,SpringBoot启动过程中会调用该方法,我们在该方法第一行打个断点

    sessionRepository 断点
  2. SpringHttpSessionConfiguration#springSessionRepositoryFilter

    注意看RedisHttpSessionConfiguration的完整定义

    @Configuration(proxyBeanMethods = false)public class RedisHttpSessionConfiguration extends SpringHttpSessionConfigurationimplements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware

    它继承SpringHttpSessionConfiguration,跟进去你会发现有个被@Bean修饰的springSessionRepositoryFilter方法,正常情况下,SpringBoot启动过程中也会调用该方法,我们也在该方法第一行打个断点

    springSessionRepositoryFilter 断点

打完断点后,重新以debug方式进行启动,我们会发现最先来到springSessionRepositoryFilter的断点

springSessionRepositoryFilter 断点进入

然后我们按F9,会发现项目启动完了都没有来到RedisHttpSessionConfiguration#sessionRepository的断点,这是为什么?SpringHttpSessionConfiguration#springSessionRepositoryFilter方法有个参数SessionRepository<S> sessionRepository,它依赖RedisIndexedSessionRepository实例,也就说RedisHttpSessionConfiguration#sessionRepository应该被先调用,sessionRepository方法都没有被调用,那springSessionRepositoryFilter方法的参数实例是个什么鬼?我们再次以debug方式启动

springSessionRepositoryFilter 参数

怎么是RedisOperationsSessionRepository,为什么不是RedisIndexedSessionRepository?我们来看看RedisOperationsSessionRepository

RedisOperationsSessionRepository

它继承了RedisIndexedSessionRepository,重点是它被@Deprecated了呀,怎么还会创建该类型的实例,它是哪里被实例化了?按住ctrl键,鼠标左击RedisOperationsSessionRepository

RedisOperationsSessionRepository 被调用

点进RedisConfig一看吓一跳

RedisOperationsSessionRepository 实例化

一看提交记录,竟然是2021-09-26提交的,一看提交人,好家伙,早就离职了!

代码里下毒

我估摸着,当初想做 session 共享,但是开发到了一半,直接离职了,你说你离职就离职吧,为什么要提交这一半代码,真的是,气的我牙都咬碎了!

注释掉RedisConfig后重启,一切恢复正常,maxInactiveIntervalInSecondsredisNamespace都正常生效;实际工作开发中,此事就完结了,不要再去细扣了,除非你确实闲的蛋疼。但话说回来,你们都来看博客了,那确实是闲,既然你们这么闲,那我们继续扣一扣,扣什么呢

为什么我们指定 RedisOperationsSessionRepository 后,RedisHttpSessionConfiguration#sessionRepository 方法不被调用,而且 maxInactiveIntervalInSeconds 、redisNamespace 不生效

  1. RedisHttpSessionConfiguration#sessionRepository 为什么没被调用

    不管是我们自定义的RedisConfig#redisOperationsSessionRepository,还是 SpringBoot 的 RedisHttpSessionConfiguration#sessionRepository,都会在启动过程中被 SpringBoot 解析成 BeanDefinition,至于如何解析的,这就涉及到@Configuration的解析原理,不了解的可以先看看:spring-boot-2.0.3源码篇 - @Configuration、Condition与@Conditional 。另外,BeanDefinition 的扫描是有先后顺序的,详情请看:三探循环依赖 → 记一次线上偶现的循环依赖问题

    回到我们的案例,那么RedisConfig#redisOperationsSessionRepository会先于RedisHttpSessionConfiguration#sessionRepository 扫描成 BeanDefinition

    bean定义顺序

    而紧接着的 bean 实例化就是按着这个顺序进行的,也就说 RedisConfig#redisOperationsSessionRepository会先被调用;我们把重点放到名字叫做sessionRepository 的 bean 的实例化过程上。这里补充个 debug 小技巧,因为 bean 很多,而我们只关注其中某个 bean 的实例化,可以借助 IDEA 的 Condition来实现

    idea 条件debug

    然后按F9,会直接来到sessionRepository实例化过程,然后经过getBean(String name)来到doGetBean

    doGetBeanpng

    跟进transformedBeanName方法,继续跟进来到canonicalName

    anonialName

    这是重点,大家看仔细了,根据别名递归读取主名,返回最后那个主名,是不是这么个逻辑?然而新的疑问又来了

    哪来的别名、主名呀

    常规情况下,bean 只有一个名字,也就是主名,使用 @Bean的时候如果没有指定名字,那么名字默认就是方法名,而如果指定了名字就采用指定的名字;支持指定多个名字,第一个是主名,后面的都是别名

    主名别名
    aliasMap

    所以,根据别名sessionRepository就得到了redisOperationsSessionRepository这个主名

    sessionRepository被替换OperationsSessionRepository

    而名叫redisOperationsSessionRepository 的 bean 已经被创建过了,类型是 RedisOperationsSessionRepository,直接从容器中获取,然后返回;所以RedisHttpSessionConfiguration#sessionRepository没被调用,你们明白了吗?

    回到最初的问题,如果不注释RedisConfig,而只是拿掉别名sessionRepository

    @Configurationpublic class RedisConfig {@Autowiredprivate RedisTemplate redisTemplate;@Bean({"redisOperationsSessionRepository"})public RedisOperationsSessionRepository redisOperationsSessionRepository() {return new RedisOperationsSessionRepository(redisTemplate);}}

    问题能不能得到解决?

    总结下

    根据扫描先后循序,RedisConfig#redisOperationsSessionRepository 的 BeanDefinition 排在 RedisHttpSessionConfiguration#sessionRepository 前面,所以 bean 实例创建的时候,RedisOperationsSessionRepository 实例会被先创建,而这个实例的别名 sessionRepository 正好与 RedisHttpSessionConfiguration#sessionRepository 名字重复,所以不会调用 RedisHttpSessionConfiguration#sessionRepository 来创建实例,而是直接返回已经创建好的 RedisOperationsSessionRepository 实例

  2. maxInactiveIntervalInSeconds 、redisNamespace 为什么不生效

    大家注意看RedisConfig

    @Configurationpublic class RedisConfig {@Autowiredprivate RedisTemplate redisTemplate;@Bean({"redisOperationsSessionRepository","sessionRepository"})public RedisOperationsSessionRepository redisOperationsSessionRepository() {return new RedisOperationsSessionRepository(redisTemplate);}}

    试问如何让 maxInactiveIntervalInSeconds 、redisNamespace 生效?

    既然官方已经把RedisOperationsSessionRepository废弃了,我们就不要纠结它了,直接不用它!

总结

  1. SpringBoot Session 共享配置很简单,如果配置好了结果不对,不要怀疑自己,肯定是有人在代码里下毒了
  2. 压箱底的东西(debug 源码)虽说不推荐用,但确实是一个万能的方法,不要求你们精通,但必须掌握
  3. 作为一个开发者,一定要有职业素养,开发一半的代码就不要提交了,着实坑人呀!

博客园

这个人很懒...

用户评论 (0)

发表评论

captcha