一文讀懂Spring IoC(控制反轉(zhuǎn)) / DI(依賴注入)

聲明:文章的前三部分參考博文:https://www.cnblogs.com/Nouno/p/5706103.html
這篇文章首發(fā)是在我的個(gè)人微信訂閱號Java編程社區(qū)欧募,關(guān)注我的微信訂閱號查看更多文章。

1. IoC理論的背景

我們都知道吠架,在采用面向?qū)ο蠓椒ㄔO(shè)計(jì)的軟件系統(tǒng)中,它的底層實(shí)現(xiàn)都是由N個(gè)對象組成的,所有的對象通過彼此的合作诱鞠,最終實(shí)現(xiàn)系統(tǒng)的業(yè)務(wù)邏輯贱傀。

圖1:軟件系統(tǒng)中耦合的對象

如果我們打開機(jī)械式手表的后蓋,就會看到與上面類似的情形哈打,各個(gè)齒輪分別帶動時(shí)針瑰排、分針和秒 針順時(shí)針旋轉(zhuǎn)殿托,從而在表盤上產(chǎn)生正確的時(shí)間昔穴。圖1中描述的就是這樣的一個(gè)齒輪組镰官,它擁有多個(gè)獨(dú)立的齒輪,這些齒輪相互嚙合在一起吗货,協(xié)同工作泳唠,共同完成某項(xiàng) 任務(wù)。我們可以看到宙搬,在這樣的齒輪組中笨腥,如果有一個(gè)齒輪出了問題,就可能會影響到整個(gè)齒輪組的正常運(yùn)轉(zhuǎn)勇垛。
齒輪組中齒輪之間的嚙合關(guān)系,與軟件系統(tǒng)中對象之間的耦合關(guān)系非常相似脖母。對象之間的耦合關(guān)系是無法避免的,也是必要的闲孤,這是協(xié)同工作的基礎(chǔ)∽患叮現(xiàn)在,伴隨著 工業(yè)級應(yīng)用的規(guī)模越來越龐大讼积,對象之間的依賴關(guān)系也越來越復(fù)雜肥照,經(jīng)常會出現(xiàn)對象之間的多重依賴性關(guān)系,因此币砂,架構(gòu)師和設(shè)計(jì)師對于系統(tǒng)的分析和設(shè)計(jì)建峭,將面臨 更大的挑戰(zhàn)。對象之間耦合度過高的系統(tǒng)决摧,必然會出現(xiàn)牽一發(fā)而動全身的情形亿蒸。

圖2:對象之間復(fù)雜的依賴關(guān)系

耦合關(guān)系不僅會出現(xiàn)在對象與對象之間,也會出現(xiàn)在軟件系統(tǒng)的各模塊之間掌桩,以及軟件系統(tǒng)和硬件系統(tǒng)之間边锁。如何降低系統(tǒng)之間、模塊之間和對象之間的耦合度波岛,是軟件工程永遠(yuǎn)追求的目標(biāo)之一茅坛。為了解決對象之間的耦合度過高的問題,軟件專家Michael Mattson提出了IOC理論则拷,用來實(shí)現(xiàn)對象之間的“解耦”贡蓖,目前這個(gè)理論已經(jīng)被成功地應(yīng)用到實(shí)踐當(dāng)中,很多的J2EE項(xiàng)目均采用了IOC框架產(chǎn)品Spring煌茬。

2. 什么是控制反轉(zhuǎn)(IoC)

IOC是Inversion of Control的縮寫斥铺,多數(shù)書籍翻譯成“控制反轉(zhuǎn)”,還有些書籍翻譯成為“控制反向”或者“控制倒置”坛善。
1996年晾蜘,Michael Mattson在一篇有關(guān)探討面向?qū)ο罂蚣艿奈恼轮辛诰欤紫忍岢隽薎OC 這個(gè)概念。對于面向?qū)ο笤O(shè)計(jì)及編程的基本思想剔交,前面我們已經(jīng)講了很多了肆饶,不再贅述,簡單來說就是把復(fù)雜系統(tǒng)分解成相互合作的對象岖常,這些對象類通過封裝以 后驯镊,內(nèi)部實(shí)現(xiàn)對外部是透明的,從而降低了解決問題的復(fù)雜度腥椒,而且可以靈活地被重用和擴(kuò)展阿宅。IOC理論提出的觀點(diǎn)大體是這樣的:借助于“第三方”實(shí)現(xiàn)具有依 賴關(guān)系的對象之間的解耦,如下圖:

圖3:IOC解耦過程

大家看到了吧笼蛛,由于引進(jìn)了中間位置的“第三方”洒放,也就是IOC容器,使得A滨砍、B往湿、C、D這4個(gè)對象沒有了耦合關(guān)系惋戏,齒輪之間的傳動全部依靠“第三 方”了领追,全部對象的控制權(quán)全部上繳給“第三方”IOC容器,所以响逢,IOC容器成了整個(gè)系統(tǒng)的關(guān)鍵核心绒窑,它起到了一種類似“粘合劑”的作用,把系統(tǒng)中的所有 對象粘合在一起發(fā)揮作用舔亭,如果沒有這個(gè)“粘合劑”些膨,對象與對象之間會彼此失去聯(lián)系,這就是有人把IOC容器比喻成“粘合劑”的由來钦铺。
我們再來做個(gè)試驗(yàn):把上圖中間的IOC容器拿掉订雾,然后再來看看這套系統(tǒng):

圖4:拿掉IoC容器后的系統(tǒng)

我們現(xiàn)在看到的畫面,就是我們要實(shí)現(xiàn)整個(gè)系統(tǒng)所需要完成的全部內(nèi)容矛洞。這時(shí)候洼哎,A、B沼本、C噩峦、D這4個(gè)對象之間已經(jīng)沒有了耦合關(guān)系,彼此毫無聯(lián)系抽兆,這樣 的話壕探,當(dāng)你在實(shí)現(xiàn)A的時(shí)候,根本無須再去考慮B郊丛、C和D了李请,對象之間的依賴關(guān)系已經(jīng)降低到了最低程度。所以厉熟,如果真能實(shí)現(xiàn)IOC容器导盅,對于系統(tǒng)開發(fā)而言, 這將是一件多么美好的事情揍瑟,參與開發(fā)的每一成員只要實(shí)現(xiàn)自己的類就可以了白翻,跟別人沒有任何關(guān)系!
我們再來看看绢片,控制反轉(zhuǎn)(IOC)到底為什么要起這么個(gè)名字滤馍?我們來對比一下:
軟件系統(tǒng)在沒有引入IOC容器之前,如圖1所示底循,對象A依賴于對象B巢株,那么對象A在初始化或者運(yùn)行到某一點(diǎn)的時(shí)候,自己必須主動去創(chuàng)建對象B或者使用已經(jīng)創(chuàng)建的對象B熙涤。無論是創(chuàng)建還是使用對象B阁苞,控制權(quán)都在自己手上。
軟件系統(tǒng)在引入IOC容器之后祠挫,這種情形就完全改變了那槽,如圖3所示,由于IOC容器的加入等舔,對象A與對象B之間失去了直接聯(lián)系骚灸,所以,當(dāng)對象A運(yùn)行到需要對象B的時(shí)候慌植,IOC容器會主動創(chuàng)建一個(gè)對象B注入到對象A需要的地方甚牲。
通過前后的對比,我們不難看出來:對象A獲得依賴對象B的過程,由主動行為變?yōu)榱吮粍有袨榈咏剑刂茩?quán)顛倒過來了鳖藕,這就是“控制反轉(zhuǎn)”這個(gè)名稱的由來。

3. IOC的別名:依賴注入(DI)

2004年只锭,Martin Fowler探討了同一個(gè)問題著恩,既然IOC是控制反轉(zhuǎn),那么到底是“哪些方面的控制被反轉(zhuǎn)了呢蜻展?”喉誊,經(jīng)過詳細(xì)地分析和論證后,他得出了答案:“獲得依賴對 象的過程被反轉(zhuǎn)了”纵顾∥榍眩控制被反轉(zhuǎn)之后,獲得依賴對象的過程由自身管理變?yōu)榱擞蒊OC容器主動注入施逾。于是敷矫,他給“控制反轉(zhuǎn)”取了一個(gè)更合適的名字叫做“依賴 注入(Dependency Injection)”例获。他的這個(gè)答案,實(shí)際上給出了實(shí)現(xiàn)IOC的方法:注入曹仗。所謂依賴注入榨汤,就是由IOC容器在運(yùn)行期間,動態(tài)地將某種依賴關(guān)系注入到對 象之中怎茫。

所以收壕,依賴注入(DI)和控制反轉(zhuǎn)(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器轨蛤,利用依賴關(guān)系注入的方式蜜宪,實(shí)現(xiàn)對象之間的解耦。
我們舉一個(gè)生活中的例子祥山,來幫助理解依賴注入的過程圃验。大家對USB接口和USB設(shè)備應(yīng)該都很熟悉吧,USB為我們使用電腦提供了很大的方便枪蘑,現(xiàn)在有很多的外部設(shè)備都支持USB接口损谦。

圖5:USB接口和USB設(shè)備

現(xiàn)在,我們利用電腦主機(jī)和USB接口來實(shí)現(xiàn)一個(gè)任務(wù):從外部USB設(shè)備讀取一個(gè)文件岳颇。
電腦主機(jī)讀取文件的時(shí)候照捡,它一點(diǎn)也不會關(guān)心USB接口上連接的是什么外部設(shè)備,而且它確實(shí)也無須知道话侧。它的任務(wù)就是讀取USB接口栗精,掛接的外部設(shè)備只要符 合USB接口標(biāo)準(zhǔn)即可。所以瞻鹏,如果我給電腦主機(jī)連接上一個(gè)U盤悲立,那么主機(jī)就從U盤上讀取文件;如果我給電腦主機(jī)連接上一個(gè)外置硬盤新博,那么電腦主機(jī)就從外置 硬盤上讀取文件薪夕。掛接外部設(shè)備的權(quán)力由我作主,即控制權(quán)歸我赫悄,至于USB接口掛接的是什么設(shè)備原献,電腦主機(jī)是決定不了,它只能被動的接受埂淮。電腦主機(jī)需要外部 設(shè)備的時(shí)候姑隅,根本不用它告訴我,我就會主動幫它掛上它想要的外部設(shè)備倔撞,你看我的服務(wù)是多么的到位讲仰。這就是我們生活中常見的一個(gè)依賴注入的例子。在這個(gè)過程 中痪蝇,我就起到了IOC容器的作用鄙陡。
通過這個(gè)例子,依賴注入的思路已經(jīng)非常清楚:當(dāng)電腦主機(jī)讀取文件的時(shí)候冕房,我就把它所要依賴的外部設(shè)備,幫他掛接上趁矾。整個(gè)外部設(shè)備注入的過程和一個(gè)被依賴的對象在系統(tǒng)運(yùn)行時(shí)被注入另外一個(gè)對象內(nèi)部的過程完全一樣毒费。
我們把依賴注入應(yīng)用到軟件系統(tǒng)中,再來描述一下這個(gè)過程:
對象A依賴于對象B,當(dāng)對象 A需要用到對象B的時(shí)候愈魏,IOC容器就會立即創(chuàng)建一個(gè)對象B送給對象A。IOC容器就是一個(gè)對象制造工廠想际,你需要什么培漏,它會給你送去,你直接使用就行了胡本, 而再也不用去關(guān)心你所用的東西是如何制成的牌柄,也不用關(guān)心最后是怎么被銷毀的,這一切全部由IOC容器包辦侧甫。
在傳統(tǒng)的實(shí)現(xiàn)中珊佣,由程序內(nèi)部代碼來控制組件之間的關(guān)系。我們經(jīng)常使用new關(guān)鍵字來實(shí)現(xiàn)兩個(gè)組件之間關(guān)系的組合披粟,這種實(shí)現(xiàn)方式會造成組件之間耦合咒锻。IOC 很好地解決了該問題,它將實(shí)現(xiàn)組件間關(guān)系從程序內(nèi)部提到外部容器守屉,也就是說由容器在運(yùn)行期將組件間的某種依賴關(guān)系動態(tài)注入組件中惑艇。

4. 依賴注入(DI)的方式

最后,我們用兩個(gè)實(shí)例來看DI的注入方式拇泛,代碼也是十分簡單的滨巴!

通過構(gòu)造函數(shù)注入

ClassB 類通過類構(gòu)造函數(shù)被注入到 ClassA 類中
ClassB.java文件

package com.wangc;

public class ClassB {
    public ClassB() {
        System.out.println("hello,我在ClassB的構(gòu)造器里俺叭!");
    }
    public void say() {
        System.out.println("hello,我是ClassB!");
    }
}

ClassA.java文件

package com.wangc;

public class ClassA {
    private ClassB classB;
    public ClassA(ClassB classB) {
        System.out.println("hello恭取,我在ClassA的構(gòu)造器里!");
        this.classB = classB;
    }
    public void sayHello() {
        classB.say();
    }
}

Beans.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="classA" class="com.wangc.ClassA">
   <!-- 如果你要把一個(gè)引用傳遞給一個(gè)對象熄守,那么你需要使用標(biāo)簽的 ref 屬性蜈垮,而如果你要直接傳遞一個(gè)值,那么你應(yīng)該使用 value 屬性柠横。 -->
    <constructor-arg ref="classB" />
   </bean>

   <bean id="classB" class="com.wangc.ClassB"></bean>

</beans>

MainApp.java文件

package com.wangc;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context =new ClassPathXmlApplicationContext("Beans.xml");
        ClassA classA = (ClassA) context.getBean("classA");
        classA.sayHello();
    }
}

