SpringBoot Session共享,配置不生效問題排查 → 你竟然在程式碼裡下毒!

青石路發表於2024-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 SpringHttpSessionConfiguration
    		implements 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

    @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

    @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. 作為一個開發者,一定要有職業素養,開發一半的程式碼就不要提交了,著實坑人呀!

相關文章