1. 程式人生 > >Spring5原始碼分析系列(四)IOC容器

Spring5原始碼分析系列(四)IOC容器

本章開始進入Spring5原始碼分析,文章有點長,參考自Tom老師視訊。

什麼是IOC/DI?

IOC(InversionofControl)控制反轉:所謂控制反轉,就是把原先我們程式碼裡面需要實現的物件建立、依賴的程式碼,反轉給容器來幫忙實現。那麼必然的我們需要建立一個容器,同時需要一種描述來讓容器知道需要建立的物件與物件的關係。這個描述最具體表現就是我們可配置的檔案。

DI(DependencyInjection)依賴注入:就是指物件是被動接受依賴類而不是自己主動去找,換句話說就是指物件不是從容器中查詢它依賴的類,而是在容器例項化物件的時候主動將它依賴的類注入給它。

先從我們自己設計這樣一個視角來考慮:

物件和物件關係怎麼表示?

可能是classpath,filesystem,或者是URL網路資源,servletContext等。

回到正題,有了配置檔案,還需要對配置檔案解析。

不同的配置檔案對物件的描述不一樣,如標準的,自定義宣告式的,如何統一?在內部需要有一個統一的關於物件的定義,所有外部的描述都必須轉化成統一的描述定義。

如何對不同的配置檔案進行解析?需要對不同的配置檔案語法,採用不同的解析器

Spring核心容器體系結構

(1)BeanFactory

SpringBean的建立是典型的工廠模式,這一系列的Bean工廠,也即IOC容器為開發者管理物件間的依賴關係提供了很多便利和基礎服務,在Spring中有許多的IOC容器的實現供使用者選擇和使用,其相互關係如下:

其中BeanFactory作為最頂層的一個介面類,它定義了IOC容器的基本功能規範,BeanFactory有三個子類:ListableBeanFactory、HierarchicalBeanFactory和AutowireCapableBeanFactory。但是從上圖中我們可以發現最終的預設實現類是DefaultListableBeanFactory,他實現了所有的介面。那為何要定義這麼多層次的介面呢?查閱這些介面的原始碼和說明發現,每個介面都有他使用的場合,它主要是為了區分在Spring內部在操作過程中物件的傳遞和轉化過程中,對物件的資料訪問所做的限制。例如ListableBeanFactory介面表示這些Bean是可列表的,而HierarchicalBeanFactory表示的是這些Bean是有繼承關係的,也就是每個Bean有可能有父Bean。AutowireCapableBeanFactory介面定義Bean的自動裝配規則。這四個介面共同定義了Bean的集合、Bean之間的關係、以及Bean行為.

最基本的IOC容器介面BeanFactory

    在BeanFactory裡只對IOC容器的基本行為作了定義,根本不關心你的Bean是如何定義怎樣載入的。正如我們只關心工廠裡得到什麼的產品物件,至於工廠是怎麼生產這些物件的,這個基本的介面不關心。

而要知道工廠是如何產生物件的,我們需要看具體的IOC容器實現,Spring提供了許多IOC容器的實現。比如XmlBeanFactory,ClasspathXmlApplicationContext等。其中XmlBeanFactory就是針對最基本的IOC容器的實現,這個IOC容器可以讀取XML檔案定義的BeanDefinition(XML檔案中對bean的描述),如果說XmlBeanFactory是容器中的屌絲,ApplicationContext應該算容器中的高帥富.

ApplicationContext是Spring提供的一個高階的IOC容器,它除了能夠提供IOC容器的基本功能外,還為使用者提供了以下的附加服務。

從ApplicationContext介面的實現,我們看出其特點:

1.支援資訊源,可以實現國際化。(實現MessageSource介面)

2.訪問資源。(實現ResourcePatternResolver介面,後面章節會講到)

3.支援應用事件。(實現ApplicationEventPublisher介面)

(2)BeanDefinition

SpringIOC容器管理了我們定義的各種Bean物件及其相互的關係,Bean物件在Spring實現中是以BeanDefinition來描述的,其繼承體系如下:

Bean的解析過程非常複雜,功能被分的很細,因為這裡需要被擴充套件的地方很多,必須保證有足夠的靈活性,以應對可能的變化。Bean的解析主要就是對Spring配置檔案的解析。這個解析過程主要通過下圖中的類完成:

IOC容器的初始化

IOC容器的初始化包括BeanDefinition的Resource定位、載入和註冊這三個基本的過程。我們以ApplicationContext為例講解,ApplicationContext系列容器也許是我們最熟悉的,因為Web專案中使用的XmlWebApplicationContext就屬於這個繼承體系,還有ClasspathXmlApplicationContext等,其繼承體系如下圖所示:

ApplicationContext允許上下文巢狀,通過保持父上下文可以維持一個上下文體系。對於Bean的查詢可以在這個上下文體系中發生,首先檢查當前上下文,其次是父上下文,逐級向上,這樣為不同的Spring應用提供了一個共享的Bean定義環境。

下面我們分別簡單地演示一下兩種IOC容器的建立過程

1、XmlBeanFactory(屌絲IOC)的整個流程

通過XmlBeanFactory的原始碼,我們可以發現:

通過前面的原始碼,XmlBeanDefinitionReaderreader=newXmlBeanDefinitionReader(this);中其中this傳的是factory物件

2、FileSystemXmlApplicationContext的IOC容器流程

(1)、高富帥版IOC解剖

其實際呼叫的建構函式為:

(2)、設定資源載入器和資源定位

通過分析FileSystemXmlApplicationContext的原始碼可以知道,在建立FileSystemXmlApplicationContext容器時,構造方法做以下兩項重要工作:

首先,呼叫父類容器的構造方法(super(parent)方法)為容器設定好Bean資源載入器。

然後,再呼叫父類AbstractRefreshableConfigApplicationContext的setConfigLocations(configLocations)方法設定Bean定義資原始檔的定位路徑。

通過追蹤FileSystemXmlApplicationContext的繼承體系,發現其父類的父類AbstractApplicationContext中初始化IOC容器所做的主要原始碼如下:

AbstractApplicationContext構造方法中呼叫PathMatchingResourcePatternResolver的構造方法建立Spring資源載入器:

在設定容器的資源載入器之後,接下來FileSystemXmlApplicationContext執行setConfigLocations方法通過呼叫其父類AbstractRefreshableConfigApplicationContext的方法進行對Bean定義資原始檔的定位,該方法的原始碼如下:

通過這兩個方法的原始碼我們可以看出,我們既可以使用一個字串來配置多個SpringBean定義資原始檔,也可以使用字串陣列,即下面兩種方式都是可以的:

ClasspathResourceres=newClasspathResource(“a.xml,b.xml,......”);多個資原始檔路徑之間可以是用”,;\t\n”等分隔。

B.ClasspathResourceres=newClasspathResource(newString[]{“a.xml”,”b.xml”,......});至此,SpringIOC容器在初始化時將配置的Bean定義資原始檔定位為Spring封裝的Resource。

(3)、AbstractApplicationContext的refresh函式載入Bean定義過程:

SpringIOC容器對Bean定義資源的載入是從refresh()函式開始的,refresh()是一個模板方法,refresh()方法的作用是:在建立IOC容器前,如果已經有容器存在,則需要把已有的容器銷燬和關閉,以保證在refresh之後使用的是新建立起來的IOC容器。refresh的作用類似於對IOC容器的重啟,在新建立好的容器中對容器進行初始化,對Bean定義資源進行載入

FileSystemXmlApplicationContext通過呼叫其父類AbstractApplicationContext的refresh()函式啟動整個IOC容器對Bean定義的載入過程:

refresh()方法主要為IOC容器Bean的生命週期管理提供條件,SpringIOC容器載入Bean定義資原始檔從其子類容器的refreshBeanFactory()方法啟動,所以整個refresh()中“ConfigurableListableBeanFactory beanFactory=obtainFreshBeanFactory();”這句以後程式碼的都是註冊容器的資訊源和生命週期事件,載入過程就是從這句程式碼啟動。

refresh()方法的作用是:在建立IOC容器前,如果已經有容器存在,則需要把已有的容器銷燬和關閉,以保證在refresh之後使用的是新建立起來的IOC容器。refresh的作用類似於對IOC容器的重啟,在新建立好的容器中對容器進行初始化,對Bean定義資源進行載入

(4)、AbstractApplicationContext的obtainFreshBeanFactory()方法呼叫子類容器的refreshBeanFactory()方法,啟動容器載入Bean定義資原始檔的過程,程式碼如下:

AbstractApplicationContext類中只抽象定義了refreshBeanFactory()方法,容器真正呼叫的是其子類AbstractRefreshableApplicationContext實現的refreshBeanFactory()方法,方法的原始碼如下:

在這個方法中,先判斷BeanFactory是否存在,如果存在則先銷燬beans並關閉beanFactory,接著建立DefaultListableBeanFactory,並呼叫loadBeanDefinitions(beanFactory)裝載bean定義。

(5)、AbstractRefreshableApplicationContext子類的loadBeanDefinitions方法:

AbstractRefreshableApplicationContext中只定義了抽象的loadBeanDefinitions方法,容器真正呼叫的是其子類AbstractXmlApplicationContext對該方法的實現,AbstractXmlApplicationContext的主要原始碼如下:

loadBeanDefinitions方法同樣是抽象方法,是由其子類實現的,也即在AbstractXmlApplicationContext中。

XmlBean讀取器(XmlBeanDefinitionReader)呼叫其父類AbstractBeanDefinitionReader的reader.loadBeanDefinitions方法讀取Bean定義資源。

由於我們使用FileSystemXmlApplicationContext作為例子分析,因此getConfigResources的返回值為null,因此程式執行reader.loadBeanDefinitions(configLocations)分支。

(6)、AbstractBeanDefinitionReader讀取Bean定義資源,在其抽象父類AbstractBeanDefinitionReader中定義了載入過程。

AbstractBeanDefinitionReader的loadBeanDefinitions方法原始碼如下:

loadBeanDefinitions(Resource...resources)方法和上面分析的3個方法類似,同樣也是呼叫XmlBeanDefinitionReader的loadBeanDefinitions方法。

從對AbstractBeanDefinitionReader的loadBeanDefinitions方法原始碼分析可以看出該方法做了以下兩件事:

首先,呼叫資源載入器的獲取資源方法resourceLoader.getResource(location),獲取到要載入的資源。其次,真正執行載入功能是其子類XmlBeanDefinitionReader的loadBeanDefinitions方法。

看到上面的ResourceLoader與ApplicationContext的繼承系圖,可以知道其實際呼叫的是DefaultResourceLoader中的getSource()方法定位Resource,因為FileSystemXmlApplicationContext本身就是DefaultResourceLoader的實現類,所以此時又回到了FileSystemXmlApplicationContext中來。

(7)、資源載入器獲取要讀入的資源:

XmlBeanDefinitionReader通過呼叫其父類DefaultResourceLoader的getResource方法獲取要載入的資源,其原始碼如下

FileSystemXmlApplicationContext容器提供了getResourceByPath方法的實現,就是為了處理既不是classpath標識,又不是URL標識的Resource定位這種情況。

這樣程式碼就回到了FileSystemXmlApplicationContext中來,他提供了FileSystemResource來完成從檔案系統得到配置檔案的資源定義。

這樣,就可以從檔案系統路徑上對IOC配置檔案進行載入,當然我們可以按照這個邏輯從任何地方載入,在Spring中我們看到它提供的各種資源抽象,比如ClassPathResource,URLResource,FileSystemResource等來供我們使用。上面我們看到的是定位Resource的一個過程,而這只是載入過程的一部分.

(8)、XmlBeanDefinitionReader載入Bean定義資源:

繼續回到XmlBeanDefinitionReader的loadBeanDefinitions(Resource...)方法看到代表bean檔案的資源定義以後的載入過程。

通過原始碼分析,載入Bean定義資原始檔的最後一步是將Bean定義資源轉換為Document物件,該過程由documentLoader實現

(9)、DocumentLoader將Bean定義資源轉換為Document物件:

DocumentLoader將Bean定義資源轉換成Document物件的原始碼如下:

該解析過程呼叫JavaEE標準的JAXP標準進行處理。

至此SpringIOC容器根據定位的Bean定義資原始檔,將其載入讀入並轉換成為Document物件過程完成。接下來我們要繼續分析SpringIOC容器將載入的Bean定義資原始檔轉換為Document物件之後,是如何將其解析為SpringIOC管理的Bean物件並將其註冊到容器中的。

(10)、XmlBeanDefinitionReader解析載入的Bean定義資原始檔:

XmlBeanDefinitionReader類中的doLoadBeanDefinitions方法是從特定XML檔案中實際載入Bean定義資源的方法,該方法在載入Bean定義資源之後將其轉換為Document物件,接下來呼叫registerBeanDefinitions啟動SpringIOC容器對Bean定義的解析過程,registerBeanDefinitions方法原始碼如下:

Bean定義資源的載入解析分為以下兩個過程:

首先,通過呼叫XML解析器將Bean定義資原始檔轉換得到Document物件,但是這些Document物件並沒有按照Spring的Bean規則進行解析。這一步是載入的過程

其次,在完成通用的XML解析之後,按照Spring的Bean規則對Document物件進行解析。

按照Spring的Bean規則對Document物件解析的過程是在介面BeanDefinitionDocumentReader的實現類DefaultBeanDefinitionDocumentReader中實現的。

(11)、DefaultBeanDefinitionDocumentReader對Bean定義的Document物件解析:

BeanDefinitionDocumentReader介面通過registerBeanDefinitions方法呼叫其實現類DefaultBeanDefinitionDocumentReader對Document物件進行解析,解析的程式碼如下:

通過上述SpringIOC容器對載入的Bean定義Document解析可以看出,我們使用Spring時,在Spring配置檔案中可以使用<import>元素來匯入IOC容器所需要的其他資源,SpringIOC容器在解析時會首先將指定匯入的資源載入進容器中。使用<ailas>別名時,SpringIOC容器首先將別名元素所定義的別名註冊到容器中。

對於既不是<import>元素,又不是<alias>元素的元素,即Spring配置檔案中普通的<bean>元素的解析由BeanDefinitionParserDelegate類的parseBeanDefinitionElement方法來實現。

(12)、BeanDefinitionParserDelegate解析Bean定義資原始檔中的<bean>元素:

Bean定義資原始檔中的<import>和<alias>元素解析在DefaultBeanDefinitionDocumentReader中已經完成,對Bean定義資原始檔中使用最多的<bean>元素交由BeanDefinitionParserDelegate來解析,其解析實現的原始碼如下:

只要使用過Spring,對Spring配置檔案比較熟悉的人,通過對上述原始碼的分析,就會明白我們在Spring配置檔案中<Bean>元素的中配置的屬性就是通過該方法解析和設定到Bean中去的。

注意:在解析<Bean>元素過程中沒有建立和例項化Bean物件,只是建立了Bean物件的定義類BeanDefinition,將<Bean>元素中的配置資訊設定到BeanDefinition中作為記錄,當依賴注入時才使用這些記錄資訊建立和例項化具體的Bean物件。

