Spring原始碼:Spring 如何解決 Bean 的迴圈依賴

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第30天,點選檢視活動詳情

1. 什麼是迴圈依賴

一個專案,隨著業務的擴充套件,功能的迭代,必定會存在某些類和類之間的相互呼叫,比如 serviceA 呼叫了serviceB 的某個方法,同時 serviceB 也呼叫了serviceA 中的某個方法,從而形成了一種迴圈依賴的關係。

image-20211104142220355

假如 Spring 容器啟動後,先會例項化 A,但在 A 中又注入了 B,然後就會去例項化 B,但在例項化 B 的時候又發現 B 中注入了 A,於是又繼續迴圈,後果就是導致程式 OOM。不過一般沒有十年腦血栓應該都寫不出來,哈哈哈。

2. 回顧 Bean 的建立流程

在上篇文章中講到了 Bean 的建立流程,大致呼叫鏈路如下:

image-20211105100529775

因此,Spring在建立完整的 Bean 的過程中分為三步:

  1. 例項化,即 new 了一個物件;

    對應方法:AbstractAutowireCapableBeanFactory#createBeanInstance()

  2. 屬性注入,為第 1 步中 new 出來的物件中的屬性進行填充(依賴注入);

    對應方法:AbstractAutowireCapableBeanFactory#populateBean()

  3. 初始化,執行 Aware 介面方法,init-method以及實現了 InitializingBean 介面的方法。

    對應方法:AbstractAutowireCapableBeanFactory#initializeBean()

由於之前關於 Bean 的建立流程相對詳細,所以某些程式碼就不在這裡細講。

3. Spring 如何解決迴圈依賴

針對迴圈依賴的情況,Spring 已經幫我們解決了單例的迴圈依賴問題。

栗子

ServiceA

java @Data @Component public class ServiceA { ​    @Autowired    private ServiceB serviceB; }

ServiceB

java @Data @Component public class ServiceB { ​    @Autowired    private ServiceA serviceA; }

啟動容器會建立例項:

image-20211104152043311

需要注意的是,Spring 只提供了在單例無參建構函式情況下的解決方式(在原始碼中也能體現),有參建構函式的加 @Autowired 的方式迴圈依賴是直接報錯的,多例的迴圈依賴也是直接報錯的。

Spring 為了解決單例模式下的迴圈依賴問題,使用了三級快取,通過上圖 Bean 的建立流程來分析它是如何解決的。

3.1 三級快取

什麼是三級快取,其實就是Spring中在例項化的時候定義了三個不同的Map,如果我們要解決迴圈依賴,其核心就是提前拿到提前暴露的物件,儘管它還沒有初始化。

| 名稱 | 描述 | | --------------------- | ------------------------------------------- | | singletonObjects | 一級快取,用於存放完全初始化好的 Bean | | earlySingletonObjects | 二級快取,存放提前暴露的Bean,Bean 是不完整的,未完成屬性注入和執行初始化方法 | | singletonFactories | 三級快取,單例Bean工廠,二級快取中儲存的就是從這個工廠中獲取到的物件 |

在原始碼DefaultSingletonBeanRegistry中的體現:

image-20211105102202824

3.2 單例迴圈依賴

單例迴圈依賴,也就是上面例子中演示的情況,按照例項建立過程分析,當我們建立一個物件 A 時,必定會呼叫getBean()方法,而該方法存在 2 種情況:

  1. 快取中沒有,需要建立新的Bean;
  2. 從快取中獲取到已經被建立的物件。

從快取中獲取物件,呼叫getSingleton()方法如下:

image-20211105145239171

當第一次建立 A 時,必然一、二、三級快取中都沒有資料,然後進入單例情況下建立 Bean,如下:

image-20211105145622490

重新呼叫過載的getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法,如下:

image-20211105152137928

可以看到先會把 A 新增到一個表示正在建立的 Set 集合中,而傳入的函式 singletonFactory,實際呼叫的就是 createBean()方法,在該方法中,進入真正做事的方法 doCreateBean(),在該方法中會先呼叫createBeanInstance()進行例項化,具體過程在上篇文章中已經講過,這裡不在贅述。

通過 debug 模式可以看到,此時的 A 是一個半成品物件,因為還沒有屬性填充,如下:

image-20211105154323147

例項化之後應該就是屬性填充和初始化,但在這之前 Spring 為迴圈依賴做了處理,有這麼一個判斷:

java boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&      isSingletonCurrentlyInCreation(beanName));

第一個條件預設就是單例的,allowCircularReferences 預設值也是 true,isSingletonCurrentlyInCreation也是 true 因為 A 還沒有完全例項化,因此還在 Set 容器中,所以earlySingletonExposure = true,即會進入下面的程式碼:

image-20211105155101470

繼續進入addSingletonFactory方法:

image-20211105155203223

可以看到,就是把建立的半成品例項 A 放入了三級快取,但這裡只是添加了一個工廠,通過這個工廠ObjectFactorygetObject方法可以得到一個物件,而這個物件實際上就是通過getEarlyBeanReference這個方法建立的。那麼,什麼時候會去呼叫這個工廠的getObject方法呢?這個時候就要到建立 B 的流程了。

什麼時候去建立 B 呢?

因為在 A 例項化的過程中會收集被 @Autowried 注入的屬性,所以 A 在例項化之後就要去屬性填充注入例項 B,而這個屬性的注入過程中就會去呼叫 B 的 getBean方法,而這時 B 在快取中也是沒有的,所以又會走一遍 A 的流程。

debug 如下:

image-20211105165353632

這個時候的 B 也是一個半成品例項的狀態,因為還沒有進行屬性填充和初始化,所以 B 接著往下走,開始進行屬性填充 populateBean

而 B 又迴圈依賴了 A,所以會再次呼叫 getBean 方法去獲取例項 A,在上面的分析中我們已經知道此時的 A 已經被建立提前暴露在三級快取中,儘管此時的 A 還沒有初始化,所以當呼叫 getBean 的時候又會進入getSingleton去獲取 Bean。

image-20211105170659279

現在來回答上面的問題,什麼時候會呼叫這個 getObject,沒錯,就是在這裡呼叫,呼叫的方法就是getEarlyBeanReference,看看這個方法:

image-20211105171713605

實際上就是對 BeanPostProcessor 的型別做處理,然後呼叫SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(),而該方法的實現只有 2 個,如下:

image-20211105172110422

進入這兩個實現類,InstantiationAwareBeanPostProcessorAdapter 明顯直接返回了這個Bean,相當於什麼都沒做,也就是說只用二級快取就能解決了,為什麼還要從三級快取中在拿出來?這就要看另外一個實現類了。

image-20211105172204933

而另外一個就是 AbstractAutoProxyCreator,從名字就可以知道是用來做 AOP 代理的,這也是為什麼需要三級快取而不是二級快取,因為有的時候我們可能需要的是代理Bean,而不是真正的例項Bean。

為什麼是三級快取,不是二級快取,實際二級快取就是可以解決迴圈依賴的問題,但是,Spring 還有一個 aop,當有 bean 需要例項化,在例項化的最終階段會檢查該物件是否有被代理,若被代理則需要生成代理物件,所以,Spring 將三級快取中的 ObjectFactory 設計成返回代理物件,那如果有多個物件B,C,D等等依賴 A,ObjectFactory就不必要生成多個代理物件,只需要從二級快取中獲取之前生成的代理物件即可,所以需要三級快取。

image-20211105172420919

但這裡暫不考慮代理的情況,也就是說這裡直接返回的就是之前創建出來的半成品例項 A 並且從三級快取放到了二級快取中,並把 A 注入到 B 中,因此目前 B 的狀態為:

B 已經完全例項化,即屬性填充和初始化都已完成,此時再回到getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法:

image-20211105185330738

那麼 A 的狀態呢?在 B 完全例項化之後,B 中已經有了 A 的引用,debug如下:

image-20211105185616768

可以看到 B 中有了 A,但例項 A 中的 B = null,這是因為 A 還沒有完成屬性填充(因為被 B 給打斷了),而這個時候 A 再去進行屬性填充 B,當呼叫 B 的 getObject 的時候發現一級快取中已經有了 B 的例項,因此直接把 B 注入到 A 中,然後完成了 A 的例項化,同樣在進入getSingleton方法把例項 A 新增到一級快取中,從而解決了迴圈依賴。

3.3 單例建構函式迴圈依賴

除了上面的注入方式,還有構造器的注入方式,如下:

A

java @Data @Component public class ServiceA { ​    private ServiceB serviceB; ​ ​    public ServiceA(ServiceB serviceB) {        this.serviceB = serviceB;   } }

B

java @Data @Component public class ServiceB { ​    private ServiceA serviceA; ​    public ServiceB(ServiceA serviceA) {        this.serviceA = serviceA;   } }

當啟動容器的時候發現直接報錯了:

image-20211105191754605

這是為什麼呢?

之前講過構造器的注入方式,會在例項化 A 的時候會去例項化 B,而此時都還沒有新增到快取中去,所以會報錯。

image-20211105195248513

3.4 多例迴圈依賴

對於多例的情況,Spring 直接丟擲了異常,如下:

image-20211105201114816

可以看到在doGetBean中,Spring直接是丟擲了異常,我們看看這個多例的情況是在哪裡賦值的。

還是在doGetBean中:

image-20211105201332091

可以看到在createBean之前先呼叫了beforePrototypeCreation方法,即把這個 Bean 放入到多例的正在被建立的 Bean 的一個ThreadLocal 中去。

image-20211105201610368

如果我們按照單例的模式來走,當 A 建立的時候,把 A 放進ThreadLocal中去,然後去建立 B,由於B依賴於A,又會去呼叫 getBean,此時發現ThreadLocal中已經存在 A 了,於是就丟擲異常。

至於為什麼,想想其實可以理解,多例情況下每次的三級快取都是不一樣的,我找哪一個呢?所以找不到對應的三級快取,校驗的時候肯定會發生異常。

4. 總結

簡單來說,Spring解決了單例情況下的迴圈依賴,在不考慮AOP的情況下,大致步驟如下:

  1. A 例項化時依賴 B,於是 A 先放入三級快取,然後去例項化 B;

  2. B 進行例項化,把自己放到三級快取中,然後發現又依賴於 A,於是先去查詢快取,但一級二級都沒有,在三級快取中找到了。

    • 然後把三級快取裡面的 A 放到二級快取,然後刪除三級快取中的 A;
    • 然後 B 注入半成品的例項 A 完成例項化,並放到一級快取中;
  3. 然後回到 A,因為 B 已經例項化完成並存在於一級快取中,所以直接從一級快取中拿取,然後注入B,完成例項化,再將自己新增到一級快取中。

對於存在 AOP 代理的迴圈依賴,下次再說。

5. 參考