三探循環依賴 → 記一次線上偶現的循環依賴問題


開心一刻

  心里一直在想明天該以何種方式祭拜列祖列宗,徹夜難眠,輾轉反側,最好下定了決心

  給弟發了個微信:別熬夜了,早上早點起來,咱倆去上墳

  弟:知道了,哥

  我:記得帶上口罩

  弟:墳就在家后邊的山上,這么近帶什么口罩?

  我:就你這逼樣,好意思見列祖列宗?

  弟:我知道了,那哥你帶嗎?

  我:我也帶

前情回顧

  一探

  Spring 的循環依賴,源碼詳細分析 → 真的非要三級緩存嗎 中講到了循環依賴問題

  同樣說明了 Spring 只能解決 setter 方式的循環依賴,不能解決構造方法的循環依賴

  重點介紹了 Spring 是如何解決 setter 方式的循環依賴,感興趣的可以去看下

  二探

  既然 Spring 不能解決構造方法的循環依賴,那么它是如何甄別構造方法循環依賴的了?

  所以進行了二探:再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的

  從源碼的角度講述了 Spring 是如何判定構造方法循環依賴、原型循環依賴的

  感興趣的可以去看下

 

  大家跟源碼的時候,一定要注意版本!!!

項目模擬

  自認為經過了前兩探,對 Spring 循環依賴的問題已了若指掌,可面對線上突如其來的循環依賴問題,樓主竟然沒能一眼看出來!!!

  這樓主能忍?於是樓主又跟起了 Spring 源碼,看看問題到底出在哪?

   SpringBoot 版本是 2.0.3.RELEASE 

  線上服務采用 k8s 部署,本地環境未采用 k8s 部署

  本地啟動從未出現循環依賴問題,線上環境也只是偶發的 pod 啟動失敗(提示信息直指循環依賴)

  問題偶發,而非必現,很是頭疼,但問題還是得解決,從提示信息着手唄

  根據錯誤提示信息,樓主模擬出了一個簡化的工程,方便我們進行問題排查

 

  非常簡單,完整地址:spring-other-circular-reference

  我們來看下類圖

   MyListener 、 MyService 、 MyManager 很常規,特殊的是 MyConfig 和 MySender 

問題復現

  如果按上述工程結構,本地很難復現問題 ,反正樓主是沒復現出來

  我們稍做調整,將 MySender 前置,如下

  啟動失敗,錯誤信息如下:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myConfig': Unsatisfied dependency expressed through field 'myListener'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myListener': Unsatisfied dependency expressed through field 'myService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myServiceImpl': Unsatisfied dependency expressed through field 'myManager'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myManager': Unsatisfied dependency expressed through field 'mySender'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'mySender': Requested bean is currently in creation: Is there an unresolvable circular reference?

  此刻的 Is there an unresolvable circular reference? 讓樓主感到了陌生

問題分析

  我們從以下幾個方面來分析

  BeanDefinition 掃描

  目前 XML 方式的 Bean 定義越來越少,除了一些遺留的老項目,基本看不到 XML 方式的 Bean 定義了

  所以我們只關注注解方式的 Bean 定義的掃描

  文件夾的掃描順序與文件夾名字的升序一致,文件的順序與文件名的升序一致,如下所示

  有興趣的可以去跟下 ConfigurationClassParser 類中 doProcessConfigurationClass 方法;樓主做了下簡單的總結

   @ComponentScan 的處理早於 @Bean 

   BeanDefinition 掃描過程中,會按掃描順序會往 DefaultListableBeanFactory 的 beanDefinitionMap 中添加 BeanDefinition ,往 beanDefinitionNames 添加 BeanName 

  我們來跟下源碼,看是不是如上所說

  先被掃描的 BeanDefinition 的 BeanName 會被先添加到 beanDefinitionNames 

  BeanDefinition 覆蓋

   MyConfig 中通過 @Bean 定義了 MySender ,而 MySender 類上又用了 @Component 進行修飾

  那創建 MySender 實例的時候到底調用的哪個構造方法?(有參還是無參?)

  關於 Spring Boot 中創建對象的疑慮 → @Bean 與 @Component 同時作用同一個類,會怎么樣?從源碼的角度分析了這個問題

  結論是: SpringBoot 2.0.3.RELEASE 中, @Configuration + @Bean 修飾的 BeanDefinition 會覆蓋掉 @Component 修飾的 BeanDefinition 

  也就說 MySender 類上的 @Component 其實沒用,加不加效果是一樣的,這里說的 沒用效果 僅僅指的是 MySender 的 BeanDefinition 

  Bean 實例化順序

   BeanDefinition 用來構建實例,那么 MySender 上的 @Component 就有作用了,它決定了 MySender 的實例化順序

  是先於 MyConfig 、 MyListener 、 MyServiceImpl 、 MyManager 實例化的

  我們來看下 Bean 的實例化順序

  理論上來講,先被掃描的 Bean 會先被實例化; Bean 實例化的過程中會填充屬性,可能會導致后被掃描的 Bean 提前被實例化

  如果 Bean 之間沒有依賴,那么會嚴格按照 Bean 的掃描順序實例化

  再看問題

  我們再回到前面的問題

  這種情況下,我們分析下 Is there an unresolvable circular reference? 是如何產生的

  相較於 MyConfig 、 MyListener 、 MyManager 、 MyServiceImpl , MySender 是最先被掃描到的,所以它最先被實例化

  因為 MyConfig 中通過 @Bean 修飾了 MySender 的 BeanDefinition 

  會覆蓋掉 MySender 自身的無參 BeanDefinition 

  所以會通過 MySender 的有參構造方法來創建 MySender 實例

  因為有參構造方法依賴 myListener ,所以去 Spring 容器中找 MyListener 實例,沒有找到則創建,然后填充 MyListener 實例的屬性

  以此類推,實例的創建過程如下所示:

   Is there an unresolvable circular reference? 就此產生

  相當於是變種的構造方法循環依賴

  最初狀態

  我們還原 MySender 位置

  此時最先實例化的是 MyConfig ,實例化過程如下

  對象是都可以正常實例化、初始化的

  這種情況理論上來講是不會出現 Is there an unresolvable circular reference? 

  線上問題

  一通分析下來,還是沒能找到線上 Is there an unresolvable circular reference? 的原因

  很是尷尬,但是我萌生了這樣的想法:是不是在 k8s 部署過程中, BeanDefinition 的掃描會有偶發的隨機性?

問題修復

  雖然我們沒能找到線上問題的確切原因,但還是有辦法去根治這個問題的

   Spring 不能處理構造方法循環依賴,那我們就去規避它

  刪掉 MyConfig , MySender 改成

 

  或 MySender 改成

 

   還有 @PostConstruct 等,方式有很多,只要不產生構造方法循環依賴就好

 總結

  1、 BeanDefinition 掃描順序

    如果我們去跟源代碼就會發現,以啟動類為起點,掃描啟動類同級目錄下的所有文件夾 

    按文件夾名升序順序進行掃描,會遞歸掃描每個文件夾

    文件掃描也是按文件名升序順序進行

    從線上問題來看,對這個掃描順序,樓主是持懷疑態度的:是 Spring 會偶發的隨機掃描,還是 pod 會導致偶發的隨機掃描

  2、 BeanDefinition 覆蓋

    只要我們讀了源碼,了解 Spring 對各個注解的掃描順序,就清楚它們的替換關系了

     BeanDefinition 覆蓋並不會影響 BeanDefinition 的掃描順序

    也就是不會改變 BeanName 在 beanDefinitionNames 中的位置,即不會影響 Bean 的示例化順序

  3、 Bean 實例化順序

    理論上來講,先被掃描到的就先被實例化,但實例化過程中的屬性填充會打亂這個順序,會將被依賴的對象提前實例化

  4、 Spring 版本

    一定要結合版本來看問題

    版本不同,底層實現可能會不同


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM