小白都能看懂的 Spring 原始碼揭祕之依賴注入(DI)原始碼分析

雙子孤狼發表於2021-11-27

前言

在面試中,經常被問到 SpringIOCDI(依賴注入),很多人會覺得其實 IOC 就是 DI,但是嚴格上來說這兩個其實並不等價,因為 IOC 注重的是存,而依賴注入注重的是取,實際上我們除了依賴注入還有另一種取的方式那就是依賴查詢,可以把依賴注入和依賴查詢都理解成 IOC 的實現方式。

依賴注入的入口方法

上一篇我們講到了 IOC 的初始化流程,不過回想一下,是不是感覺少了點什麼?IOC 的初始化只是將 Bean 的相關定義檔案進行了儲存,但是好像並沒有進行初始化,而且假如一個類裡面引用了另一個類,還需要進行賦值操作,這些我們都沒有講到,這些都屬於我們今天講解的依賴注入。

預設情況下依賴注入只有在呼叫 getBean() 的時候才會觸發,因為 Spring 當中預設是懶載入,除非明確指定了配置 lazy-init=false,或者使用註解 @Lazy(value = false),才會主動觸發依賴注入的過程。

依賴注入流程分析

在分析流程之前,我們還是看下面這個例子:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
applicationContext.getBean("myBean");
applicationContext.getBean(MyBean.class);

我們的分析從 getBean() 方法開始。

AbstractBeanFactory#getBean

在前面我們講到了一個頂層介面 BeanFactory 中定義了操作 Bean 的相關方法,而 ApplicationContext 就間接實現了 BeanFactory 介面,所以其呼叫 getBean() 方法會進入到 AbstractBeanFactory 類中的方法:

可以看到,這裡呼叫之後直接就看到 doXXX 方法了,

AbstractBeanFactory#doGetBean

進入 doGetBean 這個方法進去之後呢,會有一系列判斷,主要有以下幾個方面:

  1. 當前類是不是單例,如果是的話而且單例已經被建立好,那麼直接返回。
  2. 當前原型 bean 是否正在建立,如果是的話就認為產生了迴圈依賴,丟擲異常。
  3. 手動通過 @DependsOn 註解或者 xml 配置中顯式指定的依賴是否存在迴圈依賴問題,存在的話直接丟擲異常。
  4. 當前的 BeanFactory 中的 beanDefinitionMap 容器中是否存在當前 bean 對應的 BeanDefinition,如果不存在則會去父類中繼續獲取,然後重新呼叫其父類對應的 getBean() 方法。

經過一系列的判斷之後,會判斷當前 Bean 是原型還是單例,然後走不同的處理邏輯,但是不論是原型還是單例物件,最終其都會呼叫 AbstractAutowireCapableBeanFactory 類中的 createBean 方法進行建立 bean 例項

AbstractAutowireCapableBeanFactory#createBean

這個方法裡面會先確認當前 bean 是否可以被例項化,然後會有兩個主要邏輯:

  1. 是否返回一個代理物件,是的話返回代理物件。
  2. 直接建立一個 bean 物件例項。

這裡面第一個邏輯我們不重點分析,在這裡我們主要還是分析第二個邏輯,如何建立一個 bean 例項:

AbstractAutowireCapableBeanFactory#doCreateBean

這又是一個以 do 開頭的方法,說明這裡面會真正建立一個 bean 例項物件,在分析這個方法之前,我們先自己來設想一下,假如是我們自己來實現,在這個方法需要做什麼操作?

在這個方法中,最核心的就是做兩件事:

  1. 例項化一個 bean 物件。
  2. 遍歷當前物件的屬性,如果需要則注入其他 bean,如果發現需要注入的 bean 還沒有例項化,則需要先進行例項化。

建立 bean 例項(AbstractAutowireCapableBeanFactory#createBeanInstance)

doCreateBean 方法中,會呼叫 createBeanInstance 方法來例項化一個 bean。這裡面也會有一系列邏輯去處理,比如判斷這個類是不是具有 public 許可權等等,但是最終還是會通過反射去呼叫當前 bean 的無參構造器或者有參構造器來初始化一個 bean 例項,然後再將其封裝成一個 BeanWrapper 物件返回。

不過如果這裡呼叫的是一個有參構造器,而這個引數也是一個 bean,那麼也會觸發先去初始化引數中的 bean,初始化 bean 例項除了有參構造器形式之外,相對還是比較容易理解,我們就不過多去分析細節,主要重點是分析依賴注入的處理方式。

依賴注入(AbstractAutowireCapableBeanFactory#populateBean)

在上面建立 Bean 例項完成的時候,我們的物件並不完整,因為還只是僅僅建立了一個例項,而例項中的注入的屬性卻並未進行填充,所以接下來就還需要完成依賴注入的動作,那麼在依賴注入的時候,如果發現需要注入的物件尚未初始化,還需要觸發注入物件的初始化動作,同時在注入的時候也會分為按名稱注入和按型別注入(除此之外還有構造器注入等方式):

我們在依賴注入的時候最常用的是 @Autowired@Resource 兩個註解,而這連個註解的區別之一就是一個按照型別注入,另一個優先按照名稱注入(沒有找到名稱就會按照型別注入),但是實際上這兩個註解都不會走上面的按名稱注入和按型別注入的邏輯,而是都是通過對應的 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 兩個 Bean 的後置處理器來實現的,而且 @Resource 註解當無法通過名稱找到 Bean 時也會根據型別去注入,在這裡具體的處理細節我們就不過多展開分析,畢竟我們今天的目標是分析整個依賴注入的流程,如果過多糾結於這些分支細節,反而會使大家更加困惑。

上面通過根據名稱或者根據屬性解析出依賴的屬性之後,會將其封裝到物件 MutablePropertyValues(即:PropertyValues 介面的實現類) 中,最後會再呼叫 applyPropertyValues() 方法進行真正的屬性注入:

處理完之後,最後會再呼叫 applyPropertyValues() 方法進行真正的屬性注入。

迴圈依賴問題是怎麼解決的

依賴注入成功之後,整個 DI 流水就算結束了,但是有一個問題我們沒有提到,那就是迴圈依賴問題,迴圈依賴指的是當我們有兩個類 AB,其中 A 依賴 BB 又依賴了 A,或者多個類也一樣,只要形成了一個環狀依賴那就屬於迴圈依賴,比如下面的配置就是一個典型的迴圈依賴配置:

<bean id="classA" class="ClassA" p:beanB-ref="classB"/>
<bean id="classB" class="ClassB" p:beanA-ref="classA"/>

而我們前面講解 Bean 的初始化時又講到了當我們初始化 A 的時候,如果發現其依賴了 B,那麼會觸發 B 的初始化,可是 B 又依賴了 A,導致其無法完成初始化,這時候我們應該怎麼解決這個問題呢?

在瞭解 Spring 中是如何解決這個問題之前,我們自己先想一下,如果換成我們來開發,我們會如何解決這個問題呢?其實方法也很簡單,大家應該都能想到,那就是當我們把 Bean 初始化之後,在沒有注入屬性之前,就先快取起來,這樣,就相當於快取了一個半成品 Bean 來提前暴露出來供注入時使用。

不過解決迴圈依賴也是有前提的,以下三種情形就無法解決迴圈依賴問題:

  • 構造器注入產生的迴圈依賴。通過構造器注入產生的迴圈依賴會在第一步初始化就失敗,所以也無法提前暴露出來。
  • 非單例模式 Bean,因為只有在單例模式下才會對 Bean 進行快取。
  • 手動設定了 allowCircularReferences=false,則表示不允許迴圈依賴。

而在 Spring 當中處理迴圈依賴也是這個思路,只不過 Spring 中為了考慮設計問題,並非僅僅只採用了一個快取,而是採用了三個快取,這也就是面試中經常被問到的迴圈依賴相關的三級快取問題(這裡我個人意見是不太認同三級快取這種叫法的,畢竟這三個快取是在同一個類中的三個不同容器而已,並沒有層級關係,這一點和 MyBatis 中使用到的兩級快取還是有區別的,不過既然大家都這麼叫,我們一個凡人也就隨波逐流了)。

Spring 中解決迴圈依賴的三級快取

如下圖所示,在 Spring 中通過以下三個容器(Map 集合)來快取單例 Bean

  • singletonObjects

這個容器用來儲存成品的單例 Bean,也就是所謂的第一級快取。

  • earlySingletonObjects

這個用來儲存半成品的單例 Bean,也就是初始化之後還沒有注入屬性的 Bean,也就是所謂的第二級快取。

  • singletonFactories

儲存的是 Bean 工廠物件,可以用來生成半成品的 Bean,這也就是所謂的三級快取。

為什麼需要三級快取才能解決迴圈依賴問題

看了上面的三級快取,不知道大家有沒有疑問,因為第一級快取和第二級快取都比較好理解,一個成品一個半成品,這個都沒什麼好說的,那麼為什麼又需要第三級快取呢,這又是出於什麼考慮呢?

回答這個問題之前,我梳理了有迴圈依賴和沒有迴圈依賴兩種場景的流程圖來進行對比分析:

沒有迴圈依賴的建立 Bean A 流程:

有迴圈依賴的建立 Bean A 流程(A 依賴 BB 依賴 A):

對比這兩個流程其實有一個比較大的區別,我在下面這個有迴圈依賴的注入流程標出來了,那就是在沒有迴圈依賴的情況下一個類是會先完成屬性的注入,才會呼叫 BeanPostProcessor 處理器來完成一些後置處理,這也比較符合常理也符合 Bean 的生命週期,而一旦有迴圈依賴之後,就不得不把 BeanPostProcessor 提前進行處理,這樣在一定程度上就破壞了 Bean 的生命週期。

但是到這裡估計大家還是有疑問,因為這並不能說明一定要使用三級快取的理由,那麼這裡就涉及到了 Spring Aop 了,當我們使用了 Spring Aop 之後,那麼就不能使用原生物件而應該換成用代理物件,那麼代理物件是什麼時候建立的呢?

實際上 Spring Aop 的代理物件也是通過 BeanPostProcessor 來完成的,下圖就是一個使用了 Spring Aop 的例項物件所擁有的所有 BeanPostProcessor

在這裡有一個 AnnotationAwareAspectJAutoProxyCreator 後置處理器,也就是 Spring Aop 是通過後置處理器來實現的。

知道了這個問題,我們再來確認另一個問題,Spring 中為了解決迴圈依賴問題,在初始化 Bean 之後,還未注入屬性之前就會將單例 Bean 先放入快取,但是這時候也不能直接將原生物件放入二級快取,因為這樣的話如果使用了 Spring Aop 就會出問題,其他類可能會直接注入原生物件而非代理物件。

那麼這裡我們能不能直接就建立代理物件存入二級快取呢?答案是可以,但是直接建立代理物件就必須要呼叫 BeanPostProcessor 後置處理器,這樣就使得呼叫後置處理器在屬性注入之前了,違背了 Bean 宣告週期。

在提前暴露單例之前,Spring 並不知道當前 Bean 是否有迴圈依賴,所以為了儘可能的延緩 BeanPostProcessor 的呼叫,Spring 才採用了三級快取,存入一個 Objectactory 物件,並不建立,而是當發生了迴圈依賴的時候,採取三級快取獲取到三級快取來建立物件,因為發生了迴圈依賴的時候,不得不提前呼叫 BeanPostProcessor 來完成例項的初始化。

我們看下加入三級快取的邏輯:

加入三級快取是將一個 lambda 表示式存進去,目的就是延緩建立,最後發生迴圈依賴的時候,從一二級快取都無法獲取到 Bean 的時候,會獲取三級快取,也就是呼叫 ObjectFactorygetObject() 方法,而這個方法實際上就是呼叫下面的 getEarlyBeanReference ,這裡就會提前呼叫 BeanPostProcessor 來完成例項的建立。

總結

本文主要分析了 Spinrg 依賴注入的主要流程,而依賴注入中產生的迴圈依賴問題又是其中比較複雜的處理方式,在本文分析過程中略去了詳細的邏輯,只關注了主流程。本文主要是結合了網上一些資料然後自己 debug 除錯過程得到的自己對 Spring 依賴注入的一個主要流程,如果有理解錯誤的地方,歡迎留言交流。

相關文章