Spring詳解(三)----認識IoC控制反轉/DI依賴注入


1、本章前言

我們只要提到Spring這個詞,有簡單了解過Spring的人基本上都會脫口而出IoC、DI和AOP這幾個概念。但是對於初學者來說,一下子搞懂IoC和DI的概念還是挺麻煩的。比如之前我自己剛剛學習Spring的時候,只知道IoC能夠幫我們創建對象,不再需要我們自己去創建了,並且那時IoC和DI傻傻分不清,對AOP的概念就更加不用說了。所以這次一定要好好理解。

注意:IoC和AOP這些概念並不是Spring提出來的,它們在Spring出來之前就已經存在了,只是之前更多的是偏向於理論,沒有產品很好的實現,直到Spring框架將這些概念進行了很好的實現。

2、什么是IoC(控制反轉)

IoC(Inversion of Control)的意思是“控制反轉”,它是Spring最核心的點,並且貫穿始終。IoC並不是一門技術,而是一種設計思想。在Spring框架中實現控制反轉的是Spring IoC容器,其具體就是由容器來控制對象的生命周期和業務對象之間的依賴關系,而不是像傳統方式(new 對象)中由代碼來直接控制。程序中所有的對象都會在Spring IoC容器中登記,告訴容器你是個什么,你需要什么,然后IoC容器會在系統運行到適當的時候,把你要的對象主動給你,同時也把你交給其它需要你的對象。也就是說控制對象生存周期的不再是引用它的對象,而是由Spring IoC容器來控制所有對象的創建、銷毀。對於某個具體的對象而言,以前是它控制其它對象,現在是所有對象都被Spring IoC容器所控制,所以這叫控制反轉。

控制反轉最直觀的表達就是,IoC容器讓對象的創建不用去new了,而是由Spring自動生產,使用java的反射機制,根據配置文件在運行時動態的去創建對象以及管理對象,並調用對象的方法。控制反轉的本質是控制權由應用代碼轉到了外部容器(IoC容器),控制權的轉移即是所謂的反轉。控制權的轉移帶來的好處就是降低了業務對象之間的依賴程度,即實現了解耦。即然控制反轉中提到了反轉,那么肯定有正轉,正轉和反轉有什么區別呢?我曾經在博客上看到有人在面試的時候被問到Spring IoC知識點:什么是反轉、正轉?

  • 正轉:如果我們要使用某個對象,就需要自己負責對象的創建。
  • 反轉:如果要使用某個對象,只需要從Spring 容器中獲取需要使用的對象,不關心對象的創建過程,也就是把創建對象的控制權反轉給了Spring框架。

3、什么是DI(依賴注入)

DI(Dependency Injection)的意思是"依賴注入",它是IoC(控制反轉)的一個別名為。在早些年,軟件開發教父Martin·Fowler在一篇文章中提到將IoC改名為 DI,這是原文地址:https://martinfowler.com/articles/injection.html。其中有這樣一段話,如下圖所示:

image

意思是:他認為需要為該模式(IoC)指定一個更具體的名稱。因為控制反轉是一個過於籠統的術語,所以人們會感到困惑。他與IoC的倡導者進行了大量討論之后,然后他們決定使用依賴注入這個名稱。也就是在這時DI(依賴注入)這個詞被大家知曉。我在第一章的時候也提到過,IoC和DI其實是同一個概念,只是從不同的角度描述罷了(IoC是一種思想,而DI則是一種具體的技術實現手段)。

這是我們在其它地方看到的一句話,這句話真的是醍醐灌頂,一句話就把其它人一大堆很難懂的話給說清楚了:IoC是目的(它的目的是創建對象),DI是手段(通過什么手段獲取外部對象)。所以至此我們別再傻傻分不清楚IoC和DI了。

依賴注入:即應用程序在運行時依賴IoC容器來動態注入對象需要的外部資源。依賴注入中"誰依賴誰,為什么需要依賴,誰注入誰,注入了什么",下面來深入分析一下:

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

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

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

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

綜合上述,我們可以用一句話來概括:所謂Spring IoC/DI,就是由 Spring 容器來負責對象的生命周期和對象之間的依賴關系。

4、對SpringIoC的理解

上面已經詳細介紹了IoC和DI的基本概念,為了更好的理解它們,所以接下來用一個生活中的例子來加深理解。在舉例之前,先要搞清楚,依賴關系的處理方式有兩種:

  • 主動創建對象
  • 被動創建對象

①、主動創建對象

我們知道,在傳統的Java項目中,如果需要在一個對象中內部調用另一個對象的方法,最常用的就是在主體類中使用new 對象的方式。當然我們也可以使用簡單工廠模式來實現,就是在簡單工廠模式中,我們的被依賴類由一個工廠方法創建,依賴主體先調用被依賴對象的工廠方法,接着主動基於工廠訪問被依賴對象,但這種方式任然存在問題,即依賴主體與被依賴對象的工廠之間存在着耦合。主動創建對象的程序思想圖如下所示:

image

舉例:這是我在購買的《Java EE 互聯網輕量級框架整合開發》一書中看到的一個栗子,我覺得作者的這個栗子通俗易懂,因為它源自生活。例如我們平時想要喝一杯檸檬汁,在不去飲品店購買的情況下,那么我們自己想要的得到一杯橙汁的想法是這樣的:買果汁機、買橙子,買杯子,然后准備水。這些都是你自己"主動"完成的過程,也就是說一杯橙汁需要你自己創造。如下圖所示:

image


②、被動創建對象

由於主動創建對象的方式是很難避免耦合問題,所以通過思考總結有人通過容器來統一管理對象,然后逐漸引起了大家的注意,進而開啟了被動創建對象的思潮。也正是由於容器的引入,使得應用程序不需要再主動去創建對象了,可見獲取對象的過程被反轉了,從主動獲取變成了被動接受,這也是控制反轉的過程。被動創建對象的程序思想圖如下所示:

image

舉例:在飲品店如此盛行的今天,不會還有人自己在家里制作飲品、奶茶吧!所以我們的首選肯定是去外面購買或者是外賣。那此時我們只需要描述自己需要什么飲品即可(加冰熱糖忽略),不需要在乎我們的飲品是怎么制作的。而這些正是由別人"被動"完成的過程,也就是說一杯飲品需要別人被動創造。如下圖所示:

image

通過上圖的例子我們可以發現,我們得到一杯橙汁並沒有由自己"主動"去創造,而是通過飲品店創造的,然而也完全達到了你的要求,甚至比你創造的要好上那么一些。

上面的例子只能看出不需要我們自己創建對象了,那萬一它還依賴於其它對象呢?那么對象之間要相互調用呢?我們要怎么來理解呢?下面接着舉例。

假如這個飲品店的商家是一個奸商,為了節約成本,它們在飲品中添加添加劑,舉例如下圖所示:

image

在主體對象依賴其它對象的時候,對象之間的相互調用通過注入的方式來完成,所以下面我們介紹IOC中的三種注入方式。

至此為止對Spring IOC/DI的理解已經全部介紹完了,也不知道你們看沒看懂,或者是我本身理解有誤,還請大家多多指教!!!

5、IoC的三種注入方式

對IoC模式最有權威的總結和解釋,應該是軟件開發教父Martin Fowler的那篇文章"Inversion of Control Containers and the Dependency Injection pattern",上面已經給出了鏈接,這里再說一遍:https://martinfowler.com/articles/injection.html。在這篇文章中提到了三種依賴注入的方式,即構造方法注入(constructor injection),setter方法注入(setter injection)以及接口注入(interface injection)。

image

所以下面讓來詳細看一下這三種方式的特點及其相互之間的差別:

5.1、構造函數注入

構造方法注入,顧名思義就是被注入對象可以通過在其構造方法中聲明依賴對象的參數列表,讓外部(通常是IoC容器)知道它需要哪些依賴對象。

IoC Service Provider會檢查被注入對象的構造方法,取得它所需要的依賴對象列表,進而為其注入相應的對象。同一個對象是不可能被構造兩次的,因此,被注入對象的構造乃至其整個生命周期,應該是由IoC Service Provider來管理的。

構造方法注入方式比較直觀,對象被構造完成后,即進入就緒狀態,可以馬上使用。這就好比你剛進酒吧的門,服務生已經將你喜歡的啤酒擺上了桌面一樣。坐下就可馬上享受一份清涼與愜意。


5.2、set方法注入

對於JavaBean對象來說,通常會通過setXXX()和getXXX()方法來訪問對應屬性。這些setXXX()方法統稱為setter方法,getXXX()當然就稱為getter方法。通過setter方法,可以更改相應的對象屬性,通過getter方法,可以獲得相應屬性的狀態。所以,當前對象只要為其依賴對象所對應的屬性添加setter方法,就可以通過setter方法將相應的依賴對象設置到被注入對象中。

setter方法注入雖不像構造方法注入那樣,讓對象構造完成后即可使用,但相對來說更寬松一些,可以在對象構造完成后再注入。這就好比你可以到酒吧坐下后再決定要點什么啤酒,可以要百威,也可以要大雪,隨意性比較強。如果你不急着喝,這種方式當然是最適合你的。


5.3、接口注入

相對於前兩種注入方式來說,接口注入沒有那么簡單明了。被注入對象如果想要IoC ServiceProvider為其注入依賴對象,就必須實現某個接口。這個接口提供一個方法,用來為其注入依賴對象。IoC Service Provider最終通過這些接口來了解應該為被注入對象注入什么依賴對象。


5.4、三種注入方式的比較

注入方式
描述
setter方法注入 因為方法可以命名,所以setter方法注入在描述性上要比構造方法注入好一些。 另外,setter方法可以被繼承,允許設置默認值,而且有良好的IDE支持。缺點當然就是對象無法在構造完成后馬上進入就緒狀態。
構造方法注入 這種注入方式的優點就是,對象在構造完成之后,即已進入就緒狀態,可以 馬上使用。缺點就是,當依賴對象比較多的時候,構造方法的參數列表會比較長。而通過反射構造對象的時候,對相同類型的參數的處理會比較困難,維護和使用上也比較麻煩。而且在Java中,構造方法無法被繼承,無法設置默認值。對於非必須的依賴處理,可能需要引入多個構造方法,而參數數量的變動可能造成維護上的不便。
接口注入 從注入方式的使用上來說,接口注入是現在不甚提倡的一種方式,基本處於“退役狀態”。因為它強制被注入對象實現不必要的接口,帶有侵入性。而構造方法注入和setter方法注入則不需要如此。

綜上所述,構造方法注入和setter方法注入因為其侵入性較弱,且易於理解和使用,所以是現在使用最多的注入方式,尤其是setter方法注入;而接口注入因為侵入性較強,基本已經淘汰了。

6、IoC的使用舉例

IOC的實例講解部分我們任然使用上面橙汁的例子,假如奸商為了節約成本,所以使用了添加劑,那么可以理解為飲品店的橙汁依賴於添加劑,在實際使用中我們要將添加劑對象注入到橙汁對象中。下面我通過這幾種方式來講解對IOC容器實例的應用:

  • 原始方式
  • 構造函數注入
  • setter方法注入
  • 接口注入

首先我們先分別創建橙汁OrangeJuice類和添加劑Additive類。

創建OrangeJuice類,代碼如下:

/**
 * @author tanghaorong
 * @desc 橙汁類
 */
public class OrangeJuice {
    public void needOrangeJuice(){
        System.out.println("消費者點了一杯橙汁(無添加劑)...");
    }
}

創建添加劑Additive類,代碼如下:

/**
 * @author tanghaorong
 * @desc 添加劑類
 */
public class Additive  {
    public void addAdditive(){
        System.out.println("奸商在橙汁中添加了添加劑...");
    }
}

6.1、原始方式

最原始的方式就是沒有IOC容器的情況下,我們要在主體對象中使用new的方式來獲取被依賴對象。我們看一下在主體類中的寫法,添加劑類一直不變:

public class OrangeJuice {
    public void needOrangeJuice(){
        //創建添加劑對象
        Additive additive = new Additive();
        //調用加入添加劑方法
        additive.addAdditive();
        System.out.println("消費者點了一杯橙汁(有添加劑)...");
    }
}

創建測試類:

public class Test {
    public static void main(String[] args) {
        OrangeJuice orangeJuice = new OrangeJuice();
        orangeJuice.needOrangeJuice();
    }
}

運行結果如下:

image

通過上面的例子可以發現,原始方式的耦合度非常的高,如果添加劑的種類改變了,那么整杯橙汁也需要改變。


6.2、構造函數注入

構造器注入,顧名思義就是通過構造函數完成依賴關系的注入。首先我們看一下spring的配置文件:

<?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.xsd"> <!-- bean definitions here -->

    <!--將指定類都配置給Spring,讓Spring創建其對象的實例,一個bean對應一個對象-->
    <bean id="additive" class="com.thr.Additive"></bean>

    <bean id="orangeJuice" class="com.thr.OrangeJuice">
        <!--通過構造函數注入,ref屬性表示注入另一個對象-->
        <constructor-arg ref="additive"></constructor-arg>
    </bean>
</beans>

使用構造函數方式注入的前提必須要在主體類中創建構造函數,所以我們再來看一下,構造器表示依賴關系的寫法,代碼如下所示:

public class OrangeJuice {
    //引入添加劑參數
    private Additive additive;
    //創建有參構造函數
    public OrangeJuice(Additive additive) {
        this.additive = additive;
    }

    public void needOrangeJuice(){
        //調用加入添加劑方法
        additive.addAdditive();
        System.out.println("消費者點了一杯橙汁(有添加劑)...");
    }
}

創建測試類:

public class Test {
    public static void main(String[] args) {
        //1.初始化Spring容器,加載配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2.通過容器獲取實例對象,getBean()方法中的參數是bean標簽中的id
        OrangeJuice orangeJuice = (OrangeJuice) applicationContext.getBean("orangeJuice");
        //3.調用實例中的方法
        orangeJuice.needOrangeJuice();
    }
}

運行結果如下:

image

可以發現運行結果和原始方式一樣,但是將創建對象的權利交給Spring之后,橙汁和添加劑之間的耦合度明顯降低了。此時我們的重點是在配置文件中,而不在乎程序本身,即使添加劑類型發生改變,我們只需修改配置文件即可,不需要修改程序代碼。


6.3、set方法注入

setter注入在實際開發中使用的非常廣泛,因為它可以在對象構造完成后再注入,這樣就更加直觀,也更加自然。我們來看一下spring的配置文件:

<?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.xsd"> <!-- bean definitions here -->

    <!--將指定類都配置給Spring,讓Spring創建其對象的實例,一個bean對應一個對象-->
    <bean id="additive" class="com.thr.Additive"></bean>

    <bean id="orangeJuice" class="com.thr.OrangeJuice">
        <!--通過setter注入,ref屬性表示注入另一個對象-->
        <property name="additive" ref="additive"></property>
    </bean>
</beans>

關於配置文件中的一些元素如<property>、name、ref等會在后面詳細的介紹。

接着我們再來看一下,setter表示依賴關系的寫法:

public class OrangeJuice {
    //引入添加劑參數
    private Additive additive;
    //創建setter方法
    public void setAdditive(Additive additive) {
        this.additive = additive;
    }

    public void needOrangeJuice(){
        //調用加入添加劑方法
        additive.addAdditive();
        System.out.println("消費者點了一杯橙汁(有添加劑)...");
    }
}

測試類和運行的結果和構造器注入的方式是一樣的,所以這里就不展示了。


6.4、接口注入

接口注入,就是主體類必須實現我們創建的一個注入接口,該接口會傳入被依賴類的對象,從而完成注入。

由於Spring的配置文件只支持構造器注入和setter注入,所有這里不能使用配置文件,此時僅僅起到幫我們創建對象的作用。spring的配置文件:

<?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.xsd"> <!-- bean definitions here -->

    <!--將指定類都配置給Spring,讓Spring創建其對象的實例,一個bean對應一個對象-->
    <bean id="additive" class="com.thr.Additive"></bean>

    <bean id="orangeJuice" class="com.thr.OrangeJuice"></bean>
</beans>

創建一個接口如下:

//創建注入接口
public interface InterfaceInject {
    void injectAdditive(Additive additive);
}
       主體類實現接口並且初始化添加劑參數:

//實現InterfaceInject
public class OrangeJuice implements InterfaceInject {
    //引入添加劑參數
    private Additive additive;
    //實現接口方法,並且初始化參數
    @Override
    public void injectAdditive(Additive additive) {
        this.additive = additive;
    }

    public void needOrangeJuice(){
        //調用加入添加劑方法
        additive.addAdditive();
        System.out.println("消費者點了一杯橙汁(有添加劑)...");
    }
}

創建測試類:

public class Test {
    public static void main(String[] args) {
        //1.初始化Spring容器,加載配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2.通過容器獲取實例對象,getBean()方法中的參數是bean標簽中的id
        OrangeJuice orangeJuice = (OrangeJuice) applicationContext.getBean("orangeJuice");
        Additive additive = (Additive) applicationContext.getBean("additive");
        //通過接口注入,調用注入方法並且將Additive對象注入
        orangeJuice.injectAdditive(additive);
        //3.調用實例中的方法
        orangeJuice.needOrangeJuice();
    }
}

由於接口注入方式它強制被注入對象實現了不必要的接口,具有很強的侵入性,所以這種方式已經被淘汰了。

7、總結IoC帶來什么好處

IoC的思想最核心的地方在於,資源不由使用資源的雙方管理,而由不使用資源的第三方管理。

第一,資源集中管理,實現資源的可配置和易管理

第二,降低了使用資源雙方的依賴程度,也就是我們說的耦合度

其實IoC對編程帶來的最大改變不是從代碼上,而是從思想上,發生了“主從換位”的變化。應用程序原本是老大,要獲取什么資源都是主動出擊,但是在IoC/DI思想中,應用程序就變成被動的了,被動的等待IoC容器來創建並注入它所需要的資源了。IoC很好的體現了面向對象設計法則之一好萊塢法則:“別找我們,我們找你”;即由IoC容器幫對象找相應的依賴對象並注入,而不是由對象主動去找


參考資料:

  1. 《Java EE 互聯網輕量級框架整合開發》
  2. https://blog.csdn.net/qq_22654611/article/details/52606960
  3. https://blog.csdn.net/wanghao72214/article/details/3969594


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM