Spring升級案例之IOC介紹和依賴注入

李一二發表於2020-07-14

Spring升級案例之IOC介紹和依賴注入

一、IOC的概念和作用
1.什麼是IOC

控制反轉(Inversion of Control, IoC)是一種設計思想,在Java中就是將設計好的物件交給容器控制,而不是傳統的在物件內部直接控制。傳統Java SE程式設計,我們直接在物件內部通過new進行建立物件,是程式主動去建立依賴物件;而IoC是有專門一個容器來建立這些物件,即由Ioc容器來控制物件的建立;可以理解為IoC 容器控制了物件和外部資源獲取(不只是物件包括比如檔案等)。

2.反轉和正轉

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

3.IoC的作用

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

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

二、基於XML的IOC
1.建立工程

本專案建立在入門案例中傳統三層架構的基礎上,專案結構如下:

SpringIOC的專案結構

首先在pom.xml檔案中新增如下內容:

<packaging>jar</packaging>
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
</dependencies>
2.建立xml檔案

在resource目錄下新建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.xsd">
</bean>

這裡有一個小細節,在建立xml檔案的時候,選擇new->XML Configuration File->Spring Config,就會自動建立帶有約束的Spring的xml配置檔案。如下圖:

建立xml配置檔案

3.使用Spring來建立bean物件

在bean標籤內部新增如下內容:IOC容器本質上是一個map,id就是key,class對應的就是bean物件的全限定類名,Spring可以依據全限定類名來建立bean物件來作為map的value屬性。

<!-- 把物件的建立交給Spring來管理 -->
<bean id="accountService" class="service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl"></bean>   
4.使用IOC容器建立的bean物件

在src/main/java目錄下建立ui.Client類:

public class Client {
    /**
     * 獲取Spring的IoC核心容器,並根據id獲取物件
     * @param args
     */
    public static void main(String[] args) {
        //1.獲取IoC核心容器
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
        //2.根據id獲取bean物件
        //第一種方法:只傳入id獲取到物件之後強轉為需要的型別
        IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
        System.out.println(accountService);
        //第二種方法:傳入id和所需要型別的位元組碼,這樣getBean返回的物件就已經是所需要的物件
        IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);
        System.out.println(accountDao);
    }
}

關於ApplicationContext,這裡需要說明一下,首先通過選中這個介面然後右鍵Diagrams->Show Diagrams,可以看到介面的繼承關係:其中BeanFactory介面就是IoC容器的底層介面。

ApplicationContext介面的繼承關係

在diagram中選中ApplicationContext介面,然後右鍵Show Implementations,可以看到該介面的實現類:

ApplicationContext介面的實現類

關於這些實現類需要說明如下幾點:

ApplicationContext的實現類:
1.ClassPathXmlApplicationContext:載入類路徑下的配置檔案,要求配置檔案必須在類路徑下
2.FileSystemApplicationContext:載入磁碟任意路徑下的配置檔案,要求配置檔案必須有訪問許可權,這種方法不常用
3.AnnotationApplicationContext:用於讀取註解建立容器
5.IoC核心容器的兩個介面:ApplicationContext和BeanFactory
  • ApplicationContext:建立核心容器時採用立即載入的方式建立物件,讀取配置檔案之後,立刻建立Bean物件(單例模式)。
  • BeanFactory:建立核心容器時採用延遲載入的方式建立物件,當根據id獲取物件時,才會建立Bean物件(多例模式)

為了更加清楚地看到這兩個介面之間的區別,我們在AccountDaoImpl和AccountServiceImpl類的無參構造方法中新增如下內容:

//AccountDaoImpl
public AccountDaoImpl() { System.out.println("dao建立了"); }
//AccountServiceImpl
public AccountServiceImpl() { System.out.println("service建立了"); }

對ui.Client類中的main方法新增如下程式碼:

Resource resource = new ClassPathResource("beans.xml");
BeanFactory factory = new DefaultListableBeanFactory();
BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
bdr.loadBeanDefinitions(resource);
System.out.println(factory.getBean("accountDao"));

採用斷點除錯,我們可以發現:

  1. 對於ApplicationContext來說,執行ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");之後,立刻就會輸出“service建立了”和“dao建立了”。
  2. 而對於BeanFactory來說,只有當執行到System.out.println(factory.getBean("accountDao"));之後,才會輸出“dao建立了”。
  3. 這也就說明ApplicationContext是立即載入,BeanFactory是延遲載入。通常而言,ApplicationContext介面更加常用。此外,我們也可以自己指定單例模式還是多例模式。
三、Bean物件的管理細節
1.三種建立bean物件的方式
  • 第一種方式:使用預設構造方法建立

    在Spring配置檔案中使用bean標籤,如果只有id和class屬性,就會使用預設構造方法(無參構造方法)建立物件。如果沒有預設構造方法,則物件無法建立。例如,之前我們所使用的便是這第一種方式。

    <bean id="accountService" class="service.impl.AccountServiceImpl"></bean>
    
  • 第二種方式:使用其他類(比如工廠類)中的方法建立物件,並存入Spring容器,該類可能是jar包中的類,無法通過修改原始碼來提供預設構造方法。

    為了演示,我們在src/main/java目錄新建factory包,在factory包下新建類InstanceFactory:

    public class InstanceFactory {
        //非靜態方法
        public IAccountService getAccountService() {
            return new AccountServiceImpl();
        }
    }
    

    instanceFactory對應的就是factory包下的InstanceFactory類的物件,accountService對應的是InstanceFactory類下的getAccountService方法返回的物件。factory-bean屬性用於指定建立本次物件的factory,factory-method屬性用於指定建立本次物件的factory中的方法。

    <bean id="instanceFactory" class="factory.InstanceFactory"></bean>
    <bean id="accountService" factory-bean="instanceFactory" factory-method= "getAccountService"></bean>
    
  • 第三種方式:使用其他類(比如工廠類)中的靜態方法建立物件,並存入Spring容器,該類可能是jar包中的類,無法通過修改原始碼來提供預設構造方法。

    為了演示,我們在src/main/java目錄新建factory包,在factory包下新建類StaticFactory:

    public class StaticFactory {
        //靜態方法
        public static IAccountService getAccountService() {
            return new AccountServiceImpl();
        }
    }
    

    由於是靜態方法,所以無需指定factory-bean屬性。class屬性指定建立bean物件的工廠類,factory-method方法指定建立bean物件的工廠類中的靜態方法。

    <bean id="accountService" class="factory.StaticFactory" factory-method="getAccountService"></bean>
    
2.bean物件的作用範圍

bean標籤的scope屬性(用於指定bean物件的作用範圍),有如下取值:常用的就是單例和多例

  • singleton:單例(預設值)
  • prototype:多例
  • request:作用域Web的請求範圍
  • session:作用於Web的會話範圍
  • global-session:作用於叢集的會話範圍(全域性會話範圍),當不是叢集環境時,它就是session

這裡我們演示單例和多例:

<bean id="accountService" class="service.impl.AccountServiceImpl" scope="singleton"></bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl" scope="prototype"></bean>

此時即便Client類中的main方法使用ApplicationContext介面:

public static void main(String[] args) { 
    //1.獲取IoC核心容器
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
    //2.根據id獲取bean物件
    //第一種方法:只傳入id獲取到物件之後強轉為需要的型別
    IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
    System.out.println(accountService);
    //第二種方法:傳入id和所需要型別的位元組碼,這樣getBean返回的物件就已經是所需要的物件
    IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);
    IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");
    System.out.println(accountDao == accountDao1);
}

使用斷點除錯,我們可以發現:

  1. 在執行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");時,就會輸出“service建立了”,不會輸出“dao建立了”。

  2. 只有當執行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");時,才會輸出“dao建立了”。

  3. 並且accountDao == accountDao1的結果是false。

3.bean物件的生命週期
  • 單例物件:生命週期和容器相同,容器建立物件就建立,容器銷燬物件就銷燬
  • 多例物件:當需要使用物件時(根據id獲取物件時),物件被建立;當沒有引用指向物件且物件長時間不用時,由Java的垃圾回收機制回收

為了演示,這裡需要介紹bean標籤的兩個屬性:init-method屬性指定初始化方法,destroy-method屬性指定銷燬方法

<bean id="accountService" class="service.impl.AccountServiceImpl" scope="singleton" 
      init-method="init" destroy-method="destroy"></bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl" scope="prototype" init-method="init"
          destroy-method="destroy"></bean>

同時,還有在AccountDaoImpl類和AccountService類中新增如下程式碼:

//AccountDaoImpl:
public void init() { System.out.println("dao初始化了"); }
public void destroy() { System.out.println("dao銷燬了"); }

//AccountServiceImpl:
public void init() { System.out.println("service初始化了"); }
public void destroy() { System.out.println("service銷燬了"); }

為了手動關閉容器需要在Client類中的main方法中最後加入:

//容器需要手動關閉,因為applicationContext是介面型別,所以沒有close方法,需要強制轉換為實現類物件
((ClassPathXmlApplicationContext) applicationContext).close();

這個時候,我們再去使用斷點除錯,可以發現:

  1. 當執行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");時,就會輸出“service建立了”和“service初始化了”。
  2. 只有當執行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");時,才會輸出“dao建立了”和“dao初始化了”。
  3. 執行到((ClassPathXmlApplicationContext) applicationContext).close();時,會輸出“service銷燬了”,不會輸出“dao銷燬了”。這是因為建立AccountDaoImpl類的物件時,使用的是多例模式。多例模式下的物件回收由JVM決定,關閉Ioc容器並不能使得JVM回收物件。
四、IOC的依賴注入
1.之前程式碼中的問題

在之前的程式碼中,我們一直沒有使用AccountServiceImpl物件中的saveAccount方法,這是因為我們還沒有例項化該類中的accountDao物件。我們先看看AccountServiceImpl的原始碼:

public class AccountServiceImpl implements IAccountService {
    //持久層介面物件的引用,為了降低耦合,這裡不應該是new AccountDaoImpl
    private IAccountDao accountDao;
		
    public AccountServiceImpl() { System.out.println("service建立了"); }
    /** 模擬儲存賬戶操作 */
    public void saveAccounts() {
        System.out.println("執行儲存賬戶操作");
        //呼叫持久層介面函式
        accountDao.saveAccounts();
    }
}

在之前的三層架構中,對於accoutDao物件,我們是private IAccountDao accountDao = new AccountDaoImpl(); 實際上,為了降低耦合,我們不應該在此處對accountDao物件進行例項化操作,應該直接是private IAccountDao accountDao; 。為了將該物件例項化,我們就需要用到依賴注入。

2.依賴注入介紹

依賴注入(Dependency Injection, DI):它是spring框架核心IoC的具體實現(IoC是一種思想,而DI是一種設計模式)。 在編寫程式時,通過控制反轉,把物件的建立交給了 spring,但是程式碼中不可能出現沒有依賴的情況。IoC 解耦只是降低他們的依賴關係,但不會消除。例如:我們的業務層仍會呼叫持久層的方法,這種業務層和持久層的依賴關係,在使用 spring 之後,就讓 spring 來維護了。簡單的說,就是讓框架把持久層物件傳入業務層,而不用我們自己去獲取。

3.依賴注入的資料型別和方式

在依賴注入中,能夠注入的資料型別有三類:

  • 基本型別和String型別
  • 其他Bean型別:在註解或配置檔案中配置過的Bean,也就是Spring容器中的Bean
  • 複雜型別(集合型別):例如List、Array、Map等

為了演示依賴注入,我們在src/main/java目錄下,新建一個包entity,在該包下新建實體類People:

程式碼中的欄位如下,注意構造方法一定要加上無參構造方法。

public class People {
    //如果是經常變化的資料,並不適用於依賴注入
    private String name;
    private Integer age;
    //Date型別不是基本型別,屬於Bean型別
    private Date birthDay;
    //以下都是集合型別
    private String[] myString;
    private List<String> myList;
    private Set<String> mySet;
    private Map<String, String> myMap;
    private Properties myProps;

    //為了節省空間,這裡省略了所有的set方法和toString方法,在實際程式碼中要補上
  
    public People() { }  //提供預設構造方法

    public People(String name, Integer age, Date birthDay) {
        this.name = name;
        this.age = age;
        this.birthDay = birthDay;
    }
}