上面方法中一些對一些配置如元資訊(meta)、qualifier等的解析,我們在Spring中配置時使用的也不多,我們在使用Spring的<Bean>元素時,配置最多的是<property>屬性,因此我們下面繼續分析原始碼,瞭解Bean的屬性在解析時是如何設定的。

(13)、BeanDefinitionParserDelegate解析<property>元素:BeanDefinitionParserDelegate在解析<Bean>呼叫parsePropertyElements方法解析<Bean>元素中的<property>屬性子元素,解析原始碼如下:

/

通過對上述原始碼的分析,我們可以瞭解在Spring配置檔案中,<Bean>元素中<property>元素的相關配置是如何處理的:

a.ref被封裝為指向依賴物件一個引用。

b.value配置都會封裝成一個字串型別的物件。

c.ref和value都通過“解析的資料型別屬性值.setSource(extractSource(ele));”方法將屬性值/引用與所引用的屬性關聯起來。

在方法的最後對於<property>元素的子元素通過parsePropertySubElement方法解析,我們繼續分析該方法的原始碼,瞭解其解析過程。

(14)、解析<property>元素的子元素:

在BeanDefinitionParserDelegate類中的parsePropertySubElement方法對<property>中的子元素解析,原始碼如下:

通過上述原始碼分析,我們明白了在Spring配置檔案中,對<property>元素中配置的array、list、set、map、prop等各種集合子元素的都通過上述方法解析,生成對應的資料物件,比如ManagedList、ManagedArray、ManagedSet等,這些Managed類是Spring物件BeanDefiniton的資料封裝,對集合資料型別的具體解析有各自的解析方法實現,解析方法的命名非常規範,一目瞭然,我們對<list>集合元素的解析方法進行原始碼分析,瞭解其實現過程。

(15)、解析<list>子元素:

在BeanDefinitionParserDelegate類中的parseListElement方法就是具體實現解析<property>元素中的<list>集合子元素,原始碼如下:

經過對SpringBean定義資原始檔轉換的Document物件中的元素層層解析,SpringIOC現在已經將XML形式定義的Bean定義資原始檔轉換為SpringIOC所識別的資料結構——BeanDefinition,它是Bean定義資原始檔中配置的POJO物件在SpringIOC容器中的對映,我們可以通過AbstractBeanDefinition為入口,看到了IOC容器進行索引、查詢和操作。

通過SpringIOC容器對Bean定義資源的解析後,IOC容器大致完成了管理Bean物件的準備工作,即初始化過程,但是最為重要的依賴注入還沒有發生,現在在IOC容器中BeanDefinition儲存的只是一些靜態資訊,接下來需要向容器註冊Bean定義資訊才能全部完成IOC容器的初始化過程

(16)、解析過後的BeanDefinition在IOC容器中的註冊:

讓我們繼續跟蹤程式的執行順序,接下來我們來分析DefaultBeanDefinitionDocumentReader對Bean定義轉換的Document物件解析的流程中,在其parseDefaultElement方法中完成對Document物件的解析後得到封裝BeanDefinition的BeanDefinitionHold物件,然後呼叫BeanDefinitionReaderUtils的registerBeanDefinition方法向IOC容器註冊解析的Bean,BeanDefinitionReaderUtils的註冊的原始碼如下:

當呼叫BeanDefinitionReaderUtils向IOC容器註冊解析的BeanDefinition時,真正完成註冊功能的是DefaultListableBeanFactory。

(17)、DefaultListableBeanFactory向IOC容器註冊解析後的BeanDefinition:DefaultListableBeanFactory中使用一個HashMap的集合物件存放IOC容器中註冊解析的BeanDefinition,向IOC容器註冊的主要原始碼如下:

至此,Bean定義資原始檔中配置的Bean被解析過後,已經註冊到IOC容器中,被容器管理起來,真正完成了IOC容器初始化所做的全部工作。現在IOC容器中已經建立了整個Bean的配置資訊,這些BeanDefinition資訊已經可以使用,並且可以被檢索,IOC容器的作用就是對這些註冊的Bean定義資訊進行處理和維護。這些的註冊的Bean定義資訊是IOC容器控制反轉的基礎,正是有了這些註冊的資料,容器才可以進行依賴注入。

總結:

現在通過上面的程式碼,總結一下IOC容器初始化的基本步驟:

(1).初始化的入口在容器實現中的refresh()呼叫來完成。

(2).對bean定義載入IOC容器使用的方法是loadBeanDefinition,其中的大致過程如下:

通過ResourceLoader來完成資原始檔位置的定位,DefaultResourceLoader是預設的實現,同時上下文字身就給出了ResourceLoader的實現,可以從類路徑,檔案系統,URL等方式來定為資源位置。如果是XmlBeanFactory作為IOC容器,那麼需要為它指定bean定義的資源,也就是說bean定義檔案時通過抽象成Resource來被IOC容器處理的,容器通過BeanDefinitionReader來完成定義資訊的解析和Bean資訊的註冊,往往使用的是XmlBeanDefinitionReader來解析bean的xml定義檔案-實際的處理過程是委託給BeanDefinitionParserDelegate來完成的,從而得到bean的定義資訊,這些資訊在Spring中使用BeanDefinition物件來表示-這個名字可以讓我們想到loadBeanDefinition,RegisterBeanDefinition這些相關方法-他們都是為處理BeanDefinitin服務的,容器解析得到BeanDefinition以後,需要把它在IOC容器中註冊,這由IOC實現BeanDefinitionRegistry介面來實現。註冊過程就是在IOC容器內部維護的一個HashMap來儲存得到的BeanDefinition的過程。這個HashMap是IOC容器持有Bean資訊的場所,以後對Bean的操作都是圍繞這個HashMap來實現的。

然後我們就可以通過BeanFactory和ApplicationContext來享受到SpringIOC的服務了,在使用IOC容器的時候,我們注意到除了少量粘合程式碼,絕大多數以正確IOC風格編寫的應用程式程式碼完全不用關心如何到達工廠,因為容器將把這些物件與容器管理的其他物件鉤在一起。基本的策略是把工廠放到已知的地方,最好是放在對預期使用的上下文有意義的地方,以及程式碼將實際需要訪問工廠的地方。Spring本身提供了對宣告式載入web應用程式用法的應用程式上下文,並將其儲存在ServletContext中的框架實現。

以下是容器初始化全過程的時序圖:

在使用SpringIOC容器的時候我們還需要區別兩個概念:

BeanFactory和FactoryBean,其中BeanFactory指的是IOC容器的程式設計抽象,比如ApplicationContext,XmlBeanFactory等,這些都是IOC容器的具體表現,需要使用什麼樣的容器由客戶決定,但Spring為我們提供了豐富的選擇。FactoryBean只是一個可以在IOC而容器中被管理的一個Bean,是對各種處理過程和資源使用的抽象,FactoryBean在需要時產生另一個物件,而不返回FactoryBean本身,我們可以把它看成是一個抽象工廠,對它的呼叫返回的是工廠生產的產品。所有的FactoryBean都實現特殊的org.springframework.beans.factory.FactoryBean介面,當使用容器中FactoryBean的時候,該容器不會返回FactoryBean本身,而是返回其生成的物件。Spring包括了大部分的通用資源和服務訪問抽象的FactoryBean的實現,其中包括:對JNDI查詢的處理,對代理物件的處理,對事務性代理的處理,對RMI代理的處理等,這些我們都可以看成是具體的工廠,看成是Spring為我們建立好的工廠。也就是說Spring通過使用抽象工廠模式為我們準備了一系列工廠來生產一些特定的物件,免除我們手工重複的工作,我們要使用時只需要在IOC容器裡配置好就能很方便的使用了。