深入剖析 Spring 框架:循環依賴的解決機制


你好,我是柳岸花開。

什麼是循環依賴?

很簡單,就是 A 對象依賴了 B 對象,B 對象依賴了 A 對象。

圖片

在 Spring 中,一個對象並不是簡單 new 出來了,而是會經過一系列的 Bean 的生命週期,就是因爲 Bean 的生命週期所以纔會出現循環依賴問題。當然,在 Spring 中,出現循環依賴的場景很多,有的場景 Spring 自動幫我們解決了,而有的場景則需要程序員來解決。

三級緩存

一級緩存爲:singletonObjects

二級緩存爲:earlySingletonObjects

三級緩存爲:singletonFactories

先稍微解釋一下這三個緩存的作用,後面詳細分析:

解決循環依賴思路分析

先來分析爲什麼緩存能解決循環依賴。

上文分析得到,之所以產生循環依賴的問題,主要是:

A 創建時 ---> 需要 B---->B 去創建 ---> 需要 A,從而產生了循環

那麼如何打破這個循環,加個中間人(緩存)

A 的 Bean 在創建過程中,在進行依賴注入之前,先把 A 的原始 Bean 放入緩存(提早暴露,只要放到緩存了,其他 Bean 需要時就可以從緩存中拿了),放入緩存後,再進行依賴注入,此時 A 的 Bean 依賴了 B 的 Bean,如果 B 的 Bean 不存在,則需要創建 B 的 Bean,而創建 B 的 Bean 的過程和 A 一樣,也是先創建一個 B 的原始對象,然後把 B 的原始對象提早暴露出來放入緩存中,然後在對 B 的原始對象進行依賴注入 A,此時能從緩存中拿到 A 的原始對象(雖然是 A 的原始對象,還不是最終的 Bean),B 的原始對象依賴注入完了之後,B 的生命週期結束,那麼 A 的生命週期也能結束。

因爲整個過程中,都只有一個 A 原始對象,所以對於 B 而言,就算在屬性注入時,注入的是 A 原始對象,也沒有關係,因爲 A 原始對象在後續的生命週期中在堆中沒有發生變化。

從上面這個分析過程中可以得出,只需要一個緩存就能解決循環依賴了,那麼爲什麼 Spring 中還需要 singletonFactories 呢?

這是難點,基於上面的場景想一個問題:如果 A 的原始對象注入給 B 的屬性之後,A 的原始對象進行了 AOP 產生了一個代理對象,此時就會出現,對於 A 而言,它的 Bean 對象其實應該是 AOP 之後的代理對象,而 B 的 a 屬性對應的並不是 AOP 之後的代理對象,這就產生了衝突。

B 依賴的 A 和最終的 A 不是同一個對象。

AOP 就是通過一個 BeanPostProcessor 來實現的,這個 BeanPostProcessor 就是 AnnotationAwareAspectJAutoProxyCreator,它的父類是 AbstractAutoProxyCreator,而在 Spring 中 AOP 利用的要麼是 JDK 動態代理,要麼 CGLib 的動態代理,所以如果給一個類中的某個方法設置了切面,那麼這個類最終就需要生成一個代理對象。

一般過程就是:A 類 ---> 生成一個普通對象 --> 屬性注入 --> 基於切面生成一個代理對象 --> 把代理對象放入 singletonObjects 單例池中。

而 AOP 可以說是 Spring 中除開 IOC 的另外一大功能,而循環依賴又是屬於 IOC 範疇的,所以這兩大功能想要並存,Spring 需要特殊處理。

如何處理的,就是利用了第三級緩存 singletonFactories。

首先,singletonFactories 中存的是某個 beanName 對應的 ObjectFactory,在 bean 的生命週期中,生成完原始對象之後,就會構造一個 ObjectFactory 存入 singletonFactories 中。這個 ObjectFactory 是一個函數式接口,所以支持 Lambda 表達式:() -> getEarlyBeanReference(beanName, mbd, bean)

上面的 Lambda 表達式就是一個 ObjectFactory,執行該 Lambda 表達式就會去執行 getEarlyBeanReference 方法,而該方法如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
 Object exposedObject = bean;
 if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
  for (BeanPostProcessor bp : getBeanPostProcessors()) {
   if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
    exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
   }
  }
 }
 return exposedObject;
}

該方法會去執行 SmartInstantiationAwareBeanPostProcessor 中的 getEarlyBeanReference 方法,而這個接口下的實現類中只有兩個類實現了這個方法,一個是 AbstractAutoProxyCreator,一個是 InstantiationAwareBeanPostProcessorAdapter,它的實現如下:

// InstantiationAwareBeanPostProcessorAdapter
@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
 return bean;
}
// AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
 Object cacheKey = getCacheKey(bean.getClass(), beanName);
 this.earlyProxyReferences.put(cacheKey, bean);
 return wrapIfNecessary(bean, beanName, cacheKey);
}

在整個 Spring 中,默認就只有 AbstractAutoProxyCreator 真正意義上實現了 getEarlyBeanReference 方法,而該類就是用來進行 AOP 的。上文提到的 AnnotationAwareAspectJAutoProxyCreator 的父類就是 AbstractAutoProxyCreator。

那麼 getEarlyBeanReference 方法到底在幹什麼?首先得到一個 cachekey,cachekey 就是 beanName。然後把 beanName 和 bean(這是原始對象)存入 earlyProxyReferences 中 調用 wrapIfNecessary 進行 AOP,得到一個代理對象。

那麼,什麼時候會調用 getEarlyBeanReference 方法呢?回到循環依賴的場景中

從 singletonFactories 根據 beanName 得到一個 ObjectFactory,然後執行 ObjectFactory,也就是執行 getEarlyBeanReference 方法,此時會得到一個 A 原始對象經過 AOP 之後的代理對象,然後把該代理對象放入 earlySingletonObjects 中,注意此時並沒有把代理對象放入 singletonObjects 中,那什麼時候放入到 singletonObjects 中呢?

我們這個時候得來理解一下 earlySingletonObjects 的作用,此時,我們只得到了 A 原始對象的代理對象,這個對象還不完整,因爲 A 原始對象還沒有進行屬性填充,所以此時不能直接把 A 的代理對象放入 singletonObjects 中,所以只能把代理對象放入 earlySingletonObjects,假設現在有其他對象依賴了 A,那麼則可以從 earlySingletonObjects 中得到 A 原始對象的代理對象了,並且是 A 的同一個代理對象。

當 B 創建完了之後,A 繼續進行生命週期,而 A 在完成屬性注入後,會按照它本身的邏輯去進行 AOP,而此時我們知道 A 原始對象已經經歷過了 AOP,所以對於 A 本身而言,不會再去進行 AOP 了,那麼怎麼判斷一個對象是否經歷過了 AOP 呢?會利用上文提到的 earlyProxyReferences,在 AbstractAutoProxyCreator 的 postProcessAfterInitialization 方法中,會去判斷當前 beanName 是否在 earlyProxyReferences,如果在則表示已經提前進行過 AOP 了,無需再次進行 AOP。

對於 A 而言,進行了 AOP 的判斷後,以及 BeanPostProcessor 的執行之後,就需要把 A 對應的對象放入 singletonObjects 中了,但是我們知道,應該是要把 A 的代理對象放入 singletonObjects 中,所以此時需要從 earlySingletonObjects 中得到代理對象,然後入 singletonObjects 中。

整個循環依賴解決完畢。

圖片

總結

至此,總結一下三級緩存:

  1. singletonObjects:緩存經過了完整生命週期的 bean

  2. earlySingletonObjects:緩存未經過完整生命週期的 bean,如果某個 bean 出現了循環依賴,就會提前把這個暫時未經過完整生命週期的 bean 放入 earlySingletonObjects 中,這個 bean 如果要經過 AOP,那麼就會把代理對象放入 earlySingletonObjects 中,否則就是把原始對象放入 earlySingletonObjects,但是不管怎麼樣,就是代理對象,代理對象所代理的原始對象也是沒有經過完整生命週期的,所以放入 earlySingletonObjects 我們就可以統一認爲是未經過完整生命週期的 bean。

  3. singletonFactories:緩存的是一個 ObjectFactory,也就是一個 Lambda 表達式。在每個 Bean 的生成過程中,經過實例化得到一個原始對象後,都會提前基於原始對象暴露一個 Lambda 表達式,並保存到三級緩存中,這個 Lambda 表達式可能用到,也可能用不到,如果當前 Bean 沒有出現循環依賴,那麼這個 Lambda 表達式沒用,當前 bean 按照自己的生命週期正常執行,執行完後直接把當前 bean 放入 singletonObjects 中,如果當前 bean 在依賴注入時發現出現了循環依賴(當前正在創建的 bean 被其他 bean 依賴了),則從三級緩存中拿到 Lambda 表達式,並執行 Lambda 表達式得到一個對象,並把得到的對象放入二級緩存((如果當前 Bean 需要 AOP,那麼執行 lambda 表達式,得到就是對應的代理對象,如果無需 AOP,則直接得到一個原始對象))。

  4. 其實還要一個緩存,就是 earlyProxyReferences,它用來記錄某個原始對象是否進行過 AOP 了。

爲什麼需要二級緩存?

爲什麼需要三級緩存?

我們都知道 Bean 的 aop 動態代理創建時在初始化之後,但是循環依賴的 Bean 如果使用了 AOP。那無法等到解決完循環依賴再創建動態代理,  因爲這個時候已經注入屬性。  所以如果循環依賴的 Bean 使用了 aop.   需要提前創建 aop。

但是需要思考的是動態代理在哪創建?   在實例化後直接創建? 但是我們正常的 Bean 是在初始化創建啊。 所以可以加個判斷如果是循環依賴就實例化後調用,沒有循環依賴就正常在初始化後調用。

二級緩存確實完全可以解決循環依賴的任何情況,包括擴展能力(因爲也可以在這裏調用 BeanPostProcessor, 當然 AOP 也是基於 BeanPostProcessor)。  那要三級緩存幹嘛?  我們只能這樣解釋:Spring 的方法職責都比較單例,一個方法通常只做一件事,    getBean 就是獲取 bean   但是調用創建動態代 BeanPostProcessor  是屬於 create 的過程中的, 如果在這裏明顯代碼比較耦合,閱讀性也不太好。 所以爲了解耦、方法職責單一、方便後期維護,  將調用創建動態代 BeanPostProcessor 放在 createBean 中是最合適不過了, 但是我們判斷當前是否循環依賴還是要寫在 getSingleton 裏面啊,這怎麼辦

三級緩存 存一個函數接口,  函數接口實現 創建動態代理調用 BeanPostProcessor   。  爲了避免重複創建,  調用把返回的動態代理對象或者原實例存儲在二級緩存,     三個緩存完美解決解耦、擴展、性能、代碼閱讀性。

爲什麼 Spring 不能解決構造器的循環依賴?

從流程圖應該不難看出來,在 Bean 調用構造器實例化之前,一二三級緩存並沒有 Bean 的任何相關信息,在實例化之後才放入三級緩存中,因此當 getBean 的時候緩存並沒有命中,這樣就拋出了循環依賴的異常了。

爲什麼多例 Bean 不能解決循環依賴?

我們自己手寫了解決循環依賴的代碼,可以看到,核心是利用一個 map,來解決這個問題的,這個 map 就相當於緩存。

爲什麼可以這麼做,因爲我們的 bean 是單例的,而且是字段注入(setter 注入)的,單例意味着只需要創建一次對象,後面就可以從緩存中取出來,字段注入,意味着我們無需調用構造方法進行注入。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BIZXgRlvQCNjVPhpP-Vreg