死磕Spring之IoC篇 - 單例 Bean 的迴圈依賴處理

月圓吖發表於2021-03-04

該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 Spring 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.1.14.RELEASE

開始閱讀這一系列文章之前,建議先檢視《深入瞭解 Spring IoC(面試題)》這一篇文章

該系列其他文章請檢視:《死磕 Spring 之 IoC 篇 - 文章導讀》

單例 Bean 的迴圈依賴處理

我們先回到《Bean 的建立過程》中的“從快取中獲取單例 Bean”小節,當載入一個 Bean 時,會嘗試從快取(三個 Map)中獲取物件,如果未命中則進入後面建立 Bean 的過程。再來看到《Bean 的建立過程》中的“提前暴露當前 Bean”小節,當獲取到一個例項物件(還未設定屬性和初始化)後,會將這個“早期物件”放入前面的快取中(第三個 Map),這裡暴露的物件實際是一個 ObjectFactory,可以通過它獲取“早期物件”。這樣一來,在後面設定屬性的過程中,如果需要依賴注入其他 Bean,且存在迴圈依賴,那麼上面的快取就避免了這個問題。接下來,將會分析 Spring 處理迴圈依賴的相關過程。

這裡的迴圈依賴是什麼?

迴圈依賴,其實就是迴圈引用,就是兩個或者兩個以上的 Bean 互相引用對方,最終形成一個閉環,如 A 依賴 B,B 依賴 C,C 依賴 A。

例如定義下面兩個物件:

學生類

public class Student {

    private Long id;

    private String name;

    @Autowired
    private ClassRoom classRoom;

    // 省略 getter、setter
}

教室類

public class ClassRoom {

    private String name;

    @Autowired
    private Collection<Student> students;
    
    // 省略 getter、setter
}

當載入 Student 這個物件時,需要注入一個 ClassRoom 物件,就需要去載入 ClassRoom 這個物件,此時又要去依賴注入所有的 Student 物件,這裡的 Student 和 ClassRoom 就存在迴圈依賴,那麼一直這樣迴圈下去,除非有終結條件

Spring 只處理單例 Bean 的迴圈依賴,原型模式的 Bean 如果存在迴圈依賴直接丟擲異常單例 Bean 的迴圈依賴的場景有兩種:

  • 構造器注入出現迴圈依賴
  • 欄位(或 Setter)注入出現迴圈依賴

對於構造器注入出現快取依賴,Spring 是無法解決的,因為當前 Bean 還未例項化,無法提前暴露物件,所以只能丟擲異常,接下來我們分析的都是欄位(或 Setter)注入出現迴圈依賴的處理

迴圈依賴的處理

1. 嘗試從快取中獲取單例 Bean

可以先回到《Bean 的建立過程》中的“從快取中獲取單例 Bean”小節,在獲取一個 Bean 過程中,首先會從快取中嘗試獲取物件,對應程式碼段:

// AbstractBeanFactory#doGetBean(...) 方法
Object sharedInstance = getSingleton(beanName);

// DefaultSingletonBeanRegistry.java
public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}
// DefaultSingletonBeanRegistry.java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // <1> **【一級 Map】**從單例快取 `singletonObjects` 中獲取 beanName 對應的 Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // <2> 如果**一級 Map**中不存在,且當前 beanName 正在建立
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // <2.1> 對 `singletonObjects` 加鎖
        synchronized (this.singletonObjects) {
            // <2.2> **【二級 Map】**從 `earlySingletonObjects` 集合中獲取,裡面會儲存從 **三級 Map** 獲取到的正在初始化的 Bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            // <2.3> 如果**二級 Map** 中不存在,且允許提前建立
            if (singletonObject == null && allowEarlyReference) {
                // <2.3.1> **【三級 Map】**從 `singletonFactories` 中獲取對應的 ObjectFactory 實現類
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                // 如果從**三級 Map** 中存在對應的物件,則進行下面的處理
                if (singletonFactory != null) {
                    // <2.3.2> 呼叫 ObjectFactory#getOject() 方法,獲取目標 Bean 物件(早期半成品)
                    singletonObject = singletonFactory.getObject();
                    // <2.3.3> 將目標物件放入**二級 Map**
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // <2.3.4> 從**三級 Map**移除 `beanName`
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    // <3> 返回從快取中獲取的物件
    return singletonObject;
}

這裡的快取指的就是上面三個 Map 物件:

  • singletonObjects(一級 Map):裡面儲存了所有已經初始化好的單例 Bean,也就是會儲存 Spring IoC 容器中所有單例的 Spring Bean
  • earlySingletonObjects(二級 Map),裡面會儲存從 三級 Map 獲取到的正在初始化的 Bean
  • singletonFactories(三級 Map),裡面儲存了正在初始化的 Bean 對應的 ObjectFactory 實現類,呼叫其 getObject() 方法返回正在初始化的 Bean 物件(僅例項化還沒完全初始化好)

過程如下:

  1. 【一級 Map】從單例快取 singletonObjects 中獲取 beanName 對應的 Bean
  2. 如果一級 Map中不存在,且當前 beanName 正在建立
    1. singletonObjects 加鎖
    2. 【二級 Map】earlySingletonObjects 集合中獲取,裡面會儲存從 三級 Map 獲取到的正在初始化的 Bean
    3. 如果二級 Map 中不存在,且允許提前建立
      1. 【三級 Map】singletonFactories 中獲取對應的 ObjectFactory 實現類,如果從三級 Map 中存在對應的物件,則進行下面的處理
      2. 呼叫 ObjectFactory#getOject() 方法,獲取目標 Bean 物件(早期半成品)
      3. 將目標物件放入二級 Map
      4. 三級 Map移除 beanName
  3. 返回從快取中獲取的物件

2. 提前暴露當前 Bean

回到《Bean 的建立過程》中的“提前暴露當前 Bean”小節,在獲取到例項物件後,如果是單例模式,則提前暴露這個例項物件,對應程式碼段:

// AbstractAutowireCapableBeanFactory#doCreateBean(...) 方法
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
// <3> 提前暴露這個 `bean`,如果可以的話,目的是解決單例模式 Bean 的迴圈依賴注入
// <3.1> 判斷是否可以提前暴露
boolean earlySingletonExposure = (mbd.isSingleton() // 單例模式
        && this.allowCircularReferences // 允許迴圈依賴,預設為 true
        && isSingletonCurrentlyInCreation(beanName)); // 當前單例 bean 正在被建立,在前面已經標記過
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    /**
     * <3.2>
     * 建立一個 ObjectFactory 實現類,用於返回當前正在被建立的 `bean`,提前暴露,儲存在 `singletonFactories` (**三級 Map**)快取中
     *
     * 可以回到前面的 {@link AbstractBeanFactory#doGetBean#getSingleton(String)} 方法
     * 載入 Bean 的過程會先從快取中獲取單例 Bean,可以避免單例模式 Bean 迴圈依賴注入的問題
     */
    addSingletonFactory(beanName,
            // ObjectFactory 實現類
            () -> getEarlyBeanReference(beanName, mbd, bean));
}

如果是單例模式允許迴圈依賴(預設為 true)、當前單例 Bean 正在被建立(前面已經標記過),則提前暴露

這裡會先通過 Lambda 表示式建立一個 ObjectFactory 實現類,如下:

// AbstractAutowireCapableBeanFactory.java
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() // RootBeanDefinition 不是使用者定義的(由 Spring 解析出來的)
        && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

入參 bean 為當前 Bean 的例項物件(未初始化),這個實現類允許通過 SmartInstantiationAwareBeanPostProcessor 對這個提前暴露的物件進行處理,最終會返回這個提前暴露的物件。注意,這裡也可以返回一個代理物件。

有了這個 ObjectFactory 實現類後,就需要往快取中存放了,如下:

// DefaultSingletonBeanRegistry.java
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

可以看到會將這個 ObjectFactory 往 singletonFactories (三級 Map)中存放,到這裡對於 Spring 對單例 Bean 迴圈依賴的處理是不是就非常清晰了

3. 快取單例 Bean

在完全初始化好一個單例 Bean 後,會快取起來,如下:

// DefaultSingletonBeanRegistry.java
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

singletonObjects(一級 Map)存放當前單例 Bean,同時從 singletonFactories(三級 Map)和 earlySingletonObjects(二級 Map)中移除

總結

Spring 只處理單例 Bean 的欄位(或 Setter)注入出現迴圈依賴,對於構造器注入出現的迴圈依賴會直接丟擲異常。還有就是如果是通過 denpends-on 配置的依賴出現了迴圈,也會丟擲異常,所以我覺得這裡的“迴圈依賴”換做“迴圈依賴注入”是不是更合適一點

Spring 處理迴圈依賴的解決方案如下:

  • Spring 在建立 Bean 的過程中,獲取到例項物件後會提前暴露出去,生成一個 ObjectFactory 物件,放入 singletonFactories(三級 Map)中
  • 在後續設定屬性過程中,如果出現迴圈,則可以通過 singletonFactories(三級 Map)中對應的 ObjectFactory#getObject() 獲取這個早期物件,避免再次初始化

問題一:為什麼需要上面的 二級 Map

因為通過 三級 Map獲取 Bean 會有相關 SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference 的處理,避免重複處理,處理後返回的可能是一個代理物件

例如在迴圈依賴中一個 Bean 可能被多個 Bean 依賴, A -> B(也依賴 A) -> C -> A,當你獲取 A 這個 Bean 時,後續 B 和 C 都要注入 A,沒有上面的 二級 Map的話,三級 Map 儲存的 ObjectFactory 實現類會被呼叫兩次,會重複處理,可能出現問題,這樣做在效能上也有所提升

問題二:為什麼不直接呼叫這個 ObjectFactory#getObject() 方法放入 二級Map中,而需要 三級 Map

對於沒有不涉及到 AOP 的 Bean 確實可以不需要 singletonFactories(三級 Map),但是 Spring AOP 就是 Spring 體系中的一員,如果沒有singletonFactories(三級 Map),意味著 Bean 在例項化後就要完成 AOP 代理,這樣違背了 Spring 的設計原則。Spring 是通過 AnnotationAwareAspectJAutoProxyCreator 這個後置處理器在完全建立好 Bean 後來完成 AOP 代理,而不是在例項化後就立馬進行 AOP 代理。如果出現了迴圈依賴,那沒有辦法,只有給 Bean 先建立代理物件,但是在沒有出現迴圈依賴的情況下,設計之初就是讓 Bean 在完全建立好後才完成 AOP 代理。

相關文章