理解Spring中依賴注入(DI)與控制反轉(IoC)

相關概念

依賴注入(Dependency Injection,簡稱DI)與控制反轉(IoC)的含義相同,只不過這兩個稱呼是從兩個角度描述的同一個概念。對於一個Spring初學者來說,這兩種稱呼很難理解,下面我們將通過簡單的語言來描述這兩個概念。

當某個Java對象(調用者)需要調用另一Java對象(被調用者,即被依賴對象)時,在傳統模式下,調用者通常會採用“new被調用者”的代碼方式來創建對象。這種方式會導致調用者與被調用者之間的耦合性增加,不利於後期項目的升級和維護。

在使用Spring框架之後,對象的實例不再由調用者來創建,而是由Spring容器來創建,Spring容器會負責控制程序之間的關係,而不是由調用者的程序代碼直接控制。這樣,控制權由應用代碼轉移到了Spring容器,控制權發生了反轉,這就是Spring的控制反轉。

從Spring容器的角度來看,Spring容器負責將被依賴對象賦值給調用者的成員變量,這相當於爲調用者注入了它依賴的實例,這就是Spring的依賴注入。

相對於“控制反轉”,“依賴注入”的說法也許更容易理解一些,即由容器(如Spring)負責把組件所“依賴”的具體對象“注入”(賦值)給組件,從而避免組件之間以硬編碼的方式結合在一起。

依賴注入的實現方式

依賴注入的作用就是在使用Spring框架創建對象時,動態地將其所依賴的對象注入Bean組件中,其實現方式通常有兩種,一種是屬性setter方法注入,另一種是構造方法注入,具體介紹如下。

  • 屬性setter方法注入 指IoC容器使用setter方法注入被依賴的實例。通過調用無參構造器或無參靜態工廠方法實例化Bean後,調用該Bean的setter方法,即可實現基於setter方法的依賴注入。
  • 構造方法注入 指IoC容器使用構造方法注入被依賴的實例。基於構造方法的依賴注入通過調用帶參數的構造方法來實現,每個參數代表着一個依賴。

瞭解了兩種注入方式後,上一示例就是以屬性setter方法注入的方式爲例,下面修改上述案例,使用構造方法在Spring容器在應用中是如何實現依賴注入的。
(1)在MyEclipse中,創建一個Java項目,將Spring的4個基礎包以及commons-logging的JAR包複製到lib目錄中,併發布到類路徑下,與上一項目基礎配置相同。
(2)在src目錄下,創建一個cn.springdemo包,並在包中創建HelloSpring.java,爲期添加無參構造方法、有參構造方法,然後在類中定義一個print()方法,示例代碼如下:
【示例】 HelloSpring.java

public class HelloSpring {
	// 定義who屬性,該屬性的值將通過Spring框架進行設置
	private String who = null;

	/**
	 * 定義打印方法,輸出一句完整的問候。
	 */
	public void print() {
		System.out.println("Hello," + who + "!");
	}
	
	public HelloSpring() {
		super();
	}

	public HelloSpring(String who) {
		super();
		this.who = who;
	}
}

(3)在resources目錄下,編寫Spring配置文件,在Spring配置文件中修改id爲helloSpring的Bean爲HelloSpring類的實例,並通過構造方法爲who屬性注入屬性值。Spring配置文件內容示例代碼如下:
【示例】 applicationContext.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-3.2.xsd">
	<bean id="helloSpring" class="cn.springdemo.HelloSpring">
		<!-- 通過定義的單參構造爲helloSpring的who屬性賦值 -->
		<constructor-arg index="0" value="Spring" />
	</bean>
</beans>

(4)在cn.test包下,創建測試類HelloTest,並在類中編寫test()方法,示例代碼如下:
【示例】 HelloTest.java

@Test
public void test() {
    // 通過ClassPathXmlApplicationContext實例化Spring的上下文
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    // 通過ApplicationContext的getBean()方法,根據id來獲取bean的實例
    HelloSpring helloSpring = (HelloSpring) context.getBean("helloSpring");
    // 執行print()方法
    helloSpring.print();
}

執行程序後,控制檯的輸出結果與之前屬性setter方法注入效果一致,注意兩個HelloSpring.java的差異,使用屬性setter方法注入必須要爲屬性提供getter方法實現屬性值得注入,使用構造方法注入必須要爲類提供對應參數屬性值的構造實現才能注入值。

理解“控制反轉”

控制反轉(Inversion of Control,IoC),也稱爲依賴注入(Dependency Injection,DI),是面向對象編程中的一種設計理念,用來降低程序代碼之間的耦合度,在MVC的設計模式中經常使用。首先考慮什麼是依賴。依賴,在代碼中一般指通過局部變量、方法參數、返回值等建立的對於其他對象的調用關係。例如,在A類的方法中,實例化了B類的對象並調用其方法以完成特定的功能,我們就說A類依賴於B類。

幾乎所有的應用都是由兩個或更多的類通過彼此合作來實現完整的功能。類與類之間的依賴關係增加了程序開發的複雜程度,我們在開發一個類的時候,還要考慮對正在使用該類的其他類的影響。

例如,常見的業務層調用數據訪問層實現持久化操作,解決問題的步驟如下:
(1)獲取Spring開發包併爲工程添加Spring支持。
(2)爲業務層和數據訪問層設計接口,聲明所需方法。
(3)編寫數據訪問層接口UserDao的實現類,完成具體的持久化操作。
(4)在業務實現類中聲明UserDao接口類型的屬性,並添加適當的構造方法爲屬性賦值。
(5)在Spring的配置文件中將DAO對象以構造注入的方式賦值給業務實例中的UserDao類型的屬性。
(6)在代碼中獲取Spring配置文件中裝配好的業務類對象,實現程序功能。

實現步驟如下:
(1)在MyEclipse中,創建一個Java項目,在該項目的lib目錄中加入Spring支持和依賴的JAR包。
(2)爲業務層調用數據訪問層實現持久化操作,如示例所示。
【示例】 UserDao.java

/**
 * 增加DAO接口,定義了所需的持久化方法
 */
public interface UserDao {
	public void save(User user);
}

【示例】 UserDaoImpl.java

/**
 * 用戶DAO類,實現IDao接口,負責User類的持久化操作
 */
public class UserDaoImpl implements UserDao {
	public void save(User user) {
		// 這裏並未實現完整的數據庫操作,僅爲說明問題
		System.out.println("保存用戶信息到數據庫");
	}
}

【示例】 UserService.java

/**
 * 用戶業務接口,定義了所需的業務方法
 */
public interface UserService {
	public void addNewUser(User user);
}

【示例】 UserServiceImpl.java

/**
 * 用戶業務類,實現對User功能的業務管理
 */
public class UserServiceImpl implements UserService {
	// 聲明接口類型的引用,和具體實現類解耦合
	private UserDao userDao;
	
	// userDao 屬性的setter訪問器,會被Spring調用,實現設值注入
	public UserDao getUserDao() {
		return userDao;
	}
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void addNewUser(User user) {
		// 調用用戶DAO的方法保存用戶信息
		userDao.save(user);
	}
}

如以上代碼所示,UserServiceImpl對UserDaoImpl存在依賴關係。這樣的代碼很常見,但是存在一個嚴重的問題,即UserServiceImpl和UserDaoImpl高度耦合,如果因爲需求變化需要替換UserDao的實現類,將導致UserServiceImpl中的代碼隨之發生修改。如此,程序將不具備優良的可擴展性和可維護性,甚至在開發中難以測試。

(3)這裏我們改爲使用Spring的IoC的方式實現,在配置文件applicationContext.xml 中,創建一個id爲UserService的Bean,該Bean用於實例化UserServiceImpl類的信息,並將userDao的實例注入到UserService中,其代碼如下所示。
【示例】 applicationContext.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-3.2.xsd">
	<!--添加一個id爲userService的實例 -->
	<bean id="userDao" class="cn.dsscm.dao.UserDaoImpl" />
	<!--添加一個id爲userService的實例 -->
	<bean id="userService" class="cn.dsscm.service.UserServiceImpl">
	<!-- 將id爲userDao的Bean實例注入到userService實例中 -->
	   <property name="userDao" ref="userDao" />
	</bean>
</beans>

在上述代碼中,<property><bean>元素的子元素,它用於調用Bean實例中的setUserDao()方法完成屬性賦值,從而實現依賴注入。其name屬性表示Bean實例中的相應屬性名,ref屬性用於指定其屬性值。
(4)在cn.dsscm.test包中,創建測試類IoCTest,來對程序進行測試,編輯後其代碼如下所示。
【示例】 IoCTest.java

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.dsscm.pojo.User;
import cn.dsscm.service.UserService;

public class IoCTest {
    @Test
    public void test() {
        // 通過ClassPathXmlApplicationContext實例化Spring的上下文
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 通過ApplicationContext的getBean()方法,根據id來獲取bean的實例
        UserService userService =  (UserService) context.getBean("userService");
        // 執行print()方法
        userService.addNewUser(new User());
    }
}

執行程序後,控制檯的輸出結果.

可以看出,使用Spring容器通過UserService實現類中的addNewUser()方法,調用了UserDao實現類中的addNewUser()方法,並輸出了結果。這就是Spring容器屬性setter注入的方式,也是實際開發中最爲常用的一種方式。
分析其使用“控制反轉”方式,其利用簡單工廠和工廠方法模式的思路分析此類問題,其代碼如下所示。

【示例】 簡單工廠和工廠方法模式

**
*增加用戶DAO工廠,負責用戶DAO實例的創建工作
*public class UserDaoFactory {
//負責創建用戶DAO實例的方法
  public static UserDao getInstance() {
     //具體實現過程略
   }
}

/**
 * 用戶業務類,實現對User功能的業務管理
 */
public class UserServiceImpl implements UserService {
	private UserDao dao = UserDaoFactory.getInstance();
	public void addNewUser(User user) {
		// 調用用戶DAO的方法保存用戶信息
		dao.save(user);
	}
}

這裏的用戶DAO工廠類UserDaoFactory體現了"控制反轉"的思想:UserServiceImpl不再依靠自身的代碼去獲得所依賴的具體DAO對象,而是把這一工作轉交給了"第三方"——UserDaoFactory,從而避免了和具體UserDao實現類之間的耦合。由此可見,在如何獲取所依賴的對象這件事上,“控制權"發生了"反轉”——從UserServiceImpl轉移到了UserDaoFactory,這就是所謂的"控制反轉"。

問題雖然得到了解決,但是大量的工廠類會被引入開發過程中,明顯增加了開發的工作量。而Spring能夠替我們分擔這些額外的工作,爲我們提供完整的IoC實現,讓我們得以專注於業務類和DAO類的設計。


超全面的測試IT技術課程,0元立即加入學習!有需要的朋友戳:


騰訊課堂測試技術學習地址

歡迎轉載,但未經作者同意請保留此段聲明,並在文章頁面明顯位置給出原文鏈接。

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