四探迴圈依賴 → 當迴圈依賴遇上 BeanPostProcessor,愛情可能就產生了!

青石路發表於2022-02-21

開心一刻

  那天知道她結婚了,我整整一個晚上沒睡覺,開了三百公里的車來到她家樓下,緩緩的抽了一支菸......

  天漸漸涼了,響起了鞭炮聲,迎親車隊到了,那天披著婚紗的她很美,真的很美!

  我跟著迎親車隊開了幾公里的時候,收到了她的資訊:別送了,別送了,你的手扶拖拉機太響了 ......

前情回顧

  樓主一而再,再而三的折騰迴圈依賴,你們不煩,樓主自己都煩了,如果你們實在是受不了,那就...

  言歸正傳,雖然確實有點像懶婆娘的裹腳布,又臭又長,但確實還是有點東西的,只要大家堅持看完,肯定會有收穫的!

  我們先回顧下前三探

  一探

  Spring 的迴圈依賴,原始碼詳細分析 → 真的非要三級快取嗎 中講到了迴圈依賴問題

   Spring 通過三級快取解決 setter 迴圈依賴

  一級快取 singletonObjects 存的是對外暴露的物件,也就是我們應用真正用到的物件

  二級快取 earlySingletonObjects 存的是半成品物件或半成品物件的代理物件,用於處理迴圈依賴的物件建立問題

  三級快取 singletonFactories 存的是建立物件的工廠方法,用於處理存在 AOP + 迴圈依賴的物件建立問題

  著重分析了是否一定需要三級快取來解決迴圈依賴問題

  二探

   Spring 不能處理構造方法的迴圈依賴,也不能處理原型迴圈依賴

  再探迴圈依賴 → Spring 是如何判定原型迴圈依賴和構造方法迴圈依賴的,從原始碼的角度分析了 Spring 是如何鑑別構造方法迴圈依賴、原型迴圈依賴的

   Set<String> singletonsCurrentlyInCreation 會記錄當前正在建立中的例項名稱, Spring 建立例項物件之前,會判斷 singletonsCurrentlyInCreation 中是否存在該例項的名稱,如果存在則表示產生構造方法迴圈依賴了

   ThreadLocal<Object> prototypesCurrentlyInCreation 會記錄當前執行緒正在建立中的原型例項名稱, Spring 建立原型例項物件之前,會判斷 prototypesCurrentlyInCreation 中是否存在該例項的名稱,如果存在則表示產生原型迴圈依賴了

  三探

  三探迴圈依賴 → 記一次線上偶現的迴圈依賴問題,從原始碼的角度分析了這次偶現問題可能出現的原因

   BeanDefinition 的掃描順序:以啟動類為起點,掃描啟動類同級目錄下的所有資料夾,按資料夾名升序順序進行掃描,會遞迴掃描每個資料夾,檔案掃描也是按檔名升序順序進行

   BeanDefinition 覆蓋, @Configuration + @Bean 修飾的 BeanDefinition 會覆蓋 @Component 修飾的 BeanDefinition , BeanDefinition 的覆蓋並不影響 BeanDefinition 的掃描

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

  一通分析下來,雖說沒能找到問題的真正原因,但至少知道了如何去規避這個問題,如何正確的書寫規範的程式碼

問題復現

  經過前面三探,樓主以為對 Spring 的迴圈依賴已經拿捏的死死的了,然而當他出現後,樓主才發現,不是她離不開我,而是我離不開她了

  我們來看看迴圈依賴和 BeanPostProcessor 是如何產生愛情的火花的

   SpringBoot 版本 2.0.3.RELEASE ,示例程式碼地址:spring-circular-beanpostprocessor

  我們只需要關注三個類

  依賴很簡單, ServiceAImpl 依賴 ServiceBImpl , ServiceBImpl 也依賴 ServiceAImpl ,這種迴圈依賴,樓主自認為拿捏的死死的

  直到 BeanPostProcessor 的出現,迴圈依賴決定不再遷就,她倆的愛情就產生了

四探迴圈依賴 → 當迴圈依賴遇上 BeanPostProcessor,愛情可能就產生了!

  她倆的愛情資訊:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serviceAImpl': Bean with name 'serviceAImpl' has been injected into other beans [serviceBImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

  此刻,樓主才明白,小丑竟是我自己!

問題分析

  其實她倆的愛情資訊已經提示的很明顯了,樓主再忍痛翻譯一下: serviceAImpl 作為迴圈依賴的一部分注入到了 serviceBImpl 後,又被包裝了,這就意味著 serviceBImpl 引用的不是最終版本的 serviceAImpl 

  關於 BeanPostProcessor ,樓主不想過多介紹,大家可以檢視:Spring擴充介面之BeanPostProcessor,我們來看看它的底層實現

  從錯誤堆疊資訊,我們可以追蹤到 Spring 報錯的程式碼

   因為 ServiceAImpl 比 ServiceBImpl 先被掃描,所以 serviceAImpl 先被例項化,例項化過程如下

  此時一切都正常,問題就出在 serviceAImpl 填充屬性serviceBImpl 完成之後,我們來 debug 下

四探迴圈依賴 → 當迴圈依賴遇上 BeanPostProcessor,愛情可能就產生了!

  從 debug 結果可以看到, ServiceBImpl 的例項物件 ServiceBImpl@5171 中注入的 ServiceAImpl 物件是 ServiceAImpl@5017 

  而經過 initializeBean(beanName, exposedObject, mbd); 後, Spring 暴露出來的 ServiceAImpl 的最終物件是 $Proxy53@5212 

  這就導致 ServiceBImpl@5171 中注入的 ServiceAImpl@5017 並不是最終版本的 ServiceAImpl ,她們的愛情就這麼產生了

問題處理

  面對這樣的問題,我們可以怎麼處理了

  @Lazy

  通過 @Lazy 延遲注入,在真正使用到的時候才進行注入

  在任意一個屬性上加 @Lazy 即可,例如

 

  或者

  或者兩個都加上 @Lazy 

  SmartInstantiationAwareBeanPostProcessor

  棄用 BeanPostProcessor ,改用 SmartInstantiationAwareBeanPostProcessor 

  重寫的方法是: getEarlyBeanReference ,而非 postProcessAfterInitialization 方法,提前暴露代理物件

四探迴圈依賴 → 當迴圈依賴遇上 BeanPostProcessor,愛情可能就產生了!

  也就是說在 ServiceAImpl 物件填充屬性(populateBean(beanName, mbd, instanceWrapper))之前,就將代理物件提前暴露到第三級快取中

  後續給 ServiceBImpl 物件填充 serviceAImpl 屬性時,就用第三級快取中的 ServiceAImpl 代理物件

  剔除迴圈依賴

  迴圈依賴本就不合理,專案中應儘量避免

  至於如何剔除,無法一概而論,需要大家自己去琢磨了

總結

  迴圈依賴

  雖說 Spring 通過三級快取解決了 setter 方式的迴圈依賴,但這不能成為我們有恃無恐的理由

  迴圈依賴本就不合理,儘量去規避

  真實專案問題

  相信很多小夥伴會有這樣的疑問:樓主,你是怎麼就讓 迴圈依賴 遇上 BeanPostProcessor ?

  因為已有程式碼的不規範,導致很多地方都產生了迴圈依賴,而最近又引入 Shareding-JDBC 做分庫,而 Shareding-JDBC 又通過 BeanPostProcessor 來生成代理物件

  就這樣,她倆就相遇了

相關文章