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();
}
運行代碼,結果如下: