Spring——IOC(控制反轉)、DI(依賴注入)

一、概述

1.1、IoC是什麼

  Ioc—Inversion of Control,即“控制反轉”,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味着將你設計好的對象交給容器控制,而不是傳統的在你的對象內部直接控制。如何理解好Ioc呢?理解好Ioc的關鍵是要明確“誰控制誰,控制什麼,爲何是反轉(有反轉就應該有正轉了),哪些方面反轉了”,那我們來深入分析一下:

  ●誰控制誰,控制什麼:傳統Java SE程序設計,我們直接在對象內部通過new進行創建對象,是程序主動去創建依賴對象;而IoC是有專門一個容器來創建這些對象,即由Ioc容器來控制對 象的創建;誰控制誰?當然是IoC 容器控制了對象;控制什麼?那就是主要控制了外部資源獲取(不只是對象包括比如文件等)。

  ●爲何是反轉,哪些方面反轉了:有反轉就有正轉,傳統應用程序是由我們自己在對象中主動控制去直接獲取依賴對象,也就是正轉;而反轉則是由容器來幫忙創建及注入依賴對象;爲何是反轉?因爲由容器幫我們查找及注入依賴對象,對象只是被動的接受依賴對象,所以是反轉;哪些方面反轉了?依賴對象的獲取被反轉了。

1.2、IoC能做什麼

  IoC 不是一種技術,只是一種思想,一個重要的面向對象編程的法則,它能指導我們如何設計出松耦合、更優良的程序。傳統應用程序都是由我們在類內部主動創建依賴對象,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把創建和查找依賴對象的控制權交給了容器,由容器進行注入組合對象,所以對象與對象之間是 鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程序的整個體系結構變得非常靈活。

  其實IoC對編程帶來的最大改變不是從代碼上,而是從思想上,發生了“主從換位”的變化。應用程序原本是老大,要獲取什麼資源都是主動出擊,但是在IoC/DI思想中,應用程序就變成被動的了,被動的等待IoC容器來創建並注入它所需要的資源了。

  IoC很好的體現了面向對象設計法則之一—— 好萊塢法則:“別找我們,我們找你”;即由IoC容器幫對象找相應的依賴對象並注入,而不是由對象主動去找。

1.3、IoC和DI

  DI—Dependency Injection,即“依賴注入”:組件之間依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個依賴關係注入到組件之中。依賴注入的目的並非爲軟件系統帶來更多功能,而是爲了提升組件重用的頻率,併爲系統搭建一個靈活、可擴展的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。

  理解DI的關鍵是:“誰依賴誰,爲什麼需要依賴,誰注入誰,注入了什麼”,那我們來深入分析一下:

  ●誰依賴於誰:當然是應用程序依賴於IoC容器;

  ●爲什麼需要依賴:應用程序需要IoC容器來提供對象需要的外部資源;

  ●誰注入誰:很明顯是IoC容器注入應用程序某個對象,應用程序依賴的對象;

  ●注入了什麼:就是注入某個對象所需要的外部資源(包括對象、資源、常量數據)。

  IoC和DI由什麼關係呢?其實它們是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解爲容器控制對象這一個層面,很難讓人想到誰來維護對象關係),所以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴注入”,相對IoC 而言,“依賴注入”明確描述了“被注入對象依賴IoC容器配置依賴對象”。

  看過很多對Spring的Ioc理解的文章,好多人對Ioc和DI的解釋都晦澀難懂,反正就是一種說不清,道不明的感覺,讀完之後依然是一頭霧水,感覺就是開濤這位技術牛人寫得特別通俗易懂,他清楚地解釋了IoC(控制反轉) 和DI(依賴注入)中的每一個字,讀完之後給人一種豁然開朗的感覺。我相信對於初學Spring框架的人對Ioc的理解應該是有很大幫助的。

二、IoC思想

首先想說說IoC(Inversion of Control,控制倒轉)。這是spring的核心,貫穿始終。所謂IoC,對於spring框架來說,就是由spring來負責控制對象的生命週期和對象間的關係。這是什麼意思呢,舉個簡單的例子,我們是如何找女朋友的?常見的情況是,我們到處去看哪裏有長得漂亮身材又好的mm,然後打聽她們的興趣愛好、qq號、電話號、ip號、iq號………,想辦法認識她們,投其所好送其所要,然後嘿嘿……這個過程是複雜深奧的,我們必須自己設計和麪對每個環節。傳統的程序開發也是如此,在一個對象中,如果要使用另外的對象,就必須得到它(自己new一個,或者從JNDI中查詢一個),使用完之後還要將對象銷燬(比如Connection等),對象始終會和其他的接口或類藕合起來。

那麼IoC是如何做的呢?有點像通過婚介找女朋友,在我和女朋友之間引入了一個第三者:婚姻介紹所。婚介管理了很多男男女女的資料,我可以向婚介提出一個列表,告訴它我想找個什麼樣的女朋友,比如長得像李嘉欣,身材像林熙雷,唱歌像周杰倫,速度像卡洛斯,技術像齊達內之類的,然後婚介就會按照我們的要求,提供一個mm,我們只需要去和她談戀愛、結婚就行了。簡單明瞭,如果婚介給我們的人選不符合要求,我們就會拋出異常。整個過程不再由我自己控制,而是有婚介這樣一個類似容器的機構來控制。Spring所倡導的開發方式就是如此,所有的類都會在spring容器中登記,告訴spring你是個什麼東西,你需要什麼東西,然後spring會在系統運行到適當的時候,把你要的東西主動給你,同時也把你交給其他需要你的東西。所有的類的創建、銷燬都由spring來控制,也就是說控制對象生存週期的不再是引用它的對象,而是spring。對於某個具體的對象而言,以前是它控制其他對象,現在是所有對象都被spring控制,所以這叫控制反轉。

IoC的一個重點是在系統運行中,動態的向某個對象提供它所需要的其他對象。這一點是通過DI(Dependency Injection,依賴注入)來實現的。比如對象A需要操作數據庫,以前我們總是要在A中自己編寫代碼來獲得一個Connection對象,有了 spring我們就只需要告訴spring,A中需要一個Connection,至於這個Connection怎麼構造,何時構造,A不需要知道。在系統運行時,spring會在適當的時候製造一個Connection,然後像打針一樣,注射到A當中,這樣就完成了對各個對象之間關係的控制。A需要依賴 Connection才能正常運行,而這個Connection是由spring注入到A中的,依賴注入的名字就這麼來的。

三、Spring IoC總覽

Spring的IoC容器在實現控制反轉和依賴注入的過程中,可以劃分爲兩個階段:

  • 容器啓動階段
  • Bean實例化階段

 

四、容器啓動階段的講解

1、IOC的技術實現方式

“夥計,來杯啤酒!”當你來到酒吧,想要喝杯啤酒的時候,通常會直接招呼服務生,讓他爲你

送來一杯清涼解渴的啤酒。同樣地,作爲被注入對象,要想讓IoC容器爲其提供服務,並

將所需要的被依賴對象送過來,也需要通過某種方式通知對方。

  • 如果你是酒吧的常客,或許你剛坐好,服務生已經將你最常喝的啤酒放到了你面前
  • 如果你是初次或偶爾光顧,也許你坐下之後還要招呼服務生,“Waiter,Tsingdao, please.”
  • 還有一種可能,你根本就不知道哪個牌子是哪個牌子,這時,你只能打手勢或乾脆畫出商標

圖來告訴服務生你到底想要什麼了吧!

不管怎樣,你終究會找到一種方式來向服務生表達你的需求,以便他爲你提供適當的服務。那麼,在IoC模式中,被注入對象又是通過哪些方式來通知IoC容器爲其提供適當服務的呢?