注入的方式有三種:

  • 使用構造方法注入

    這種方式使用的標籤為constructor-arg,在bean標籤的內部使用,該標籤的屬性有五種,其中的1-3種用於指定給構造方法中的哪個引數注入資料:

    1. type:用於要注入的資料的資料型別,該資料型別也是構造方法中某個或某些引數的型別
    2. index:用於給構造方法中指定索引位置的引數注入資料,索引從0開始
    3. name:用於給構造方法中指定名稱的引數注入資料(最常用)
    4. value:要注入的資料的值(只能是基本型別或者String型別)
    5. ref:用於指定其他bean型別資料(只能是在Spring的IOC核心中出現過的bean物件)
    <bean id="people1" class="entity.People">
        <!-- 如果有多個String型別的引數,僅使用type標籤無法實現注入 -->
        <constructor-arg type="java.lang.String" value="Jack"></constructor-arg>
        <constructor-arg index="1" value="18"></constructor-arg>
        <constructor-arg name="birthDay"  ref="date"></constructor-arg>
    </bean>
    <!-- 配置一個日期物件 -->
    <bean id="date" class="java.util.Date"></bean>
    
  • 使用set方法注入

    這種方式使用的標籤為property,在bean標籤的內部使用,該標籤的屬性有三種:

    1. name:用於指定注入時所呼叫的set方法名稱,即set之後的名稱,並且要改成小寫(例如"setUsername"對應的name就是"username"),換句話說就是屬性名稱
    2. value:要注入的資料的值(只能是基本型別或者String型別)
    3. ref:用於指定其他bean型別資料(只能是在Spring的IOC核心中出現過的bean物件)
    <bean id="people2" class="entity.People">
        <property name="name" value="Jack"></property>
        <property name="age" value="18"></property>
        <property name="birthDay"  ref="date"></property>
    </bean>
    
  • 使用註解注入:本篇主要講解使用xml配置檔案的方式注入,因此這種方法暫不做介紹

4.關於集合型別的注入

這裡我們使用set方法來向集合中注入資料,對於使用的標籤,注意以下三點:

  1. 用於給List結構集合注入的標籤有:array、list、set
  2. 用於給Map結構集合注入的標籤有:map、props
  3. 結構相同,標籤可以互換
<bean id="people3" class="entity.People">
    <property name="myString">
        <array>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </array>
    </property>
    <property name="myList">
        <list>
            <value>ListA</value>
            <value>ListB</value>
            <value>ListC</value>
        </list>
    </property>
    <property name="mySet">
        <set>
            <value>SetA</value>
            <value>SetB</value>
            <value>SetC</value>
        </set>
    </property>
    <property name="myMap">
        <map>
            <entry key="A" value="MapA"></entry>
            <entry key="B" value="MapB"></entry>
            <!-- 對於entry標籤,可以使用value屬性來指定值,也可以在標籤內部使用value標籤 -->
            <entry key="C">
                <value>MapC</value>
            </entry>
        </map>
    </property>
    <property name="myProps">
        <props>
            <!-- 對於prop標籤,只有key屬性,沒有value屬性,所以直接將該標籤的值作為value -->
            <prop key="A">PropA</prop>
            <prop key="B">PropB</prop>
            <prop key="C">PropC</prop>
        </props>
    </property>
</bean>
5.完善之前的程式碼

在本部分的開頭,我們還有一個問題沒有解決,那就是AccountServiceImpl類中的accountDao物件無法例項化。現在我們就可以通過配置的方式來對進行依賴注入:

<bean id="accountService" class="service.impl.AccountServiceImpl">
    <property name="accountDao"  ref="accountDao"></property>
</bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl"></bean>

最後我們再進行統一的測試,修改Client類中的main方法:

public static void main(String[] args) {
    //驗證依賴注入
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
    People people1 = applicationContext.getBean("people1", People.class);
    System.out.println(people1);
    People people2 = applicationContext.getBean("people2", People.class);
    System.out.println(people2);
    People people3 = applicationContext.getBean("people3", People.class);
    System.out.println(people3);

    //向accountService中注入accountDao以呼叫saveAccounts方法
    IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
    System.out.println(accountService);
    accountService.saveAccounts();

}

執行程式碼,結果如下:

執行結果

相關文章