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

开心一刻

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

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

Session 共享

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

  1. 添加依赖

    xml 复制代码
    <?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

    yaml 复制代码
    spring:
      session:
        store-type: redis
      redis:
        timeout: 3000
        password: 123456
        host: 10.5.108.226
        port: 6379

    注解配置

    java 复制代码
    @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

    java 复制代码
    @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 信息

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

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

配置不生效

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

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

debug 源码

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

debug 调试源码

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

  1. RedisHttpSessionConfiguration#sessionRepository

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

  2. SpringHttpSessionConfiguration#springSessionRepositoryFilter

    注意看 RedisHttpSessionConfiguration 的完整定义

    java 复制代码
    @Configuration(proxyBeanMethods = false)
    public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
    		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware

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

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

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

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

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

点进 RedisConfig 一看吓一跳

一看提交记录,竟然是 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 实例化就是按着这个顺序进行的,也就说 RedisConfig#redisOperationsSessionRepository 会先被调用;我们把重点放到名字叫做 sessionRepository 的 bean 的实例化过程上。这里补充个 debug 小技巧,因为 bean 很多,而我们只关注其中某个 bean 的实例化,可以借助 IDEA 的 Condition 来实现

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

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

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

    哪来的别名、主名呀

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

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

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

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

    java 复制代码
    @Configuration
    public class RedisConfig {
    
    	@Autowired
    	private 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

    java 复制代码
    @Configuration
    public class RedisConfig {
    
    	@Autowired
    	private RedisTemplate redisTemplate;
    
    	@Bean({"redisOperationsSessionRepository", "sessionRepository"})
    	public RedisOperationsSessionRepository redisOperationsSessionRepository() {
    		return new RedisOperationsSessionRepository(redisTemplate);
    	}
    }

    试问如何让 maxInactiveIntervalInSeconds 、redisNamespace 生效?

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

总结

  1. SpringBoot Session 共享配置很简单,如果配置好了结果不对,不要怀疑自己,肯定是有人在代码里下毒了
  2. 压箱底的东西(debug 源码)虽说不推荐用,但确实是一个万能的方法,不要求你们精通,但必须掌握
  3. 作为一个开发者,一定要有职业素养,开发一半的代码就不要提交了,着实坑人呀!
相关推荐
带刺的坐椅1 天前
SpringBoot3 使用 SolonMCP 开发 MCP
java·ai·springboot·solon·mcp
LUCIAZZZ2 天前
JVM之虚拟机运行
java·jvm·spring·操作系统·springboot
堕落年代2 天前
SpringSecurity当中的CSRF防范详解
前端·springboot·csrf
一零贰肆2 天前
深入理解SpringBoot中的SpringCache缓存技术
java·springboot·springcache·缓存技术
LUCIAZZZ4 天前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot
LUCIAZZZ5 天前
JVM之内存管理(二)
java·jvm·后端·spring·操作系统·springboot
天上掉下来个程小白6 天前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
程序员buddha8 天前
SpringBoot+Dubbo+Zookeeper实现分布式系统步骤
分布式·zookeeper·dubbo·springboot
天上掉下来个程小白8 天前
缓存套餐-03.功能测试
redis·缓存·springboot·苍穹外卖·springcache
Ten peaches9 天前
苍穹外卖(订单状态定时处理、来单提醒和客户催单)
java·数据库·sql·springboot