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

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();

}

運行代碼,結果如下:

運行結果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章