天天看點

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

開心一刻

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

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

  弟:知道了,哥

  我:記得帶上口罩

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

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

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

  我:我也帶

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

前情回顧

  一探

  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 版本

    一定要結合版本來看問題

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