前言
在面試中,經常被問到 Spring
的 IOC
和 DI
(依賴注入),很多人會覺得其實 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
這個方法進去之后呢,會有一系列判斷,主要有以下幾個方面:
- 當前類是不是單例,如果是的話而且單例已經被創建好,那么直接返回。
- 當前原型
bean
是否正在創建,如果是的話就認為產生了循環依賴,拋出異常。 - 手動通過
@DependsOn
注解或者xml
配置中顯式指定的依賴是否存在循環依賴問題,存在的話直接拋出異常。 - 當前的
BeanFactory
中的beanDefinitionMap
容器中是否存在當前bean
對應的BeanDefinition
,如果不存在則會去父類中繼續獲取,然后重新調用其父類對應的getBean()
方法。
經過一系列的判斷之后,會判斷當前 Bean
是原型還是單例,然后走不同的處理邏輯,但是不論是原型還是單例對象,最終其都會調用 AbstractAutowireCapableBeanFactory
類中的 createBean
方法進行創建 bean
實例
AbstractAutowireCapableBeanFactory#createBean
這個方法里面會先確認當前 bean
是否可以被實例化,然后會有兩個主要邏輯:
- 是否返回一個代理對象,是的話返回代理對象。
- 直接創建一個
bean
對象實例。
這里面第一個邏輯我們不重點分析,在這里我們主要還是分析第二個邏輯,如何創建一個 bean
實例:
AbstractAutowireCapableBeanFactory#doCreateBean
這又是一個以 do
開頭的方法,說明這里面會真正創建一個 bean
實例對象,在分析這個方法之前,我們先自己來設想一下,假如是我們自己來實現,在這個方法需要做什么操作?
在這個方法中,最核心的就是做兩件事:
- 實例化一個
bean
對象。 - 遍歷當前對象的屬性,如果需要則注入其他
bean
,如果發現需要注入的bean
還沒有實例化,則需要先進行實例化。
創建 bean 實例(AbstractAutowireCapableBeanFactory#createBeanInstance)
在 doCreateBean
方法中,會調用 createBeanInstance
方法來實例化一個 bean
。這里面也會有一系列邏輯去處理,比如判斷這個類是不是具有 public
權限等等,但是最終還是會通過反射去調用當前 bean
的無參構造器或者有參構造器來初始化一個 bean
實例,然后再將其封裝成一個 BeanWrapper
對象返回。
不過如果這里調用的是一個有參構造器,而這個參數也是一個 bean
,那么也會觸發先去初始化參數中的 bean
,初始化 bean
實例除了有參構造器形式之外,相對還是比較容易理解,我們就不過多去分析細節,主要重點是分析依賴注入的處理方式。
依賴注入(AbstractAutowireCapableBeanFactory#populateBean)
在上面創建 Bean
實例完成的時候,我們的對象並不完整,因為還只是僅僅創建了一個實例,而實例中的注入的屬性卻並未進行填充,所以接下來就還需要完成依賴注入的動作,那么在依賴注入的時候,如果發現需要注入的對象尚未初始化,還需要觸發注入對象的初始化動作,同時在注入的時候也會分為按名稱注入和按類型注入(除此之外還有構造器注入等方式):
我們在依賴注入的時候最常用的是 @Autowired
和 @Resource
兩個注解,而這連個注解的區別之一就是一個按照類型注入,另一個優先按照名稱注入(沒有找到名稱就會按照類型注入),但是實際上這兩個注解都不會走上面的按名稱注入和按類型注入的邏輯,而是都是通過對應的 AutowiredAnnotationBeanPostProcessor
和 CommonAnnotationBeanPostProcessor
兩個 Bean
的后置處理器來實現的,而且 @Resource
注解當無法通過名稱找到 Bean
時也會根據類型去注入,在這里具體的處理細節我們就不過多展開分析,畢竟我們今天的目標是分析整個依賴注入的流程,如果過多糾結於這些分支細節,反而會使大家更加困惑。
上面通過根據名稱或者根據屬性解析出依賴的屬性之后,會將其封裝到對象 MutablePropertyValues
(即:PropertyValues
接口的實現類) 中,最后會再調用 applyPropertyValues() 方法進行真正的屬性注入:
處理完之后,最后會再調用 applyPropertyValues()
方法進行真正的屬性注入。
循環依賴問題是怎么解決的
依賴注入成功之后,整個 DI
流水就算結束了,但是有一個問題我們沒有提到,那就是循環依賴問題,循環依賴指的是當我們有兩個類 A
和 B
,其中 A
依賴 B
,B
又依賴了 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
依賴 B
,B
依賴 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
的時候,會獲取三級緩存,也就是調用 ObjectFactory
的 getObject()
方法,而這個方法實際上就是調用下面的 getEarlyBeanReference
,這里就會提前調用 BeanPostProcessor
來完成實例的創建。
總結
本文主要分析了 Spinrg
依賴注入的主要流程,而依賴注入中產生的循環依賴問題又是其中比較復雜的處理方式,在本文分析過程中略去了詳細的邏輯,只關注了主流程。本文主要是結合了網上一些資料然后自己 debug
調試過程得到的自己對 Spring
依賴注入的一個主要流程,如果有理解錯誤的地方,歡迎留言交流。