開心一刻
快 8 點了,街邊賣油條的還沒來,我只能給他打電話
大哥在電話中說到:勞資賣了這麼多年油條,從來都是自由自在,自從特麼認識了你,居然讓我有了上班的感覺!
Session 共享
SpringBoot
session 共享配置,我相信你們都會,但出於負責的態度,我還是給你們演示一遍
-
新增依賴
<?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>
-
新增配置
檔案配置
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
-
操作 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 資訊
有效時長為什麼是 870
而不是 900
,請把頭伸過來,我悄悄告訴你
我就問你們,SpringBoot Session
共享是不是很簡單?但就是這麼簡單的內容,竟然有人往裡面下毒,而我很不幸的成了那個中毒之人,如果不是我有絕招,說不定就噶過去了,具體細節且聽我慢慢道來
配置不生效
實際專案中,我也是按如上配置的,可 redis 中的存放內容卻是
從結果來看,session 確實是共享了,但為什麼 maxInactiveIntervalInSeconds
、redisNamespace
配置都未生效?我還特意去對比了另外一個專案,一樣的配置流程,那個專案的 名稱空間
和 有效時長
都是正常生效的,而此專案卻未生效,這就讓我徹底懵圈了
debug 原始碼
該嘗試的都嘗試了,maxInactiveIntervalInSeconds
、redisNamespace
始終不生效,沒有辦法了,只能上絕招了
debug 除錯原始碼
問題又來了:斷點打在哪?有兩個地方需要打斷點
-
RedisHttpSessionConfiguration#sessionRepository
跟進到
@EnableRedisHttpSession
註解裡面,會看到@Import(RedisHttpSessionConfiguration.class)
,跟進RedisHttpSessionConfiguration
,會看到被@Bean
修飾的sessionRepository
方法,正常情況下,SpringBoot
啟動過程中會呼叫該方法,我們在該方法第一行打個斷點 -
SpringHttpSessionConfiguration#springSessionRepositoryFilter
注意看
RedisHttpSessionConfiguration
的完整定義@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
後重啟,一切恢復正常,maxInactiveIntervalInSeconds
、redisNamespace
都正常生效;實際工作開發中,此事就完結了,不要再去細扣了,除非你確實閒的蛋疼。但話說回來,你們都來看部落格了,那確實是閒,既然你們這麼閒,那我們繼續扣一扣,扣什麼呢
為什麼我們指定 RedisOperationsSessionRepository 後,RedisHttpSessionConfiguration#sessionRepository 方法不被呼叫,而且 maxInactiveIntervalInSeconds 、redisNamespace 不生效
-
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
@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 例項 -
maxInactiveIntervalInSeconds 、redisNamespace 為什麼不生效
大家注意看
RedisConfig
@Configuration public class RedisConfig { @Autowired private RedisTemplate redisTemplate; @Bean({"redisOperationsSessionRepository", "sessionRepository"}) public RedisOperationsSessionRepository redisOperationsSessionRepository() { return new RedisOperationsSessionRepository(redisTemplate); } }
試問如何讓 maxInactiveIntervalInSeconds 、redisNamespace 生效?
既然官方已經把
RedisOperationsSessionRepository
廢棄了,我們就不要糾結它了,直接不用它!
總結
- SpringBoot Session 共享配置很簡單,如果配置好了結果不對,不要懷疑自己,肯定是有人在程式碼裡下毒了
- 壓箱底的東西(debug 原始碼)雖說不推薦用,但確實是一個萬能的方法,不要求你們精通,但必須掌握
- 作為一個開發者,一定要有職業素養,開發一半的程式碼就不要提交了,著實坑人呀!