Spring IOC 原理深層解析

CryFace發表於2020-08-09

1 Spring IOC概念認識

1.1 區別IOC與DI

首先我們要知道IOC(Inverse of Control:控制反轉)是一種設計思想,就是 將原本在程式中手動建立物件的控制權,交由Spring框架來管理。這並非Spring特有,在其他語言裡面也有體現。IOC容器是Spring用來實現IOC的載體, IOC容器實際上就是個Map(key,value),Map 中存放的是各種物件。

或許是IOC不夠開門見山,Martin Fowler提出了DI(dependency injection)來替代IOC,即讓呼叫類對某一介面實現類的依賴關係由第三方(容器或協作類)注入,以移除呼叫類對某一介面實現類的依賴。

所以我們要區別IOC與DI,簡單來說IOC的主要實現方式有兩種:

  • 依賴查詢
  • 依賴注入

我們DI就是依賴注入,也就是IOC的一種可取的實現方式!對兩個概念總結以下:

  • IOC (Inversion of control ) 控制反轉/反轉控制。是站在物件的角度,物件例項化以及管理的許可權(反轉)交給了容器。
  • DI (Dependancy Injection)依賴注入。是站在容器的角度,容器會把物件依賴的其他物件注入(送進去)。例如:物件A 例項化過程中因為宣告瞭一個B型別的屬性,那麼就需要容器把B物件注入到A中。

通過使用IOC容器可以對我們的物件注入依賴(DI),實現控制反轉!

1.2 IOC解決的問題

通過上面的介紹,我們大概理解了IOC的概念,也知道它的作用。那麼也會有疑惑,為什麼需要依賴反轉呢,有什麼好處,解決了什麼問題?

簡單來說,IOC 容器就像是一個工廠一樣,當我們需要建立一個物件的時候,只需要配置好配置檔案/註解即可,完全不用考慮物件是如何被建立出來的。 在實際專案中一個 Service 類可能有幾百甚至上千個類作為它的底層,假如我們需要例項化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的建構函式,這可能會把人逼瘋。如果利用 IOC 的話,你只需要配置好,然後在需要的地方引用就行了,這大大增加了專案的可維護性且降低了開發難度。

舉個例子:現有一個針對User的操作,利用 Service 和 Dao 兩層結構進行開發!

在沒有使用IOC思想的情況下,Service 層想要使用 Dao層的具體實現的話,需要通過new關鍵字在UserServiceImpl 中手動 new出 IUserDao 的具體實現類 UserDaoImpl(不能直接new介面類)。

這種方式可以實現,但是如果開發過程中接到新需求,針對IUserDao 介面開發出另一個具體實現類。因為Server層依賴了IUserDao的具體實現,所以我們需要修改UserServiceImpl中new的物件。如果只有一個類引用了IUserDao的具體實現,可能覺得還好,修改起來也不是很費力氣,但是如果有許許多多的地方都引用了IUserDao的具體實現的話,一旦需要更換IUserDao的實現方式,那修改起來將會非常的頭疼。

但是如果使用IOC容器的話,我們就不需要操心這些事,只需要用的時候往IOC容器裡面“要”就完事。

2 Spring IOC容器實現

在IOC容器的設計中,有兩個主要的容器系列,一個是實現BeanFactory介面的簡單容器系列,這系列容器只實現了容器的最基本功能;另一個是ApplicationContext應用上下文,它作文容器的高階形態而存在。後面作為容器的高階形態,在簡單容器的基礎上面增加了許多的面向框架的特性,同時對應用環境作了許多適配。

2.1 BeanFactory

BeanFactory,從名字上也很好理解,生產 bean 的工廠,它負責生產和管理各個 bean 例項。

我們先來看一下BeanFactory的繼承體系

先介紹一下里面比較重要的一些介面和類

  1. ApplicationContext 繼承了 ListableBeanFactory,這個 Listable 的意思就是,通過這個介面,我們可以獲取多個 Bean,大家看原始碼會發現,最頂層 BeanFactory 介面的方法都是獲取單個 Bean 的。
  2. ApplicationContext 繼承了 HierarchicalBeanFactory,Hierarchical 單詞本身已經能說明問題了,意思是分層,也就是說我們可以在應用中起多個 BeanFactory,然後可以將各個 BeanFactory 設定為父子關係。
  3. AutowireCapableBeanFactory 這個名字中的 Autowire 大家都非常熟悉,它就是用來自動裝配 Bean 用的(如按名字匹配,按型別匹配等),但是仔細看上圖,ApplicationContext 並沒有繼承它,不過不用擔心,不使用繼承,不代表不可以使用組合,如果你看到 ApplicationContext 介面定義中的最後一個方法 getAutowireCapableBeanFactory() 就知道了。
  4. ConfigurableListableBeanFactory 也是一個特殊的介面,看圖,特殊之處在於它繼承了第二層所有的三個介面,而 ApplicationContext 沒有。用於擴充套件IOC容器的定製性!

2.2 ApplicationContext

ApplicationContext下面有著我們通過配置檔案來構建,也是我們的子實現類。先來看一下繼承體系

我們重點了解一下比較主要的實現類:

  • ClassPathXmlApplicationContext從名字可以看出一二,就是在ClassPath中尋找xml配置檔案,根據xml檔案內容來構件ApplicationContext容器。

  • FileSystemXmlApplicationContext 的建構函式需要一個 xml 配置檔案在系統中的路徑,其他和 ClassPathXmlApplicationContext 基本上一樣。

  • AnnotationConfigApplicationContext 是基於註解來使用的,它不需要配置檔案,採用 Java 配置類和各種註解來配置,是比較簡單的方式,也是大勢所趨。

  • ConfigurableApplicationContext 擴充套件於 ApplicationContext,它新增加了兩個主要的方法: refresh()和 close(),讓 ApplicationContext 具有啟動、重新整理和關閉應用上下文的能力。在應用上下文關閉的情況下呼叫 refresh()即可啟動應用上下文,在已經啟動的狀態下,呼叫 refresh()則清除快取並重新裝載配置資訊,而呼叫close()則可關閉應用上下文。

此外,ApplicationContext還通過其他介面擴充套件了BeanFactory的功能,如下圖

  • ApplicationEventPublisher:讓容器擁有釋出應用上下文事件的功能,包括容器啟動事件、關閉事件等。實現了 ApplicationListener 事件監聽介面的 Bean 可以接收到容器事件 , 並對事件進行響應處理 。 在 ApplicationContext 抽象實現類AbstractApplicationContext 中,我們可以發現存在一個 ApplicationEventMulticaster,它負責儲存所有監聽器,以便在容器產生上下文事件時通知這些事件監聽者。
  • MessageSource:為應用提供 i18n 國際化訊息訪問的功能。
  • ResourcePatternResolver :ApplicationContext 實現類都實現了類似於PathMatchingResourcePatternResolver 的功能,可以通過帶字首的 Ant 風格的資原始檔路徑裝載 Spring 的配置檔案。

2.3 WebApplicationContext

在ApplicationContext下面還有一個實現類是WebApplicationContext,是專門為 Web 應用準備的容器,它允許從相對於 Web 根目錄的路徑中裝載配置檔案完成初始化工作。

從WebApplicationContext 中可以獲得 ServletContext 的引用,整個 Web 應用上下文物件將作為屬性放置到 ServletContext 中,以便 Web 應用環境可以訪問 Spring 應用上下文。 WebApplicationContext 定義了一個常量ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,在上下文啟動時, WebApplicationContext 例項即以此為鍵放置在 ServletContext 的屬性列表中,因此我們可以直接通過以下語句從 Web 容器中獲取WebApplicationContext:

WebApplicationContext wac = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

整合圖如下,其他不過多介紹:

3 SpringIOC的啟動流程

Spring IOC的啟動時會讀取應用程式提供的Bean的配置資訊,並在Spring容器中生成一份相應的Bean配置登錄檔,然後根據登錄檔載入、例項化bean、建立bean與bean之間的依賴關係。然後將這些準備就緒的bean放到bean快取池中,等待應用程式呼叫。

總結一下,我們可以把IOC的啟動流程分為一下兩個重要的階段:

  1. 容器的啟動階段
  2. Bean的例項化階段

這裡補充一下,在 Spring 中,最基礎的容器介面方法是由 BeanFactory 定義的,而 BeanFactory 的實現類採用的是 延遲載入,也就是說,容器啟動時,只會進行第一個階段的操作, 當需要某個類的例項時,才會進行第二個階段的操作。而 ApplicationContext(另一個容器的實現類)在啟動容器時就完成了所有初始化,這就需要更多的系統資源,我們需要根據不同的場景選擇不同的容器實現類。我們下面介紹更多是以ApplicationContext為主來介紹!

3.1 IOC容器的啟動階段

在容器啟動階段,我們的Spring經歷了很多事情,具體的話可以分為以下幾個步驟:

  1. 載入配置資訊
  2. 解析配置資訊
  3. 裝配BeanDefinition
  4. 後處理

3.1.1 載入配置資訊

這裡我們要先回顧一下之前的beanfactory了,我們說這是一個最基礎的bean工廠介面,那麼就需要我們的實現類,我們上面雖然說到了ApplicationContext,但是我們再仔細看一下那張圖,然後站高處來看。ApplicationContext 繼承自 BeanFactory,但是它不應該被理解為 BeanFactory 的實現類,而是說其內部持有一個例項化的 BeanFactory(DefaultListableBeanFactory)。以後所有的 BeanFactory 相關的操作其實是委託給這個例項來處理的。

我們為什麼選擇了DefaultListableBeanFactory,可以看到它繼承的兩個父類,然後繼續延伸上去齊全了所有的功能。可以說DefaultListableBeanFactory 基本上是最牛的 BeanFactory 了,這也是為什麼這邊會使用這個類來例項化的原因。

好了,我們繼續回到載入配置檔案資訊這個話題。我們Spring載入配置檔案最開始圖裡面也介紹了,有ClassPathXmlApplicationContext 類路徑載入和FileSystemXmlApplicationContext 檔案系統載入。

然後就是我們IOC 容器讀取配置檔案的介面為 BeanDefinitionReader,它會根據配置檔案格式的不同給出不同的實現類,將配置檔案中的內容讀取並對映到 BeanDefinition 中。比如xml檔案就會用XmlBeanDefinitionReader

3.1.2 解析配置資訊

我們解析配置資訊就是要將我們讀取的配置資訊裡面的資訊轉換成一個dom樹,然後解析裡面的配置資訊裝配到我們的BeanDefinition。我們在processBeanDefinition中先將解析後的資訊封裝到一個BeanDefinitionHolder,一個BeanDefinitionHolder其實就是一個 BeanDefinition 的例項和它的 beanName、aliases (別名)這三個資訊。

processBeanDefinition過程可以解析很多的標籤,如factory-beanfactory-method<lockup-method /><replaced-method /><meta /><qualifier />,當然最顯目的就是<bean/>,例如以下的屬性:

Property
class 類的全限定名
name 可指定 id、name(用逗號、分號、空格分隔)
scope 作用域
constructor arguments 指定構造引數
properties 設定屬性的值
autowiring mode no(預設值)、byName、byType、 constructor
lazy-initialization mode 是否懶載入(如果被非懶載入的bean依賴了那麼其實也就不能懶載入了)
initialization method bean 屬性設定完成後,會呼叫這個方法
destruction method bean 銷燬後的回撥方法

在具體的xml配置檔案中可以是這樣子的:

<bean id="exampleBean" name="name1, name2, name3" class="com.javadoop.ExampleBean"
      scope="singleton" lazy-init="true" init-method="init" destroy-method="cleanup">

    <!-- 可以用下面三種形式指定構造引數 -->
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg index="0" value="7500000"/>

    <!-- property 的幾種情況 -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

3.1.3 裝配BeanDefinition

在上面我們將資訊解析後,就會裝配到一個BeanDefinitionHolder,裡面就包含了我們的BeanDefinition。然後裝配BeanDefinition,就是將這些BeanDefinition註冊到BeanDefinitionRegistry(說到底核心是一個 beanName-> beanDefinition 的 map)中。我們在獲取的BeanDefinition的時候需要通過key(beanName)獲取別名,然後通過別名再一次重定向獲取我們的BeanDefinition。

Spring容器的後續操作直接從BeanDefinitionRegistry中讀取配置資訊。具體註冊實現就是在我們上面介紹到的DefaultListableBeanFactory實現類裡。

3.1.4 後處理

在我們的後續操作,容器掃描BeanDefinitionRegistry中的BeanDefinition,使用Java的反射機制自動識別出Bean工廠後處理後器(實現BeanFactoryPostProcessor介面)的Bean,然後呼叫這些Bean工廠後處理器對BeanDefinitionRegistry中的BeanDefinition進行加工處理。主要完成以下兩項工作:

  • 對使用到佔位符的元素標籤進行解析,得到最終的配置值,這意味對一些半成品式的BeanDefinition物件進行加工處理並得到成品的BeanDefinition物件;

  • BeanDefinitionRegistry中的BeanDefinition進行掃描,通過Java反射機制找出所有屬性編輯器的Bean(實現java.beans.PropertyEditor介面的Bean),並自動將它們註冊到Spring容器的屬性編輯器登錄檔中(PropertyEditorRegistry);

Spring容器從BeanDefinitionRegistry中取出加工後的BeanDefinition,並呼叫InstantiationStrategy著手進行Bean例項化的工作;在例項化Bean時,Spring容器使用BeanWrapper對Bean進行封裝,BeanWrapper提供了很多以Java反射機制操作Bean的方法,它將結合該Bean的BeanDefinition以及容器中屬性編輯器,完成Bean屬性的設定工作。

在我們裝配好Bean容器後,還要通過方法prepareBeanFactory準備Bean容器,在準備階段會註冊一些特殊的Bean,這裡不做深究。在準備容器後我們可能會對bean進行一些加工,就需要用到beanPostProcessor來進行一些後處理。我們利用容器中註冊的Bean後處理器(實現BeanPostProcessor介面的Bean)對已經完成屬性設定工作的Bean進行後續加工,直接裝配出一個準備就緒的Bean。這個在下面例項化階段後再介紹到!

這裡可能會對BeanPostProcessorBeanFactoryPostProcessor產生混亂,理解不清。總結一下兩者的區別:

  • BeanPostProcessor 對容器中的Bean進行後處理,對Bean進行額外的加強,加工。使用點是在我們單例Bean例項化過程中穿插執行的。
  • BeanFactoryPostProcessorSpring容器本身進行後處理,增強容器的功能。是在我們單例例項化之前執行的。

更加具體的可以參考這一篇博文。點選跳轉

3.1.5 總結

這裡關於容器的啟動過程很多細節並不是很詳細,因為很多東西都需要配著原始碼才能分析。關於原始碼解析推薦這一篇,更加深入(Spring IOC 容器原始碼分析

3.2 Bean的例項化階段

然後就是我們Bean的預先例項化階段。在ApplicationContext中,所有的BeanDefinition的Scope預設是Singleton,針對Singleton我們Spring容器採用是預先例項化的策略。這樣我們在獲取例項的時候就會直接從快取裡面拉取出來,提升了執行效率。

但是如果我們設定了懶載入的話,那麼就不會預先例項化。而是在我們第一次getBean的時候才會去例項化。不過我們大部分時候都不會去使用懶載入,除非這個bean比較特殊,例如非常耗費資源,在應用程式的生命週期裡的使用概率比較小。在這種情況下我們可以將它設定為懶載入!

3.2.1 例項化過程

針對我們的Bean的例項化,具體一點的話可以分為以下階段:

  1. Spring對bean進行例項化,預設bean是單例;
  2. Spring對bean進行依賴注入,比如有沒有配置當前depends-on的依賴,有的話就去例項依賴的bean;
  3. 如果bean實現了BeanNameAware介面,spring將bean的id傳給setBeanName()方法;
  4. 如果bean實現了BeanFactoryAware介面,spring將呼叫setBeanFactory方法,將BeanFactory例項傳進來;
  5. 如果bean實現了ApplicationContextAware介面,它的setApplicationContext()方法將被呼叫,將應用上下文的引用傳入到bean中;
  6. 如果bean實現了BeanPostProcessor介面,它的postProcessBeforeInitialization方法將被呼叫;
  7. 如果bean實現了InitializingBean介面,spring將呼叫它的afterPropertiesSet介面方法,類似的如果bean使用了init-method屬性宣告瞭初始化方法,則再呼叫該方法;
  8. 如果bean實現了BeanPostProcessor介面,它的postProcessAfterInitialization介面方法將被呼叫;
  9. 此時bean已經準備就緒,可以被應用程式使用了,他們將一直駐留在應用上下文中,直到該應用上下文被銷燬;
  10. 若bean實現了DisposableBean介面,spring將呼叫它的distroy()介面方法。如果bean使用了destroy-method屬性宣告瞭銷燬方法,則再呼叫該方法;

上面提及到的方法有點多,但是我們可以按照分類去記憶

分類型別 所包含方法
Bean自身的方法 配置檔案中的init-method和destroy-method配置的方法、Bean物件自己呼叫的方法
Bean級生命週期介面方法 BeanNameAware、BeanFactoryAware、InitializingBean、DiposableBean等介面中的方法
容器級生命週期介面方法 InstantiationAwareBeanPostProcessor、BeanPostProcessor等後置處理器實現類中重寫的方法

3.2.2 迴圈依賴問題

關於例項化過程其實是一塊比較複雜的東西,如果不去看原始碼的話,講個上面的流程也差不多。畢竟完全講的話,哪能記住那麼多。在這裡還有一個主要講的就是在例項化過程中一個比較複雜的問題,就是“迴圈依賴問題”。這裡花點篇幅講解一下。

迴圈依賴問題,舉個例子引入一下。比如我們有A,B兩個類,A的 構造方法有一個引數是B,B的構造方法有一個引數是A,這種A依賴於B,B依賴於A的問題就是依賴問題。

@Service
public class A {  
    public A(B b) {  }
}

@Service
public class B {  
    public B(A a) {  
    }
}

或者說A依賴於B,B依賴於C,C依賴於A也是。

我們的迴圈依賴可以分類成三種:

  • 原型迴圈依賴
  • 單例構造器迴圈依賴
  • 單例setter注入迴圈依賴

我們的Spring是無法解決構造器的迴圈依賴的,但是可以解決setter的迴圈依賴。關於這三者的區別,這裡給出一篇比較詳細的博文可以參考。(迴圈依賴的三種方式

3.2.3 迴圈依賴解決

構造器依賴問題

我們說過構造器的迴圈依賴Spring是無法解決的,那引出另一個問題就是Spirng是如何判斷構造器發生了迴圈依賴呢?

簡單介紹一下,我們在上面介紹的例子,A依賴於B,B依賴於A。在我們A例項化的時候要去例項B,然後B又要去例項A,在我們過程中,我們這將beanName新增到一個set結構中,當第二次新增A的時候,也就是B依賴於A,要去例項化A的時候,因為Set已經存在A的beanName了,所以Spring就會判斷髮生了迴圈依賴問題,丟擲異常!

原型依賴問題

至於原型依賴的判斷條件其實和構造器的判斷差不多,最主要的區別就是set的型別變成了ThreadLocal型別的。

Setter是如何具體解決迴圈依賴問題呢?

我們的Spring是通過三級快取來解決的。

三級快取呢,其實就是有三個快取:

  • singletonObjects(一級快取)
  • earlySingletonObjects(二級快取)
  • singletonFactories(三級快取)

我們以上面A依賴於B,B依賴於A的樣例來分析一下setter是如何通過三級快取解決迴圈依賴問題。

  1. 首先我們在例項化A的時候,通過beanDifinition定義拿到A class的無參構造方法,通過反射建立了這個例項物件。這個A的例項物件是一個尚未進行依賴注入和init-method方法呼叫等等邏輯處理的早期例項,是我們業務無法使用的。然後在進行後續的包裝處理前,我們會將它封裝成一個ObjectFactory物件然後存入到我們的三級快取中(key是beanName,value是ObjectFactory物件),相當於一個早起工廠提前曝光。
  2. 然後呢我們的會繼續例項化A,在例項過程中因為A依賴於B,我們通過Setter注入依賴的時候,通過getBean(B)去獲取依賴物件B,但是這個B還沒有例項化,所以我們就需要去建立B的例項。
  3. 然後我們就開始建立B的例項,同上A的過程。在例項B的過程中,因為B依賴於A,所以也會呼叫getBean(A)去獲得A的例項,首先就會去一級快取訪問,如果沒有就去二級快取,再沒有就去三級快取。然後在三級快取中發現我們的早期例項A,不過也拿來用了。然後完成B的依賴,再完成後面B例項化過程的一系列階段,最後並且存放到Spring的一級快取中。並將二三級快取清理掉。
  4. 完成B的例項後,我們就會回到A的例項階段,我們的A在有了B的依賴後,也繼續完成了後續的例項化過程,把一個早期的物件變成一個完整的物件。並將A存進到一級快取中,清除二三級快取。

為什麼要有三級快取?二級快取不夠用嗎?

我們在上面分析的過程中呢,可能會感覺二級快取的存在感不是特別強。為什麼不去掉第二級的快取然後變成一個二級快取呢。

這裡呢,解釋一下。我們的B在拿到A的早期例項後就會進行快取升級,將A從從三級快取移到二級快取中。之所以需要有三級快取呢,是因為在這一步,我們的bean可能還需要一些其他的操作,可能會被bean後置處理器進行一些增強之類的啥,或者做一些AOP的判斷。如果只有二級快取的話,那麼返回的就是早期例項而不是我們增強的後的例項!

總結

對啟動流程這一塊,看了網上的很多資料,也照著看了一下原始碼。雖然自己總結了,但總感覺哪裡會有紕漏。如果哪裡有錯誤的話,還請看官們幫忙指出,或者給我指明一下哪裡需要修改的地方,大家一起進步學習!

參考資料

Spring技術內幕:深入理解Spring架構與設計原理 [機械工業出版社]

IOC&AOP詳解

Spring IOC 容器原始碼分析

Spring IOC原理總結

Spring IOC 啟動過程

Spring IOC:Spring IOC 的具體過程

Spring IOC -bean物件的生命週期詳解

相關文章