運(yùn)行結(jié)果:

hello窃款,我在ClassB的構(gòu)造器里!
hello牍氛,我在ClassA的構(gòu)造器里晨继!
hello,我是ClassB!

通過setter方法注入

當(dāng)容器調(diào)用一個(gè)無參的構(gòu)造函數(shù)或一個(gè)無參的靜態(tài) factory 方法來初始化你的 bean 后,通過容器在你的 bean 上調(diào)用設(shè)值函數(shù)搬俊,基于設(shè)值函數(shù)的 DI 就完成了紊扬。 將上面例子中的ClassA.java文件和Beans.xml文件作如下改動蜒茄,其他文件不變。
ClassA.java文件

package com.wangc;

public class ClassA {
    private ClassB classB;

    public ClassB getClassB() {
        return classB;
    }
    public void setClassB(ClassB classB) {
        System.out.println("hello餐屎,我在setClassB里檀葛!");
        this.classB = classB;
    }
    public void sayHello() {
        classB.say();
    }
}

Beans.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="classA" class="com.wangc.ClassA">
    <!-- 如果你要把一個(gè)引用傳遞給一個(gè)對象,那么你需要使用標(biāo)簽的 ref 屬性腹缩,而如果你要直接傳遞一個(gè)值屿聋,那么你應(yīng)該使用 value 屬性。 -->
        <property name="classB" ref="classB" />
   </bean>

   <bean id="classB" class="com.wangc.ClassB"></bean>

</beans>

運(yùn)行結(jié)果:

hello藏鹊,我在ClassB的構(gòu)造器里润讥!
hello,我在setClassB里盘寡!
hello,我是ClassB!

我的個(gè)人微信公眾號Java編程社區(qū) 歡迎大家加入楚殿。。竿痰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脆粥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子影涉,更是在濱河造成了極大的恐慌变隔,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件常潮,死亡現(xiàn)場離奇詭異弟胀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)喊式,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門孵户,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岔留,你說我怎么就攤上這事夏哭。” “怎么了献联?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵竖配,是天一觀的道長。 經(jīng)常有香客問我里逆,道長进胯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任原押,我火速辦了婚禮胁镐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己盯漂,他們只是感情好颇玷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著就缆,像睡著了一般帖渠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竭宰,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天空郊,我揣著相機(jī)與錄音,去河邊找鬼切揭。 笑死渣淳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伴箩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼鄙漏,長吁一口氣:“原來是場噩夢啊……” “哼嗤谚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起怔蚌,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤巩步,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后桦踊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體椅野,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年籍胯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了竟闪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杖狼,死狀恐怖炼蛤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝶涩,我是刑警寧澤理朋,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站绿聘,受9級特大地震影響嗽上,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜熄攘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一兽愤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦烹看、人聲如沸国拇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酱吝。三九已至,卻和暖如春土思,著一層夾襖步出監(jiān)牢的瞬間务热,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工己儒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留崎岂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓闪湾,卻偏偏與公主長得像冲甘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子途样,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容