1. 程式人生 > 實用技巧 >Spring5詳解(三)——認識IoC控制反轉/DI依賴注入

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

1、前言

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

注意: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。其中有這樣一段話,如下圖所示:

他認為需要為該模式(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 物件】的方式。當然我們也可以使用簡單工廠模式來實現,就是在簡單工廠模式中,我們的被依賴類由一個工廠方法建立,依賴主體先呼叫被依賴物件的工廠方法,接著主動基於工廠訪問被依賴物件,但這種方式任然存在問題,即依賴主體與被依賴物件的工廠之間存在著耦合。主動建立物件的程式思想圖如下所示:

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

②、被動建立物件

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

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

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

上面的例子只能看出不需要我們自己建立物件了,那萬一它還依賴於其它物件呢?那麼物件之間要相互呼叫呢?我們要怎麼來理解呢?下面接著舉例。

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

在主體物件依賴其它物件的時候,物件之間的相互呼叫通過注入的方式來完成,所以下面我們介紹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)。

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

①、建構函式注入

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

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

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

②、set方法注入(推薦)

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

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

③、介面注入

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

④、三種注入方式的比較

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

綜上所述,構造方法注入和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("奸商在橙汁中添加了新增劑...");
    }
}

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

①、原始方式

最原始的方式就是沒有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();
    }
}

執行結果如下:

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

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

②、建構函式注入

構造器注入,顧名思義就是通過建構函式完成依賴關係的注入。首先我們看一下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();
    }
}

執行結果如下:

可以發現執行結果和原始方式一樣,但是將建立物件的權利交給Spring之後,橙汁和新增劑之間的耦合度明顯降低了。此時我們的重點是在配置檔案中,而不在乎程式本身,即使新增劑型別發生改變,我們只需修改配置檔案即可,不需要修改程式程式碼。

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

③、setter方法注入(推薦)

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("消費者點了一杯橙汁(有新增劑)...");
    }
}

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

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

④、介面注入

介面注入,就是主體類必須實現我們建立的一個注入介面,該介面會傳入被依賴類的物件,從而完成注入。

由於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