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.建立工程
本專案建立在入門案例中傳統三層架構的基礎上,專案結構如下:
首先在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配置檔案。如下圖:
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容器的底層介面。
在diagram中選中ApplicationContext介面,然後右鍵Show Implementations,可以看到該介面的實現類:
關於這些實現類需要說明如下幾點:
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"));
採用斷點除錯,我們可以發現:
- 對於ApplicationContext來說,執行ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");之後,立刻就會輸出“service建立了”和“dao建立了”。
- 而對於BeanFactory來說,只有當執行到System.out.println(factory.getBean("accountDao"));之後,才會輸出“dao建立了”。
- 這也就說明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);
}
使用斷點除錯,我們可以發現:
-
在執行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");時,就會輸出“service建立了”,不會輸出“dao建立了”。
-
只有當執行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");時,才會輸出“dao建立了”。
-
並且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();
這個時候,我們再去使用斷點除錯,可以發現:
- 當執行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");時,就會輸出“service建立了”和“service初始化了”。
- 只有當執行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");時,才會輸出“dao建立了”和“dao初始化了”。
- 執行到((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種用於指定給構造方法中的哪個引數注入資料:
- type:用於要注入的資料的資料型別,該資料型別也是構造方法中某個或某些引數的型別
- index:用於給構造方法中指定索引位置的引數注入資料,索引從0開始
- name:用於給構造方法中指定名稱的引數注入資料(最常用)
- value:要注入的資料的值(只能是基本型別或者String型別)
- 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標籤的內部使用,該標籤的屬性有三種:
- name:用於指定注入時所呼叫的set方法名稱,即set之後的名稱,並且要改成小寫(例如"setUsername"對應的name就是"username"),換句話說就是屬性名稱
- value:要注入的資料的值(只能是基本型別或者String型別)
- 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方法來向集合中注入資料,對於使用的標籤,注意以下三點:
- 用於給List結構集合注入的標籤有:array、list、set
- 用於給Map結構集合注入的標籤有:map、props
- 結構相同,標籤可以互換
<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();
}
執行程式碼,結果如下: