1. 程式人生 > >談談我對Spring IOC的理解

談談我對Spring IOC的理解

反轉 頻率 註解 改變 enc encoding 圖1 1.3 ram


轉自京東開濤大神的微博,這是我看過最好的對IOC DI的解釋.

學習過Spring框架的人一定都會聽過Spring的IoC(控制反轉) 、DI(依賴註入)這兩個概念,對於初學Spring的人來說,總覺得IoC 、DI這兩個概念是模糊不清的,是很難理解的,今天和大家分享網上的一些技術大牛們對Spring框架的IOC的理解以及談談我對Spring Ioc的理解。

1.1、IoC是什麽

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

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

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

  用圖例說明一下,傳統程序設計如圖2-1,都是主動去創建相關對象然後再組合起來:

技術分享

圖1-1 傳統應用程序示意圖

  當有了IoC/DI的容器後,在客戶端類中不再主動去創建這些對象了,如圖2-2所示:

技術分享

圖1-2有IoC/DI容器後程序結構示意圖

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的理解應該是有很大幫助的。

二、分享Bromon的blog上對IoC與DI淺顯易懂的講解

2.1、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控制,所以這叫控制反轉。

2.2、DI(依賴註入)

  IoC的一個重點是在系統運行中,動態的向某個對象提供它所需要的其他對象。這一點是通過DI(Dependency Injection,依賴註入)來實現的。比如對象A需要操作數據庫,以前我們總是要在A中自己編寫代碼來獲得一個Connection對象,有了 spring我們就只需要告訴spring,A中需要一個Connection,至於這個Connection怎麽構造,何時構造,A不需要知道。在系統運行時,spring會在適當的時候制造一個Connection,然後像打針一樣,註射到A當中,這樣就完成了對各個對象之間關系的控制。A需要依賴 Connection才能正常運行,而這個Connection是由spring註入到A中的,依賴註入的名字就這麽來的。那麽DI是如何實現的呢? Java 1.3之後一個重要特征是反射(reflection),它允許程序在運行的時候動態的生成對象、執行對象的方法、改變對象的屬性,spring就是通過反射來實現註入的。

  理解了IoC和DI的概念後,一切都將變得簡單明了,剩下的工作只是在spring的框架中堆積木而已。

三、我對IoC(控制反轉)和DI(依賴註入)的理解

  在平時的java應用開發中,我們要實現某一個功能或者說是完成某個業務邏輯時至少需要兩個或以上的對象來協作完成,在沒有使用Spring的時候,每個對象在需要使用他的合作對象時,自己均要使用像new object() 這樣的語法來將合作對象創建出來,這個合作對象是由自己主動創建出來的,創建合作對象的主動權在自己手上,自己需要哪個合作對象,就主動去創建,創建合作對象的主動權和創建時機是由自己把控的,而這樣就會使得對象間的耦合度高了,A對象需要使用合作對象B來共同完成一件事,A要使用B,那麽A就對B產生了依賴,也就是A和B之間存在一種耦合關系,並且是緊密耦合在一起,而使用了Spring之後就不一樣了,創建合作對象B的工作是由Spring來做的,Spring創建好B對象,然後存儲到一個容器裏面,當A對象需要使用B對象時,Spring就從存放對象的那個容器裏面取出A要使用的那個B對象,然後交給A對象使用,至於Spring是如何創建那個對象,以及什麽時候創建好對象的,A對象不需要關心這些細節問題(你是什麽時候生的,怎麽生出來的我可不關心,能幫我幹活就行),A得到Spring給我們的對象之後,兩個人一起協作完成要完成的工作即可。

  所以控制反轉IoC(Inversion of Control)是說創建對象的控制權進行轉移,以前創建對象的主動權和創建時機是由自己把控的,而現在這種權力轉移到第三方,比如轉移交給了IoC容器,它就是一個專門用來創建對象的工廠,你要什麽對象,它就給你什麽對象,有了 IoC容器,依賴關系就變了,原先的依賴關系就沒了,它們都依賴IoC容器了,通過IoC容器來建立它們之間的關系。

  這是我對Spring的IoC(控制反轉)的理解。DI(依賴註入)其實就是IOC的另外一種說法,DI是由Martin Fowler 在2004年初的一篇論文中首次提出的。他總結:控制的什麽被反轉了?就是:獲得依賴對象的方式反轉了。

四、小結

  對於Spring Ioc這個核心概念,我相信每一個學習Spring的人都會有自己的理解。這種概念上的理解沒有絕對的標準答案,仁者見仁智者見智。如果有理解不到位或者理解錯的地方,歡迎廣大園友指正!

---------------------------------------------------------------------------------------------------------------------

引述:IoC(控制反轉:Inverse of Control)是Spring容器的內核,AOP、聲明式事務等功能在此基礎上開花結果。但是IoC這個重要的概念卻比較晦澀隱諱,不容易讓人望文生義,這不能不說是一大遺憾。不過IoC確實包括很多內涵,它涉及代碼解耦、設計模式、代碼優化等問題的考量,我們打算通過一個小例子來說明這個概念。

通過實例理解IoC的概念

賀歲大片在中國已經形成了一個傳統,每到年底總有多部賀歲大片紛至沓來讓人應接不暇。在所有賀歲大片中,張之亮的《墨攻》算是比較出彩的一部。該片講述了戰國時期墨家人革離幫助梁國反抗趙國侵略的個人英雄主義故事,恢宏壯闊、渾雄凝重的歷史場面相當震撼。其中有一個場景:當劉德華所飾演的墨者革離到達梁國都城下,城上梁國守軍問到:“來者何人?”劉德華回答:“墨者革離!”我們不妨通過一個Java類為這個“城門叩問”的場景進行編劇,並借此理解IoC的概念:
代碼清單3-1 MoAttack:通過演員安排劇本

Java代碼 技術分享
  1. public class MoAttack {
  2. public void cityGateAsk(){
  3. //①演員直接侵入劇本
  4. LiuDeHua ldh = new LiuDeHua();
  5. ldh.responseAsk("墨者革離!");
  6. }
  7. }


我們會發現以上劇本在①處,作為具體角色飾演者的劉德華直接侵入到劇本中,使劇本和演員直接耦合在一起(圖3-1)。
技術分享
一個明智的編劇在劇情創作時應圍繞故事的角色進行,而不應考慮角色的具體飾演者,這樣才可能在劇本投拍時自由地遴選任何適合的演員,而非綁定在劉德華一人身上。通過以上的分析,我們知道需要為該劇本主人公革離定義一個接口:
代碼清單3-2 MoAttack:引入劇本角色

Java代碼 技術分享
  1. public class MoAttack {
  2. public void cityGateAsk()
  3. {
  4. //①引入革離角色接口
  5. GeLi geli = new LiuDeHua();
  6. //②通過接口開展劇情
  7. geli.responseAsk("墨者革離!");
  8. }
  9. }


在①處引入了劇本的角色——革離,劇本的情節通過角色展開,在拍攝時角色由演員飾演,如②處所示。因此墨攻、革離、劉德華三者的類圖關系如圖 3 2所示:
技術分享
可是,從圖3 2中,我們可以看出MoAttack同時依賴於GeLi接口和LiuDeHua類,並沒有達到我們所期望的劇本僅依賴於角色的目的。但是角色最終必須通過具體的演員才能完成拍攝,如何讓LiuDeHua和劇本無關而又能完成GeLi的具體動作呢?當然是在影片投拍時,導演將LiuDeHua安排在GeLi的角色上,導演將劇本、角色、飾演者裝配起來(圖3-3)。
技術分享
通過引入導演,使劇本和具體飾演者解耦了。對應到軟件中,導演像是一個裝配器,安排演員表演具體的角色。
現在我們可以反過來講解IoC的概念了。IoC(Inverse of Control)的字面意思是控制反轉,它包括兩個內容:

  • 其一是控制
  • 其二是反轉


那到底是什麽東西的“控制”被“反轉”了呢?對應到前面的例子,“控制”是指選擇GeLi角色扮演者的控制權;“反轉”是指這種控制權從《墨攻》劇本中移除,轉交到導演的手中。對於軟件來說,即是某一接口具體實現類的選擇控制權從調用類中移除,轉交給第三方決定。
因為IoC確實不夠開門見山,因此業界曾進行了廣泛的討論,最終軟件界的泰鬥級人物Martin Fowler提出了DI(依賴註入:Dependency Injection)的概念用以代替IoC,即讓調用類對某一接口實現類的依賴關系由第三方(容器或協作類)註入,以移除調用類對某一接口實現類的依賴。“依賴註入”這個名詞顯然比“控制反轉”直接明了、易於理解。

IoC的類型

從註入方法上看,主要可以劃分為三種類型:構造函數註入、屬性註入和接口註入。Spring支持構造函數註入和屬性註入。下面我們繼續使用以上的例子說明這三種註入方法的區別。

構造函數註入

在構造函數註入中,我們通過調用類的構造函數,將接口實現類通過構造函數變量傳入,如代碼清單3-3所示:
代碼清單3-3 MoAttack:通過構造函數註入革離扮演者

Java代碼 技術分享
  1. public class MoAttack {
  2. private GeLi geli;
  3. //①註入革離的具體扮演者
  4. public MoAttack(GeLi geli){
  5. this.geli = geli;
  6. }
  7. public void cityGateAsk(){
  8. geli.responseAsk("墨者革離!");
  9. }
  10. }


MoAttack的構造函數不關心具體是誰扮演革離這個角色,只要在①處傳入的扮演者按劇本要求完成相應的表演即可。角色的具體扮演者由導演來安排,如代碼清單3-4所示:
代碼清單3-4 Director:通過構造函數註入革離扮演者

Java代碼 技術分享
  1. public class Director {
  2. public void direct(){
  3. //①指定角色的扮演者
  4. GeLi geli = new LiuDeHua();
  5. //②註入具體扮演者到劇本中
  6. MoAttack moAttack = new MoAttack(geli);
  7. moAttack.cityGateAsk();
  8. }
  9. }


在①處,導演安排劉德華飾演革離的角色,並在②處,將劉德華“註入”到墨攻的劇本中,然後開始“城門叩問”劇情的演出工作。

屬性註入

有時,導演會發現,雖然革離是影片《墨攻》的第一主角,但並非每個場景都需要革離的出現,在這種情況下通過構造函數註入相當於每時每刻都在革離的飾演者在場,可見並不妥當,這時可以考慮使用屬性註入。屬性註入可以有選擇地通過Setter方法完成調用類所需依賴的註入,更加靈活方便:
代碼清單3-5 MoAttack:通過Setter方法註入革離扮演者

Java代碼 技術分享
  1. public class MoAttack {
  2. private GeLi geli;
  3. //①屬性註入方法
  4. public void setGeli(GeLi geli) {
  5. this.geli = geli;
  6. }
  7. public void cityGateAsk() {
  8. geli.responseAsk("墨者革離");
  9. }
  10. }


MoAttack在①處為geli屬性提供一個Setter方法,以便讓導演在需要時註入geli的具體扮演者。
代碼清單3-6 Director:通過Setter方法註入革離扮演者

Java代碼 技術分享
  1. public class Director {
  2. public void direct(){
  3. GeLi geli = new LiuDeHua();
  4. MoAttack moAttack = new MoAttack();
  5. //①調用屬性Setter方法註入
  6. moAttack.setGeli(geli);
  7. moAttack.cityGateAsk();
  8. }
  9. }


和通過構造函數註入革離扮演者不同,在實例化MoAttack劇本時,並未指定任何扮演者,而是在實例化MoAttack後,在需要革離出場時,才調用其setGeli()方法註入扮演者。按照類似的方式,我們還可以分別為劇本中其他諸如梁王、巷淹中等角色提供註入的Setter方法,這樣,導演就可以根據所拍劇段的不同,註入相應的角色了。

接口註入

將調用類所有依賴註入的方法抽取到一個接口中,調用類通過實現該接口提供相應的註入方法。為了采取接口註入的方式,必須先聲明一個ActorArrangable接口:

Java代碼 技術分享
  1. public interface ActorArrangable {
  2. void injectGeli(GeLi geli);
  3. }


然後,MoAttack實現ActorArrangable接口提供具體的實現:
代碼清單3-7 MoAttack:通過接口方法註入革離扮演者

Java代碼 技術分享
  1. public class MoAttack implements ActorArrangable {
  2. private GeLi geli;
  3. //①實現接口方法
  4. public void injectGeli (GeLi geli) {
  5. this.geli = geli;
  6. }
  7. public void cityGateAsk() {
  8. geli.responseAsk("墨者革離");
  9. }
  10. }


Director通過ActorArrangable的injectGeli()方法完成扮演者的註入工作。
代碼清單3-8 Director:通過接口方法註入革離扮演者

Java代碼 技術分享
  1. public class Director {
  2. public void direct(){
  3. GeLi geli = new LiuDeHua();
  4. MoAttack moAttack = new MoAttack();
  5. moAttack. injectGeli (geli);
  6. moAttack.cityGateAsk();
  7. }
  8. }


由於通過接口註入需要額外聲明一個接口,增加了類的數目,而且它的效果和屬性註入並無本質區別,因此我們不提倡采用這種方式。

通過容器完成依賴關系的註入

雖然MoAttack和LiuDeHua實現了解耦,MoAttack無須關註角色實現類的實例化工作,但這些工作在代碼中依然存在,只是轉移到Director類中而已。假設某一制片人想改變這一局面,在選擇某個劇本後,希望通過一個“海選”或者第三中介機構來選擇導演、演員,讓他們各司其職,那劇本、導演、演員就都實現解耦了。
所謂媒體“海選”和第三方中介機構在程序領域即是一個第三方的容器,它幫助完成類的初始化與裝配工作,讓開發者從這些底層實現類的實例化、依賴關系裝配等工作中脫離出來,專註於更有意義的業務邏輯開發工作。這無疑是一件令人向往的事情,Spring就是這樣的一個容器,它通過配置文件或註解描述類和類之間的依賴關系,自動完成類的初始化和依賴註入的工作。下面是Spring配置文件的對以上實例進行配置的配置文件片斷:

Xml代碼 技術分享
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:p="http://www.springframework.org/schema/p"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
  7. <!--①實現類實例化-->
  8. <bean id="geli" class="LiuDeHua"/>
  9. <bean id="moAttack" class="com.baobaotao.ioc.MoAttack"
  10. p:geli-ref="geli"/><!--②通過geli-ref建立依賴關系-->
  11. </beans>


通過new XmlBeanFactory(“beans.xml”)等方式即可啟動容器。在容器啟動時,Spring根據配置文件的描述信息,自動實例化Bean並完成依賴關系的裝配,從容器中即可返回準備就緒的Bean實例,後續可直接使用之。
Spring為什麽會有這種“神奇”的力量,僅憑一個簡單的配置文件,就能魔法般地實例化並裝配好程序所用的Bean呢?這種“神奇”的力量歸功於Java語言本身的類反射功能。

談談我對Spring IOC的理解