聊透Spring迴圈依賴

語言: CN / TW / HK

 在上一主題 聊透Spring依賴注入 中,我們站在原始碼的角度上,詳細分析了Spring依賴注入的方式、原理和過程,相信認真看完的小夥伴們一定會有所收穫。清楚了Spring依賴注入的來龍去脈之後,本章節我們聊一下和依賴注入密切相關,並且在實際開發中很常見,面試也很喜歡問的一個問題:Spring是怎麼解決迴圈依賴的?

 筆者之前就被問過Spring是怎麼解決迴圈依賴的問題,當時年少無知,對Spring原始碼知之甚少,也沒有做足功課。只是支支吾吾的說到:好像是通過多級快取解決的吧。面試官看我實在窘迫,也沒有深問,算是逃過一劫,可在心裡總是個羈絆。後來隨著對Spring原始碼的深入閱讀和理解,慢慢清楚了Spring解決迴圈依賴的方式。

 後來筆者也一直在想,為什麼這麼多年了,面試還是喜歡問Spring是怎麼解決迴圈依賴的這種問題呢?除了工作中比較常見,究其原因,可能跟需要你對Spring bean的生命週期和AOP有所瞭解才能回答好這個問題有關吧,而這兩者也能直接反應出你對Spring框架的理解程度,也許這就是面試喜歡問這道題深層的含義吧。

 好了,我們不猜面試官的心裡了。既然迴圈依賴這麼實用,那本章節我們就一起聊聊Spring迴圈依賴吧,我們主要分為以下幾個部分進行討論:

什麼是迴圈依賴?
Spring迴圈依賴的解決之道
一定需要三級快取來解決迴圈依賴嗎
在哪些場景下是不支援迴圈依賴的?有解決方法嗎

1. 什麼是迴圈依賴

 在探討Spring迴圈依賴的解決方式以前,我們先來回憶一下什麼是迴圈依賴:A依賴B的同時,B也依賴了A,就構成了迴圈依賴。依賴關係如下圖所示:

 體現到程式碼中為: ```java @Component public class A{ // 依賴B @Autowired private B b; public B getB() { return b; } }

@Component public class B { // 依賴A @Autowired private A a; public A getA() { return a; } }

//比較特殊的迴圈依賴 @Component public class A{ // 依賴B @Autowired private A a; } ```  當然也有一些鏈路較長,隱藏的比較深的迴圈依賴,比如:A -> B -> C -> D -> ... -> B這種,無論如何,都要形成一個環才能被稱為迴圈依賴。

 Spring的迴圈依賴過程,跟bean的生命週期密切相關,在例項化bean的時候,會完成依賴注入。所以就會出現:例項化A -> 屬性填充注入B ->B還沒有例項化,需要先進行例項化B(A等待) -> 例項化B -> 注入A -> A例項化未完成,無法注入 -> 例項化B失敗 -> 例項化A失敗。這個問題類似於我們常見的死鎖,頗有點有點窈窕淑女,求之不得的味道。

 有沒有辦法解決呢?當然有,英文Spring就解決了嘛。就是在例項化過程中,提前把bean暴露出來,雖然此時還是個半成品(屬性未填充),但是我們允許先注入,這樣確實能解決問題。我們梳理一下: 例項化A -> 暴露a物件(半成品)-> 屬性填充注入B ->B還沒有例項化,需要先進行例項化B(A等待) -> 例項化B -> 注入A(半成品) -> 例項化B成功 -> 例項化A成功。通過提前把半成品的物件暴露出來,支援別的bean注入,確實可以解決迴圈依賴的問題,實際上,Spring也確實是這麼做的。沿著這個思路,下文我們詳細分析Spring的處理方式。

 什麼,你問b物件中注入的a屬性還是半成品怎麼辦?大兄弟,難道你忘了java物件的傳遞,本質是傳遞引用的嗎,記憶體中是同一個物件啊,物件的修改是相互影響的啊。有朝一日,物件a完整了,b物件中的a屬性,肯定也碼生完整了。

2. Spring迴圈依賴解決之道

2.1 Spring通過三級快取解決依賴注入

  Spring究竟是不是通過上述我們說的方式解決的呢,其實思路是一致的,只是Spring處理的更加嚴謹。Spring是通過三級快取來解決迴圈依賴的,提前暴露的物件存放在三級快取中,二級快取存放過渡bean,一級快取存放最終形態的bean。下面我們還是用A -> B -> A的場景,看一下Spring是如何解決迴圈依賴的。我們按照過程一步步來分析,首先是例項化A的過程:


 此時執行到屬性填充環節,需要注入b,因為Spring管理的bean預設是單例的,為防止重複建立,Spring會先去容器中查詢b,如果查詢不到,再進行建立。此時容器中是沒有b的,所以需要先例項化b,流程和例項化a一致。
image.png

 此時B也執行到屬性填充的環節了,有意思的地方開始了,此時又需要注入a,此時還是會先去容器中查詢a,此時的a雖然沒在單例池中,但是因為在建立中,並且也在三級快取中了。所以此時獲取a的流程就發生了變化:不再是直接建立,而是會從三級快取中獲取a,三級快取存放的並不是bean物件,而是生成bean的ObjectFactory,在獲取時會經過AbstractAutowireCapableBeanFactory#getEarlyBeanReference()的處理,才能獲取到bean,然後放入二級快取中,同時返回a進行依賴注入。
image.png

這裡小夥伴可能有疑問:為什麼三級快取中存放的是ObjectFactory而不是bean呢? 而AbstractAutowireCapableBeanFactory#getEarlyBeanReference()的處理又起什麼作用,為什麼三級快取要經過它的處理之後,才能放入二級快取呢?這些問題請小夥伴們稍安勿躁,後面我們會詳細說明的。

 截止到目前,通過提前暴露物件到多級快取,已經成功將例項b中的屬性a注入了,那後面的流程自然一路暢通:繼續執行b的例項化initializeBean() -> 將b從正在建立列表移出 -> 將b放入一級快取(同時將b在二級快取和三級快取中刪除) ->返回b
image.png

 在b例項化完成並返回後,a的例項化流程也從等待著甦醒,繼續執行,後續流程和b的完全一致。
image.png

 其實這就是Spring解決迴圈依賴的流程,其核心思路就是:先將bean提前暴露到三級快取中,後續有依賴注入的話,先將這個半成品的bean進行注入。之所以說這個bean是半成品,是因為暴露在三級快取和二級快取中的bean雖然已經建立成功,但是屬性還沒有進行填充,Aware回撥等流程也沒有執行,所以說它是一個不完整的bean物件。

2.2 多級快取

2.2.1 多級快取的作用

 通過上述對Spring解決迴圈依賴的分析,我們知道Spring採用了三級快取,這裡我們重點看一下每級快取中存放的都是什麼內容:

三級快取(singletonFactories)
 其存放的物件為ObjectFactory型別,主要作用是產生bean物件。Spring在這裡存放的是一個匿名內部類,呼叫getObject()最終呼叫的是getEarlyBeanReference()。該方法的主要作用是:如果有需要,產生代理物件。如果bean被AOP切面代理,返回代理bean物件;如果未被代理,就返回原始的bean物件。

 getEarlyBeanReference()呼叫的SmartInstantiationAwareBeanPostProcessor,其實是Spring留得拓展點,本質是通過BeanPostProcessor定製bean的產生過程。絕大多數AOP(比如@Transactional)單例物件的產生,都是在這裡進行了拓展,進而實現單例物件的生成。

二級快取(earlySingletonObjects)
 主要存放過渡bean,也就是三級快取中ObjectFactory產生的物件。主要作用是防止bean被AOP切面代理時,重複通過三級快取物件ObjectFactory建立物件。被代理情況下,每次呼叫ObjectFactory#getObject()都是會產生新的代理物件的。這明顯不滿足spring單例的原則,所以需要二級快取進行快取。
 同時需要注意:二級快取中存放的bean也是半成品的,此時屬性未填充。

一級快取(singletonObjects)
 也被稱為單例池, 主要存放最終形態的bean(如果存在代理,存放的代理後的bean)。 一般情況我們獲取bean都是從這裡獲取的,但是並不是所有的bean都在單例池裡面,一些特殊的,比如原型的bean就不在裡面。

2.2.2 一定需要三級快取嗎?二級快取行不行?

 縱觀Spring解決迴圈依賴的過程,好像二級快取沒啥實際作用啊,不要二級快取貌似也能搞定迴圈依賴啊?確實,在沒有AOP的情況下,二級快取沒有實際作用,只通過三級快取和一級快取就可以搞定,我們看一下: 1. 首先例項化A,例項化前先將半成品暴露在三級快取中。 2. 填充屬性B,發現B還沒有例項化,先去例項化B。 3. 例項化B的過程中,需要填充屬性A,從三級快取中通過ObjectFactory#getObject()直接獲取A(在沒有AOP的場景下,多次獲取的是同一個bean),進行依賴注入,並完成例項化流程。 4. 獲取到b,例項化A的流程繼續,注入到b到a中,進而完成a的例項化。

 那如果bean被AOP代理了,情況就會大不一樣,最核心的區別點:就是每次呼叫ObjectFactory#getObject()都會產生一個新的代理物件,我們用存在事務的場景測試一下: ```java @Component @EnableTransactionManagement // 開啟事務 public class A { @Autowired private B b;

@Transactional //增加事務註解,會對bean生成代理物件 public B getB() { System.out.println(Thread.currentThread().getName()); return b; } } `` &emsp;測試方法很簡單:我們給A的getB()加上事務註解@Transactional,此時A就會被AOP代理,生成的例項a也是代理物件了。我們debug驗證一下,就會發現此時A確實被CGLIB代理了: <img src="http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/66171406e2934f35b1a83faf8011b9cf~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" width="70%" /><br> &emsp;我們在驗證一下二級快取存在的必要的條件:是不是bean被AOP代理後,多次呼叫ObjectFactory#getObject(),產生的代理物件不是同一個:<br> <img src="http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4fcb4899de214971a29ed564fc623569~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" width="70%" /><br> &emsp;經過singletonFactory.getObject() == singletonObjectfalse`的測試,我們可以確認,確實不是同一個。

這和代理物件的生成有關,後續我們講到AOP的時候,再詳細介紹

 那麼問題來了:A是單例的,也就是要保證,在Spring中,使用到該bean的地方,都是同一個bean才行。但是每次執行singletonFactory.getObject()都會產生新的代理物件。假設只有一級和三級快取,每次從三級快取中獲取代理物件,都會產生新的代理物件,忽略效能不說,是不符合單例原則的。

 所以這裡我們要藉助二級快取來解決這個問題,將singleFactory.getObject()產生的物件放到二級快取中去,後面直接從二級快取中拿,保證始終只有一個代理物件。現在我們已經明白為什麼Spring採用三級快取了吧,我們再總結一下各個快取存放的內容:

image.png

3. 不支援迴圈依賴的情況

3.1 非單例的bean無法支援迴圈依賴

```java //AbstractAutowireCapableBeanFactory.java protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { // 省略部分程式碼 // 是否支援迴圈依賴 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references"); } // 做迴圈依賴的支援 將早期例項化bean的ObjectFactory,新增到單例工廠(三級快取)中 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }

} `` &emsp;在上述支援迴圈依賴的討論中,都有一個前提:提前把半成品bean暴露到三級快取中。在Spring原始碼中,這裡的暴露有前置條件:mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName),我們一起分析一下這些條件: -mbd.isSingleton():要求bean是單例的。 -this.allowCircularReferences:是否允許迴圈依賴,預設為true,即預設支援迴圈依賴。 -isSingletonCurrentlyInCreation(beanName)`:判斷當前bean是否正在建立中,預設是成立的,因為在建立bean的時候,會先設定該標誌。

 通過這個前置條件,我們可以得出結論,只有單例bean才有支援迴圈依賴的可能,非單例的bean不支援迴圈依賴。

3.2 constructor注入導致無法支援迴圈依賴

 如果存在迴圈依賴 A -> B -> A,且都是通過建構函式依賴的,無法支援迴圈依賴,我們來看一下這種場景: ```java @Component public class A{ // 依賴B private B b;

public A(B b){ this.b = b; }

public B getB() { return b; } }

@Component public class B { // 依賴A private A a;

public A(A a){ this.a = a; }

public A getA() { return a; } } `` &emsp;這種情況下,A例項建立時->構造注入B->查詢B,容器中不存在,先例項化建立B->構造注入A->容器中不存在A(此時A還沒有新增到三級快取中) ->異常UnsatisfiedDependencyException`。因為暴露物件放入三級快取的過程在例項建立之後,通過構造方法注入時,還沒有放入三級快取呢,所以無法支援構造器注入型別的迴圈依賴。
 我們簡單看一下bean的生命週期。關於bean的生命週期,後續我們會出單獨的章節講解,這裡先簡單瞭解一下:
image.png

3.3 @Async導致無法支援迴圈依賴

 當迴圈依賴遇到@Async,會出現無法支援的情況,我們先來看一下這種情況: ```java @Component @EnableAsync //開啟非同步 public class A { @Autowired private B b;

@Async // 標註方法非同步處理 public B getBService() { System.out.println(Thread.currentThread().getName()); return b; } }

@Component public class B { @Autowired private A a;

public A getAService() { return a; } } ```

// 輸出資訊: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [b] 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 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:616) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:480) ...  奇了怪了,我們知道@Async@Transactional的底層原理,都是被AOP攔截生成代理物件,進行功能的增強,那為什麼一個支援迴圈依賴,一個不支援呢?我們還是從原始碼中找找答案吧。

 這裡先介紹一些基礎知識,AOP的代理物件的生成是藉助BeanPostProcessor後置處理器觸發的,@Transactional藉助的是InfrastructureAdvisorAutoProxyCreator這個後置處理器,@Async藉助的是AsyncAnnotationBeanPostProcessor這個後置處理器,我們先來一下這兩個後置處理器在類圖上有什麼不同:
image.png
image.png
  我們仔細觀察BeanPostProcessor這條鏈路上的繼承關係,發現雖然兩者都是BeanPostProcessor的子類,但是InfrastructureAdvisorAutoProxyCreator還實現了SmartInstantiationAwareBeanPostProcessor,而AsyncAnnotationBeanPostProcessor沒有。乍一看是不是覺得沒啥,然而這就是造成兩者不同的核心原因😂,是不是一臉問號。 image.png

  小夥伴不要著急,我們一起來看一下這神奇的操作是怎麼產生的。小夥伴們還記得三級快取存放的是ObjectFactory吧,在注入前通過getObject()生成物件進行注入,同時存放到二級快取中。前文我們反覆提過,getObject()其實呼叫的是AbstractAutowireCapableBeanFactory#getEarlyBeanReference(),這裡其實只會處理SmartInstantiationAwareBeanPostProcessor觸發的代理物件的生成。也就是說@Transactional代理物件,在這一步會生成,而@Async代理物件,這裡並不會生成(在之後生成,所以注入的不是代理物件)。我們去原始碼中驗證一下: java protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { // 只會處理SmartInstantiationAwareBeanPostProcessor型別的代理bean if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }   so what,這和迴圈依賴又有什麼關係呢。我們回到正題,知道了這個前提,我們可以得出結論:在迴圈依賴A -> B -> A,並且A中的getB()被@Async標識的情況下,例項b在屬性填充階段,填充的屬性a的值,是沒有被代理的原始物件。我們debug證明一下: image.png

三級快取中ObjectFactory通過getObject()生成物件後,放入二級快取的同時,返回了a,之後直接注入給屬性了,所以這種情況下,二級快取中的和屬性注入的值,都是原始物件。

 但是我們知道,a最終肯定是要被代理的,因為@Async非同步執行的能力,只有增強後的bean才會有。那問題就浮出水面了:容器中最終形態的a是代理後的bean,而例項b中注入的未被代理的bean,兩者是不一致的。這種情況在Spring中被允許嗎,當然不,Spring會盡量控制這種情況的發生,這也就是這個迴圈依賴無法支援的原因。我們看一下原始碼中是怎麼檢測的。
```java protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { // 1: 建立物件 instanceWrapper = createBeanInstance(beanName, mbd, args);

// 2: 完成Merged,這裡主要是完成@Autowired @Resource屬性的查詢
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
  // 3:做迴圈依賴的支援 將早期例項化bean的ObjectFactory,新增到單例工廠(三級快取)中
  addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

//4: 屬性填充
populateBean(beanName, mbd, instanceWrapper);

// 5: 初始化bean,@Async在該步驟生成了代理物件,exposedObject為代理物件
exposedObject = initializeBean(beanName, exposedObject, mbd);

// 預設支援單例bean的迴圈依賴,條件成立 if (earlySingletonExposure) { // 從二級快取獲取早期bean,針對@Async的情況,此時獲取到的是原始物件(不是單例物件) Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { // 針對@Aysnc而言:exposedObject是被代理過的物件, 而bean是原始物件,所以此處不相等 if (exposedObject == bean) { exposedObject = earlySingletonReference; } // allowRawInjectionDespiteWrapping: 是否允許Bean的原始型別被注入到其它Bean裡面,即使自己最終會被包裝(代理), // 預設是false表示不允許,如果改為true表示允許,就不會報錯啦。這是其中一個解決方案; //dependentBeanMap: 記錄著每個Bean它所依賴的Bean的Map~~~~ else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { // 獲取依賴當前bean的bean名稱,B依賴了A,所以beanName為a時,所以此處值為["b"] String[] dependentBeans = getDependentBeans(beanName); Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);

        // 對所有的依賴進行一一檢查
        for (String dependentBean : dependentBeans) {
            /**
            * 此處會有問題, 首先b在alreadyCreated裡面,因為他已經建立完成了,所以返回false。
            * b都例項化完成了,屬性a肯定也賦值完成了,這裡有個隱藏邏輯:屬性a賦值的一定是從二級快取中獲取到的那個原始物件。
            * 而這裡的要返回,最終放入一級快取的是exposedObject,也就是代理物件。
            * 所以B裡面引用的a和主流程我這個A竟然不相等,那肯定就有問題(說明不是最終的)。
            * 這裡Spring將A真正的依賴,加入到actualDependentBeans裡面去
            */
           if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
              actualDependentBeans.add(dependentBean);
           }
        }
        // 發現actualDependentBeans不為空,報錯
        if (!actualDependentBeans.isEmpty()) {
           throw new BeanCurrentlyInCreationException(beanName,
                 "Bean with name '" + beanName + "' has been injected into other beans [" +
                 StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                 "] 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 " +
                 "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
        }
     }
  }

} return exposedObject; } ```

image.png
 通過原始碼跟蹤和debug,證實了我們的猜想:Spring管理的bean是單例的,所以Spring預設要保證使用該bean的地方,指向的都是一個地址,也就是都是最終版本的bean。像帶有@Async的迴圈依賴,會導致在b中注入的a和最終放到容器的a不一致,所以Spring提供了這樣的自檢機制,防止這種問題的發生。

 關於自檢機制,我們在代理註釋中詳細進行了說明,不過小夥伴們可能有個疑惑:Spring發現二級快取準獲取的和最終暴露的不一致後,直接獲取到依賴當前bean(這裡是a)的bean集合,然後遍歷判斷:如果這些bean建立完成了,就說明注入的a有問題,丟擲異常的邏輯依據是什麼?

 其實有這個疑問的小夥伴可能對前面我們說的注入過程還不夠清楚。注入過程發生在屬性填充階段,流程是:從三級快取取出ObjectFactory -> 呼叫getObject()生成物件-> 先放入了二級快取 -> 反射注入給依賴它的屬性,所以注入到其他依賴者進行屬性填充的物件,和二級快取中的同一個物件。二級快取中物件和最終暴露的不一致,注入屬性的物件當然和最終暴露的也不一致了。

 還有一點需要小夥伴們注意,屬性一旦注入後,是不會自動重新整理的。所以:建立完成 -> 屬性注入肯定完成 -> 注入的一定不是最終物件,這個條件是成立的,當然這樣判斷自然也是可以的。

 囉嗦了這麼久,相比大家應該知道為什麼帶有@Async的迴圈依賴的迴圈依賴無法支援了吧。其實就是從設計上就不想支援,是的,現實就是這麼殘忍。

3.3.1 @Async無法支援迴圈依賴的解決方案

  • allowRawInjectionDespiteWrapping設定為true
     修改該引數的配置後,容器啟動將不再報錯了,但是:a的@Aysnc修飾的方法將不起作用了,因為b裡面依賴的a是個原始物件,所以它最終沒法執行非同步操作(即使容器內的a是個代理物件)。

  • 使用@Lazy或者@ComponentScan(lazyInit = true)
    ``` @Component public class B { @Autowired @Lazy private A a;

public A getAService() { return a; } } ```

 本方案只需要在類B的依賴屬性A a上加上@Lazy即可(因為是B希望依賴進來的是最終的代理物件進來,所以B加上即可,A上並不需要加)。但是需要稍微注意的是:此種情況下B裡持有A的引用和Spring容器裡的A並不是同一個,雖然兩者都是代理物件。至於為什麼,後面我們在講解@Lazy的時候,再詳細解釋吧。等我哦

  • 不要讓@Async的Bean參與迴圈依賴
     顯然該方案是解決它的最優方案,奈何它卻是現實情況中最為難達到的方案。因為在實際業務開發中像迴圈依賴、類內方法呼叫等情況並不能避免,除非重新設計、按規範改變程式碼結構,因此此種方案就見仁見智吧~