Spring框架淺析 -- IoC容器與Bean的生命週期

爲什麼要使用IoC

在介紹Spring IoC容器之前,先讓我們來回顧一下什麼是IoC,權威的概念如下:https://en.wikipedia.org/wiki/Inversion_of_control

簡單說就是,不要再手寫代碼來維護類與類的依賴關係了,而是將依賴關係配置化,交由容器去解析、識別、處理,按照配置文件的方式,完成類及其依賴類的實例化、初始化等工作。

那麼爲什麼要使用IoC呢?其實這個問題的實質是:編碼式實例化Bean有哪些弊端,使用配置方式描述Bean的依賴關係並進行注入又有什麼好處。

編碼式實例化Bean並注入容器的弊端:

  1. 實現不夠靈活,靠編碼來實現Bean的依賴關係,一旦依賴關係發生變化(哪怕只是值發生變化),也需要更改代碼,重新build工程,重新發布;
  2. 業務邏輯與可抽象出來的容器(即業務邏輯所處的環境)混在在一起,牽一髮動全身,領域模型不夠抽象,容器功能也難以沉澱,代碼可複用性差。

而依靠配置描述Bean之間的依賴關係,可針對以上兩點不足進行改進:

 

  1. 依賴關係配置化,與代碼隔離開(如果使用xml文件方式的話);即使使用Annotation的方式,也可以將配置與代碼的主體邏輯部分隔離開,職責更加明晰,改動起來也比較方便;
  2. 業務模塊與通用功能可以抽象出來。使用容器實現通用功能,將複雜業務邏輯落在業務模塊上,二者職責明晰,便於維護。

Spring IoC容器

IoC算是Spring最爲核心的功能之一了,其重點在於對BeanFactory和ApplicationContext的理解。

BeanFactory可認爲是Spring框架中管理Bean的容器,所有需要引入的Bean都會註冊到BeanFactory中。使用者可通過getBean(String beanName)來根據Bean的名稱獲取到該Bean。

ApplicationContext繼承了BeanFactory,顧名思義,ApplicationContext主要保有了應用的上下文信息,上下文中顯然包含了注入的Bean的集合,此外還包含了應用名稱、啓動時間等。

那麼Spring是如何在web容器啓動的時候,獲取啓動事件,將BeanFactory啓動,將Web容器中的上下文獲取並存儲在ApplicationContext中,之後又是如何將需要注入的Bean按照配置化的依賴注入信息,依次有序地注入到BeanFactory中,並對這些需要注入的Bean進行加工、處理(後邊會提到BeanFactoryProcessor、BeanPostProcessor等組件的處理),實現對Bean的生命週期進行管理的呢?我將在下文一一進行分析和介紹。

Spring的配置及與Web容器的集成

以使用tomcat作爲web容器的web工程爲例,通常需要設置web.xml作爲web配置文件。在web.xml中,通常會設置以下listener作爲監聽web容器啓動/銷燬事件的監聽器。

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener
</listener>

ContextLoaderListener實現了javax.servlet.ServletContextListener接口,當web容器啓動時,監聽到javax.servlet.ServletContextEvent事件,通過事件獲取ServletContext,作爲Spring容器啓動的主入口,啓動Spring容器。

Spring IoC容器的啓動過程

ContextLoaderListener作爲主入口,創建ApplicationContext(默認爲XmlWebApplicationContext),然後調用其refresh方法(本質上是調用其父類AbstractApplicationContext的refresh方法)。

在refresh方法的調用鏈中,最爲重要的方法爲:

  1. obtainFreshBeanFactory:對於xml配置方式,解析配置xml,將xml中配置的bean注入到BeanFactory中,並在此過程中解析它的依賴關係;對於Annotation配置方式(需要在xml配置文件中,使用context:component-scan設置掃描包路徑),基於asm,對class文件進行解析,獲取到符合條件的Bean,然後注入到BeanFactory中;
  2. invokeBeanFactoryPostProcessors:調用註冊到BeanFactory中的BeanFactoryPostProcessor集合,依次調用其postProcessBeanFactory方法,在BeanFactory層級,在Bean的生命週期中,對Bean進行加工處理與修飾;
  3. finishBeanFactoryInitialization:將註冊到BeanFactory中的Bean進行處理,完成其生命週期中的實例化、初始化,以及調用BeanPostProcessor等容器級別組件,對Bean進行適當的加工處理與修飾

針對這三個方法,我們將其分爲兩大類:獲取BeanFactory(即1)和Bean的創建、實例化與修飾(即2,3),在下文中,我們將一一加以介紹。

獲取BeanFactory

Bean的配置方式主要有兩種:基於xml配置文件的配置方式和基於Annotation的配置方式。兩種方式下,獲取BeanFactory的過程略有不同。

1. xml方式注入

先看基於xml配置文件的配置方式,Bean是如何注入到容器的。

obtainFreshBeanFactory方法調用鏈中主要進行了如下操作:

  1. 通過AbstractRefreshableApplicationContext的createBeanFactory創建出beanFactory,這是一個DefaultListableBeanFactory,可認爲是BeanFactory的經典默認實現版;
  2. 通過AbstractRefreshableApplicationContext的customizeBeanFactory方法,對上一步創建出來的BeanFactory進行修飾,修飾過程中設置了兩個非常重要的屬性:allowBeanDefinitionOverriding和allowCircularReferences。顧名思義,allowBeanDefinitionOverriding用來標識在BeanFactory中,面對先後兩個同名Bean,是否允許進行覆蓋;而allowCircularReferences標識了是否允許有限度地解決環形依賴的問題(下文中將介紹何爲有限度地支持);
  3. 通過AbstractRefreshableApplicationContext的loadBeanDefinitions加載配置文件中配置的Bean,將其包裝成爲BeanDefinition。這一步應該算是最爲重要的一步了,調用鏈也很長,但我們只需要抓住其中幾個比較重要的點:從上層開始,層層調用到DefaultDocumentLoader的createDocumentBuilder方法,創建DocumentBuilder。也就是說,spring是通過dom來實現xml文件的解析與xml結構的加載,所以我個人其實並不建議在xml配置中配置得過於複雜,否則加載速度會比較慢。
  4. 執行完xml文件的加載之後,回到XmlBeanDefinitionReader的registerBeanDefinitions方法,根據上一步解析並加載後的org.w3c.dom.Document,對Bean進行注入。順着調用鏈,層層向下到XmlBeanDefinitionReader的doRegisterBeanDefinitions方法,再到DefaultBeanDefinitionDocumentReader類的parseBeanDefinitions方法中,即可看到會根據XML element是否爲默認namespace,來進行處理。對於默認namespace的,使用parseDefaultElement,對於xml配置方式,對Bean的解析和注入基本都在默認namespace的範圍內;對於自定義namespace的,使用parseCustomElement(對於Annotation的配置方式,需要用到context namespace的解析器)。這一步之後就基本上完成了obtainFreshBeanFactory的調用鏈。

針對剛纔所述的第四步,我們重點進行一下分析。

首先看默認命名空間的情況,會處理四種情況:import, alias, bean, beans,可以結合xml配置文件來看。

import

import的情況通常是在xml配置文件有這樣的配置:

<import resource="xxxx.xml"/>

其作用在於,在主配置xml文件中,引入分xml文件。通常,我們會將jdbc的xml配置文件、rpc的xml配置文件、cache的xml配置文件、mq的xml配置文件單列成一個分xml文件,這樣職責比較單一,整個工程組織也比較明晰。在主配置xml文件中,比如spring-root.xml中,需要import這些分xml文件,以達到將分xml文件中定義的bean也注入到BeanFactory中的效果。

alias

alias的情況通常是在xml配置文件有這樣的配置:

<alias name="xxx" alias="yyy"/>

其作用在於,對於name爲xxx的bean,其具備了別名yyy。不過,在實際工作中,我並沒有這樣使用過。

bean

bean的情況比較常見,簡化版配置如下:

<bean id="xxx" class="yyy"/>

建議使用id屬性來標識Bean。

此外,在此還可以設置bean的scope屬性(作用域,singleton或prototype),lazy-init屬性(是否懶加載),init-method/destroy-method(初始化方法/銷燬方法)等屬性。不過個人比較建議在xml配置中,僅進行簡單的配置。

beans

重複上述過程,解析嵌套在其內部的標籤。

2. 註解方式注入

與上述xml方式注入bean相比,註解方式注入bean僅在剛纔梳理的obtainFreshBeanFactory方法調用鏈的第四步有所不同,即需要根據自定義namespace的方式進行解析。

對於註解方式注入bean而言,通常在spring配置文件中需要配上這麼一段:

<context:component-scan base-package="xx">
  <context:include-filter type="yy" expression="..."/>
  <context:exclude-filter type="zz" expression="..."/>
</context>

通過這一段,我們知道這實際上是使用了context命名空間,在對應的spring-context-xx.xx.xx.jar包的META-INF文件夾下,不難發現context命名空間下對應的NamespaceHandler爲org.springframework.context.config.ContextNamespaceHandler。

再到ContextNamespaceHanlder的init方法下,能夠看到對於component-scan、annotation-config等屬性,均註冊了響應的BeanDefinitionParser。component-scan屬性註冊的BeanDefinitionParser爲ComponentScanBeanDefinitionParser。

好了,現在我們回到obtainFreshBeanFactory方法調用鏈的第四步,回到DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法,對於自定義namespace,需要調用BeanDefinitionParserDelegate的parseCustomElement方法,順着其調用鏈深入下去,可以看到以下這段代碼:

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
		String namespaceUri = getNamespaceURI(ele);
		NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
		if (handler == null) {
			error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
			return null;
		}
		return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
	}

從這段代碼中,我們看到,首先通過獲取到的context的namespaceUri,獲取到NamespaceHandler,也就是剛纔談到的ContextNamespaceHandler,然後調用其parse方法,根據component-scan屬性獲取到對應的BeanDefinitionParser,即ComponentScanBeanDefinitionParser,進而調用其parse方法,完成以下幾步:

  1. 配置Scanner,將xml文件中配置的base-package、include-filter和exclude-filter設置到ClassPathBeanDefinitionScanner中,當然還有默認的識別@Component的AnnotationTypeFilter。根據這些屬性,我們即可知道Spring將掃描哪些類,從而將符合條件的Bean注入到BeanFactory中。此外,我們也明白了爲何@Controller(web層),@Service(服務層),@Repository(數據層)標註的類能夠被注入到BeanFactory中,原因就是他們都是Component,採用不同的標註只是爲了顯式區分各自的功能罷了;
  2. 調用scan.doScan,使用asm技術,從class文件中解析出類的元信息,包括其屬性、方法、標註等,根據上一步設置的限制條件,獲取符合條件的Bean;
  3. 註冊BeanPostProcessor,用於在Bean創建、初始化過程中,對bean進行相應的修飾和加工,重要的BeanPostProcessor有:AutowiredAnnotationBeanPostProcessor,CommonAnnotationBeanPostProcessor,RequiredAnnotationBeanPostProcessor等。在下文,我們會對其進行詳細介紹。

3. 小節

以上介紹了xml方式注入和註解注入兩種注入方式,那麼在實際使用過程中,我們應該如何進行選擇呢?

其實在技術上,很難會有一個放之四海而皆準的金標準,也很難說哪種技術方案一定好,哪種技術方案一定不好,很多時候,都需要結合實際使用情況,結合技術方案的特點,根據業務需求進行權衡,尋找出最爲適合的方案。

xml方式注入的優點是配置-代碼分離,但是缺點是不如註解方式方便;

註解方式注入的優點是可以自動裝配(Autowired),無需層層描述bean與bean的依賴關係,但是缺點是配置與代碼在一起。

因此,在日常使用中,我通常會優先選擇註解方式注入業務相關bean,而把框架相關bean或者說與業務無關的bean,放在xml配置文件中。因爲業務相關bean會隨着業務的發展不斷演進,配置與代碼在一起無傷大雅,且依賴關係較爲複雜,使用註解方式注入,可以很好地利用其自動裝配功能。對於框架相關bean,依賴關係可能還算簡單,且會被多處業務代碼所引用,當升級配置時(如中心註冊服務器地址,或者是資源池大小),如果混在各處業務代碼中,將是一種災難。

 

Bean的創建、初始化與修飾

在上文中,大致介紹瞭如何解析配置文件(或解析代碼中的Annotation),將解析得到的Bean注入到BeanFactory中(其實也就是容器中)。但是這些Bean還沒有被實例化、初始化,也沒有進行必要的加工修飾,還不能對外提供服務。本節將介紹Spring容器是如何將這些Bean創建、初始化的,如何利用容器組件(BeanFactoryPostProcessor,BeanPostProcessor)對Bean進行修飾、加工,使其具備對外服務的能力。

我們還是回到上文中所述的Spring IoC容器的啓動過程,在執行完獲取BeanFactory的方法之後,將執行以下兩個方法:

 

  1. invokeBeanFactoryPostProcessors
  2. finishBeanFactoryInitialization

在方法一中,將調用註冊到BeanFactory中的BeanFactoryPostProcessor集合,依次調用其postProcessBeanFactory方法,在BeanFactory層級,在Bean的生命週期中,對Bean進行加工處理與修飾。我們就不展開介紹了,感興趣的同學可以深入調用鏈看一下Spring容器都有哪些BeanFactoryPostProcessor,又都完成了哪些操作。

在方法二中,將註冊到BeanFactory中的Bean進行處理,完成其生命週期中的實例化、初始化,以及調用BeanPostProcessor等容器級別組件,對Bean進行適當的加工處理與修飾。這將是本節介紹的重點,我們對其進行展開。

 

  1. 沿着DefaultLisableBeanFactory的preInstantiateSingletons方法,一路向下到getBean方法,再到doGetBean方法。以作用域爲Singleton的Bean爲例,進入到AbstractBeanFactory類的getSingleton方法,再到AbstractAutowiredCapableBeanFactory的createBean方法。注意,這個方法和其調用鏈內的doCreateBean方法將是比較重要的兩個方法;
  2. Bean實例化前的加工修飾:在AbstractAutowiredCapableBeanFactory的createBean方法中調用了resolveBeforeInstantiation方法,進而獲取註冊在BeanFactory中的InstantiationAwareBeanPostProcessor集合,並以此調用其postProcessBeforeInstantiation方法,在Bean實例化前進行加工修飾工作;
  3. 實例化Bean:在AbstractAutowiredCapableBeanFactory的createBeanInstance方法中,進而調用getInstantiationStrategy().instantiate(mbd, beanName, parent),利用JAVA反射,實例化Bean;
  4. 在Bean初始化之前,調用後處理器設置屬性:AbstractAutowiredCapableBeanFactory的populateBean方法中,調用InstantiationAwareBeanPostProcessor的postProcessPropertyValues方法,設置屬性。典型的InstantiationAwareBeanPostProcessor有AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor,分別處理bean中,被@Autowired和@Resource註解的屬性Bean,對於這些被依賴的Bean,調用getBean方法獲取其已可對外提供服務的實例,如果還沒有達到可用狀態,則嘗試創建。這裏邊就涉及到環形依賴的問題,在小節中會給出介紹;
  5. 在Bean初始化前,調用後處理器,對Bean進行加工處理:AbstractAutowiredCapableBeanFactory的initializeBean方法中,調用BeanPostProcessor的postProcessBeforeInitialization方法,在bean初始化前進行準備工作。bean中標註了@PostConstruct的函數就是在此時被調用的,處理它的是InitDestroyAnnotationBeanPostProcessor。
  6. 調用bean的初始化方法;
  7. Bean初始化之後,調用後處理器,對Bean進行加工處理:調用BeanPostProcessor的postProcessAfterInitialization方法,在bean初始化後,對Bean進行加工處理。

至此,Bean已經被加工完畢,可對外提供服務。當需要銷燬Bean的時候,調用Bean的destroy方法,對Bean進行銷燬。

 

小節

截止到這裏,我們已經大致介紹完了Spring IoC容器是如何啓動的,如何將Bean包裝成BeanDefinition,又是如何將其注入到BeanFactory中,以及如何在其生命週期中,基於容器內的多種組件對其進行加工、修飾,使其具備了對外提供服務的能力。

相信看完本文之後,你已經能夠回答以下問題:

 

  1. Bean的scope主要有哪些:我們只考慮主要的,Singleton和Prototype,即單例與非單例;
  2. Spring容器中提供了哪些組件,能夠在Bean的生命週期中起到作用:BeanFactoryPostProcessor, BeanPostProcessor;
  3. 被@Autowired標註的Bean是如何作爲屬性,注入到引用它的Bean中的:AutowiredAnnotationBeanPostProcessor在Bean初始化之前,調用postProcessPropertyValues方法,獲取Bean中被標註了@Autowired的屬性,然後將這些屬性對應的Bean按類型進行獲取,實例化、初始化,使用JAVA的反射,將已經可以對外提供服務的Bean設置到屬性上
  4. 被@Resource標註的Bean是如何作爲屬性,注入到引用它的Bean中的:CommonAnnotationBeanPostProcessor在Bean初始化之前,調用postProcessPropertyValues方法,獲取Bean中被標註了@Resource的屬性,然後按照name(沒有設置name就按照type),進行獲取,實例化、初始化,最後使用JAVA的反射,將其設置到屬性上
  5. 被@PostConstruct標註的方法,是在何時被調用的:CommonAnnotationBeanPostProcessor在Bean初始化之前,調用postProcessBeforeInitialization方法(實質上該方法是在其父類裏定義的)。在方法內部,調用@PostConstruct標註的方法
  6. Spring容器對於環形依賴(circle reference)是如何處理的:分兩種情況,對於作用域爲prototype的bean A,經過一系列依賴關係,引用了作用域爲prototype的bean A,拋錯,此時無法解決環形依賴的問題;對於作用域爲singleton的bean A,經過一系列的依賴關係,引用了作用域爲singleton的bean A,由於當bean A實例化的時候,向DefaultSingletonBeanRegistry的singletonsCurrentlyInCreation屬性中添加過bean A的beanName,且向earlySingletonObjects屬性(key-beanName, value-bean object)中添加過bean A的早期半成品,所以可在此直接返回bean A的早期半成品,從而結束環形依賴。我理解這應該是最大限度上對環形依賴的解決方案了。
  7. Bean的生命週期是怎樣的:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章