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


開心一刻

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

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

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

前情回顧

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

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

  我們先回顧下前三探

  一探

  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 的出現,循環依賴決定不再遷就,她倆的愛情就產生了

  她倆的愛情信息:

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 下

  從 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 方法,提前暴露代理對象

  也就是說在 ServiceAImpl 對象填充屬性(populateBean(beanName, mbd, instanceWrapper))之前,就將代理對象提前暴露到第三級緩存中

  后續給 ServiceBImpl 對象填充 serviceAImpl 屬性時,就用第三級緩存中的 ServiceAImpl 代理對象

  剔除循環依賴

  循環依賴本就不合理,項目中應盡量避免

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

總結

  循環依賴

  雖說 Spring 通過三級緩存解決了 setter 方式的循環依賴,但這不能成為我們有恃無恐的理由

  循環依賴本就不合理,盡量去規避

  真實項目問題

  相信很多小伙伴會有這樣的疑問:樓主,你是怎么就讓 循環依賴 遇上 BeanPostProcessor ?

  因為已有代碼的不規范,導致很多地方都產生了循環依賴,而最近又引入 Shareding-JDBC 做分庫,而 Shareding-JDBC 又通過 BeanPostProcessor 來生成代理對象

  就這樣,她倆就相遇了


免責聲明!

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



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