常用的有兩種方式:構造方法注入和setter方法注入,還有一種已經退出歷史舞臺的接口注入方式,下面就比較一下三種注入方式:

  • 接口注入。從注入方式的使用上來說,接口注入是現在不甚提倡的一種方式,基本處於“退

役狀態”。因爲它強制被注入對象實現不必要的接口,帶有侵入性。而構造方法注入和setter

方法注入則不需要如此。

  • 構造方法注入。這種注入方式的優點就是,對象在構造完成之後,即已進入就緒狀態,可以

馬上使用。缺點就是,當依賴對象比較多的時候,構造方法的參數列表會比較長。而通過反

射構造對象的時候,對相同類型的參數的處理會比較困難,維護和使用上也比較麻煩。而且

在Java中,構造方法無法被繼承,無法設置默認值。對於非必須的依賴處理,可能需要引入多個構造方法,而參數數量的變動可能造成維護上的不便。

  • setter方法注入。因爲方法可以命名,所以setter方法注入在描述性上要比構造方法注入好一些。 另外,setter方法可以被繼承,允許設置默認值,而且有良好的IDE支持。缺點當然就是對象無法在構造完成後馬上進入就緒狀態。

其實,這些操作都是由IoC容器來做的,我們所要做的,就是調用IoC容器來獲得對象而已。

2、IoC容器及IoC容器如何獲取對象間的依賴關係

Spring中提供了兩種IoC容器:

  • BeanFactory
  • ApplicationContext

ApplicationContext是BeanFactory的子類,所以,ApplicationContext可以看做更強大的BeanFactory,他們兩個之間的區別如下:

  • BeanFactory。基礎類型IoC容器,提供完整的IoC服務支持。如果沒有特殊指定,默認採用延遲初始化策略(lazy-load)。只有當客戶端對象需要訪問容器中的某個受管對象的時候,纔對該受管對象進行初始化以及依賴注入操作。所以,相對來說,容器啓動初期速度較快,所需要的資源有限。對於資源有限,並且功能要求不是很嚴格的場景,BeanFactory是比較合適的IoC容器選擇。
  • ApplicationContext。ApplicationContext在BeanFactory的基礎上構建,是相對比較高級的容器實現,除了擁有BeanFactory的所有支持,ApplicationContext還提供了其他高級特性,比如事件發佈、國際化信息支持等,ApplicationContext所管理的對象,在該類型容器啓動之後,默認全部初始化並綁定完成。所以,相對於BeanFactory來說,ApplicationContext要求更多的系統資源,同時,因爲在啓動時就完成所有初始化,容

器啓動時間較之BeanFactory也會長一些。在那些系統資源充足,並且要求更多功能的場景中,ApplicationContext類型的容器是比較合適的選擇。

但是我們無論使用哪個容器,我們都需要通過某種方法告訴容器關於對象依賴的信息,只有這樣,容器才能合理的創造出對象,否則,容器自己也不知道哪個對象依賴哪個對象,如果胡亂注入,那不是創造出一個四不像。理論上將我們可以通過任何方式來告訴容器對象依賴的信息,比如我們可以通過語音告訴他,但是並沒有人實現這樣的代碼,所以我們還是老老實實使用Spring提供的方法吧:

  • 通過最基本的文本文件來記錄被注入對象和其依賴對象之間的對應關係
  • 通過描述性較強的XML文件格式來記錄對應信息
  • 通過編寫代碼的方式來註冊這些對應信息
  • 通過註解方式來註冊這些對應信息

雖然提供了四種方式,但是我們一般只使用xml文件方式和註解方式,所以,就重點講解這兩種方式。

3、萬里長征第一步:加載配置文件信息

我們在介紹了一些基本的概念後,終於要迎來容器創造對象的第一步,那就是加載配置文件信息,我們已經知道我們主要通過xml文件和註解的方式來告訴容器對象間的依賴信息,那麼容器怎麼才能從xml配置文件中得到對象依賴的信息呢?且聽我慢慢道來。(這裏的容器指的是BeanFactory,至於ApplicationContext,以後會有相應的講解)

在BeanFactory容器中,每一個注入對象都對應一個BeanDefinition實例對象,該實例對象負責保存注入對象的所有必要信息,包括其對應的對象的class類型、是否是抽象類、構造方法參數以及其他屬性等。當客戶端向BeanFactory請求相應對象的時候,BeanFactory會通過這些信息爲客戶端返回一個完備可用的對象實例。

那麼BeanDefinition實例對象的信息是從哪而來呢?這裏就要引出一個專門加載解析配置文件的類了,他就是BeanDefinitionReader,對應到xml配置文件,就是他的子類XmlBeanDefinitionReader,XmlBeanDefinitionReader負責讀取Spring指定格式的XML配置文件並解析,之後將解析後的文件內容映射到相應的BeanDefinition。在我們瞭解了怎麼得到對象依賴的信息,並知道這些信息最終保存在BeanDefinition之後,我們可能會想,那麼容器怎麼通過這些信息創造出一個可用的對象了呢?

4、籠統講解容器中對象的創建和獲取

我們把容器創造一個對象的過程稱爲Bean的註冊,實現Bean的註冊的接口爲BeanDefinitionRegistry,其實BeanFactory只是一個接口,他定義瞭如何獲取容器內對象的方法,我們所說的BeanFactory容器,其實是這個接口的是實現類,但是具體的BeanFactory實現類同時也會實現BeanDefinitionRegistry接口,這樣我們才能通過容器註冊對象和獲取對象。我們通過BeanDefinitionRegistry的rsgisterBeanDefinition(BeanDefinition beandefinition)方法來進行Bean的註冊。

打個比方說,BeanDefinitionRegistry就像圖書館的書架,所有的書是放在書架上的。雖然你還書或者借書都是跟圖書館(也就是BeanFactory)打交道,但書架纔是圖書館存放各類圖書的地方。所以,書架相對於圖書館來說,就是它的BeanDefinitionRegistry。

我們來總結一下一個Bean是如何註冊到容器中,然後被我們獲取的:

首先我們需要配置該Bean的依賴信息,通常我們配置在xml文件中,然後我們通過XmlBeanDefinitionReader讀取文件內容,然後將文件內容映射到相應的BeanDefinition,然後我們可以通過BeanFactory和BeanDefinitionRegistry的具體實現類,比如DefaultListableBeanFactory實現Bean的註冊和獲取。這裏放一段代碼來演示一下這個過程:

public static void main(String[] args)
{
    //創建一個容器
     DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
     //調用方法實現Bean的註冊
     BeanFactory container = (BeanFactory)bindViaCode(beanRegistry);
     //通過容器獲取對象
     FXNewsProvider newsProvider =  (FXNewsProvider)container.getBean("djNewsProvider");
}
public static BeanFactory bindViaCode(BeanDefinitionRegistry registry)
{
     AbstractBeanDefinition newsProvider = new RootBeanDefinition(FXNewsProvider.class,true);
 
     AbstractBeanDefinition newsListener = new RootBeanDefinition(DowJonesNewsListener.class,true);
 
     AbstractBeanDefinition newsPersister = new RootBeanDefinition(DowJonesNewsPersister.class,true);
 
     // 將bean定義註冊到容器中
     registry.registerBeanDefinition("djNewsProvider", newsProvider);
     registry.registerBeanDefinition("djListener", newsListener);
     registry.registerBeanDefinition("djPersister", newsPersister);
     // 指定依賴關係
     // 1. 可以通過構造方法注入方式
     ConstructorArgumentValues argValues = new ConstructorArgumentValues();
     argValues.addIndexedArgumentValue(0, newsListener);
     argValues.addIndexedArgumentValue(1, newsPersister);
     newsProvider.setConstructorArgumentValues(argValues);
     // 2. 或者通過setter方法注入方式
     MutablePropertyValues propertyValues = new MutablePropertyValues();
     propertyValues.addPropertyValue(new ropertyValue("newsListener",newsListener));
     propertyValues.addPropertyValue(new PropertyValue("newPersistener",newsPersister));
     newsProvider.setPropertyValues(propertyValues);
     // 綁定完成
     return (BeanFactory)registry;
} 

 

五、Bean的生命週期

 

1、Bean的實例化和屬性設置

當我們完成了容器的啓動階段後,對於BeanFactory來說,並不會馬上實例化相應的bean定義。我們知道,容器現在僅僅擁有所有對象的BeanDefinition來保存實例化階段將要用的必要信息。只有當請求方通過BeanFactory的getBean()方法來請求某個對象實例的時候,纔有可能觸發Bean實例化階段的活動BeanFactory的getBean()法可以被客戶端對象顯式調用,也可以在容器內部隱式地被調用。隱式調用有如下兩種情況:

  • 對於BeanFactory來說,對象實例化默認採用延遲初始化。通常情況下,當對象A被請求而需要第一次實例化的時候,如果它所依賴的對象B之前同樣沒有被實例化,那麼容器會先實例化對象A所依賴的對象。這時容器內部就會首先實例化對象B,以及對象 A依賴的其他還沒有實例化的對象。這種情況是容器內部調用getBean(),對於本次請求的請求方是隱式的。
  • ApplicationContext啓動之後會實例化所有的bean定義,但ApplicationContext在實現的過程中依然遵循Spring容器實現流程的兩個階段,只不過它會在啓動階段的活動完成之後,緊接着調用註冊到該容器的所有bean定義的實例化方法getBean()。這就是爲什麼當你得到ApplicationContext類型的容器引用時,容器內所有對象已經被全部實例化完成。不信你查一下類org.AbstractApplicationContext的refresh()方法。

容器在實現Bean的實例化的時候,採用“策略模式(Strategy Pattern)"來決定採用何種方式初始化bean實例。通常,可以通過反射或者CGLIB動態字節碼生成來初始化相應的bean實例或者動態生成其子類。

這裏就涉及到了一些AOP的知識,我們只需要知道在容器中並不是直接通過new的方式來添加對象,而是通過AOP實現機制,創造了一個目標對象的代理對象就可以了,代理對象可以簡單理解爲目標對象的子類,他要麼和目標對象有相同的功能,要麼能力比目標對象強大,AOP我會在以後進行詳細講解。

但是容器也並不是直接就創造了一個代理對象,他還把這個代理對象包裝了一下,他把代理對象包裝成了一個BeanWrapper。

BeanWrapper定義繼承了org.springframework.beans.PropertyAccessor接口,可以以統一的方式對對象屬性進行訪問;BeanWrapper定義同時又直接或者間接繼承了PropertyEditorRegistry和TypeConverter接口。不知你是否還記得CustomEditorConfigurer?當把各種PropertyEditor註冊給容器時,知道後面誰用到這些PropertyEditor嗎?對,就是BeanWrapper!在第一步構造完成對象之後,Spring會根據對象實例構造一個BeanWrapperImpl實例,然後將之前CustomEditorConfigurer註冊的PropertyEditor複製一份給BeanWrapperImpl實例(這就是BeanWrapper同時又是PropertyEditorRegistry的原因)。然後我們就可以通過BeanWrapper來爲對象設置屬性了。

2、Aware接口

到這裏圖中的前兩個過程就已經走完了,接下來,容器會檢查當前對象實例是否實現了一系列的以Aware命名結尾的接口定義。如果是,則將這些Aware接口定義中規定的依賴注入給當前對象實例。我們可以看一看這些Aware對象到底規定了什麼依賴,對於BeanFactory來說,Aware接口有一下幾個:

  • org.springframework.beans.factory.BeanNameAware。如果Spring容器檢測到當前對象實例實現了該接口,會將該對象實例的bean定義對應的beanName設置到當前對象實例。
  • org.springframework.beans.factory.BeanClassLoaderAware。如果容器檢測到當前對

象實例實現了該接口,會將對應加載當前bean的Classloader注入當前對象實例。默認會使用加載org.springframework.util.ClassUtils類的Classloader。

  • org.springframework.beans.factory.BeanFactoryAware。如果對象聲明實現了

BeanFactoryAware接口,BeanFactory容器會將自身設置到當前對象實例。這樣,當前對象實例就擁有了一個BeanFactory容器的引用,並且可以對這個容器內允許訪問的對象按照需要進行訪問。

對於ApplicationContext類型的容器,也存在幾個Aware相關接口。如下:

  • org.springframework.context.ResourceLoaderAware。 ApplicationContext實現了Spring的ResourceLoader接口。當容器檢測到當前對象實例實現了ResourceLoaderAware接口之後,會將當前ApplicationContext自身設置到對象實例,這樣當前對象實例就擁有了其所在ApplicationContext容器的一個引用。
  • org.springframework.context.ApplicationEventPublisherAware。ApplicationContext

作爲一個容器,同時還實現了ApplicationEventPublisher接口,這樣,它就可以作爲ApplicationEventPublisher來使用。所以,當前ApplicationContext容器如果檢測到當前實例化的對象實例聲明瞭ApplicationEventPublisherAware接口,則會將自身注入當前對象。

  • org.springframework.context.MessageSourceAware。ApplicationContext通過MessageSource接口提供國際化的信息支持,即I18n(Internationalization)。它自身就實現了MessageSource接口,所以當檢測到當前對象實例實現了MessageSourceAware接口,則會將自身注入當前對象實例。
  • org.springframework.context.ApplicationContextAware。 如果ApplicationContext容器檢測到當前對象實現了ApplicationContextAware接口,則會將自身注入當前對象實例。

在瞭解了這些Aware接口的功能後,我們可能會想容器是如何實現將Aware接口中規定的依賴注入到已經生成的對象中的呢?這裏就要引出我們在容器實例化階段的擴展點了,那就是BeanPostProcessor

3、BeanPostProcessor

與BeanFactoryPostProcessor通常會處理容器內所有符合條件的BeanDefinition類似,BeanPostProcessor會處理容器內所有符合條件的實例化後的對象實例。

我們已經知道BeanFactoryPostProcessor是在容器啓動階段,對象還未創建之前對創建對象的信息就是BeanDefinition進行了修改,那麼BeanPostProcessor是如何對一個已經生成的對象進行擴展的呢,這裏當然就要用到AOP了,看來,Spring中的IoC和AOP真是“你中有我,我中有你”啊。

我們ApplicationContext對應的那些Aware接口實際上就是通過BeanPostProcessor的方式進行處理的。當ApplicationContext中每個對象的實例化過程走到BeanPostProcessor前置處理這一步時,ApplicationContext容器會檢測到之前註冊到容器的ApplicationContextAwareProcessor這個BeanPostProcessor的實現類,然後就會調用其postProcessBeforeInitialization()方法,檢查並設置Aware相關依賴。

至於如何將BeanPostProcessor註冊到容器中,BeanFactory需要手動的寫代碼注入,而ApplicationContext可以通過配置文件的方式注入

4、init-method

通俗的將,init-method可以指定我們在容器中的獲得的對象在執行任何方法前,先執行那個方法。比如我們在做任何事情前必須要先洗手,那麼我可以把洗手定義爲init-method,那麼我們在做喫飯,睡覺,玩遊戲,寫代碼。。。。之前都會去洗手。

這裏只需要保證FXTradeDateCalculator類中有一個setupHolidays方法就可以了。

5、destory-method

和init-method對應,destory-method定義的是在所有這個對象被銷燬前,需要做的方法。

 

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