三探迴圈依賴 → 記一次線上偶現的迴圈依賴問題

青石路發表於2022-02-01

開心一刻

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

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

  弟:知道了,哥

  我:記得帶上口罩

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

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

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

  我:我也帶

前情回顧

  一探

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

    一定要結合版本來看問題

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

相關文章