相關(guān)概念
依賴注入(Dependency Injection,簡稱DI)與控制反轉(zhuǎn)(IoC)的含義相同,只不過這兩個稱呼是從兩個角度描述的同一個概念。對于一個Spring初學者來說,這兩種稱呼很難理解,下面我們將通過簡單的語言來描述這兩個概念。
當某個Java對象(調(diào)用者)需要調(diào)用另一Java對象(被調(diào)用者,即被依賴對象)時,在傳統(tǒng)模式下,調(diào)用者通常會采用“new被調(diào)用者”的代碼方式來創(chuàng)建對象。這種方式會導致調(diào)用者與被調(diào)用者之間的耦合性增加,不利于后期項目的升級和維護。
在使用Spring框架之后,對象的實例不再由調(diào)用者來創(chuàng)建,而是由Spring容器來創(chuàng)建,Spring容器會負責控制程序之間的關(guān)系,而不是由調(diào)用者的程序代碼直接控制。這樣,控制權(quán)由應(yīng)用代碼轉(zhuǎn)移到了Spring容器,控制權(quán)發(fā)生了反轉(zhuǎn),這就是Spring的控制反轉(zhuǎn)。
從Spring容器的角度來看,Spring容器負責將被依賴對象賦值給調(diào)用者的成員變量,這相當于為調(diào)用者注入了它依賴的實例,這就是Spring的依賴注入。
相對于“控制反轉(zhuǎn)”,“依賴注入”的說法也許更容易理解一些,即由容器(如Spring)負責把組件所“依賴”的具體對象“注入”(賦值)給組件,從而避免組件之間以硬編碼的方式結(jié)合在一起。
依賴注入的實現(xiàn)方式
依賴注入的作用就是在使用Spring框架創(chuàng)建對象時,動態(tài)地將其所依賴的對象注入Bean組件中,其實現(xiàn)方式通常有兩種,一種是屬性setter方法注入,另一種是構(gòu)造方法注入,具體介紹如下。
屬性setter方法注入 指IoC容器使用setter方法注入被依賴的實例。通過調(diào)用無參構(gòu)造器或無參靜態(tài)工廠方法實例化Bean后,調(diào)用該Bean的setter方法,即可實現(xiàn)基于setter方法的依賴注入。
構(gòu)造方法注入指IoC容器使用構(gòu)造方法注入被依賴的實例?;跇?gòu)造方法的依賴注入通過調(diào)用帶參數(shù)的構(gòu)造方法來實現(xiàn),每個參數(shù)代表著一個依賴。
了解了兩種注入方式后,上一示例就是以屬性setter方法注入的方式為例,下面修改上述案例,使用構(gòu)造方法在Spring容器在應(yīng)用中是如何實現(xiàn)依賴注入的。
(1)在MyEclipse中,創(chuàng)建一個Java項目,將Spring的4個基礎(chǔ)包以及commons-logging的JAR包復(fù)制到lib目錄中,并發(fā)布到類路徑下,與上一項目基礎(chǔ)配置相同。
(2)在src目錄下,創(chuàng)建一個cn.springdemo包,并在包中創(chuàng)建HelloSpring.java,為期添加無參構(gòu)造方法、有參構(gòu)造方法,然后在類中定義一個print()方法,示例代碼如下:
【示例】HelloSpring.java
1 public class HelloSpring {
2?? ? // 定義who屬性,該屬性的值將通過Spring框架進行設(shè)置
3?? ? private String who = null;
4?
5 ????/**
6???????? * 定義打印方法,輸出一句完整的問候。
7???????? */
8 ????????public void print() {
9 ????????????System.out.println("Hello," + who + "!");
10???????? }
11
12 ??????public HelloSpring() {
13??????????super();
14 ??????}
15
16 ??????public HelloSpring(String who) {
17 ??????? ??super();
18?????????? this.who = who;
19?????? }
20 }
(3)在resources目錄下,編寫Spring配置文件,在Spring配置文件中修改id為helloSpring的Bean為HelloSpring類的實例,并通過構(gòu)造方法為who屬性注入屬性值。Spring配置文件內(nèi)容示例代碼如下:
【示例】applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3???? xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 ????xsi:schemaLocation="http://www.springframework.org/schema/beans
5 ????http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
6 ????<bean id="helloSpring" class="cn.springdemo.HelloSpring">
7 ????????<!-- 通過定義的單參構(gòu)造為helloSpring的who屬性賦值 -->
8 ????????<constructor-arg index="0" value="Spring" />
9 ????</bean>
10 </beans>
(4)在cn.test包下,創(chuàng)建測試類HelloTest,并在類中編寫test()方法,示例代碼如下:
【示例】HelloTest.java
1 @Test
2 public void test() {
3? ?? // 通過ClassPathXmlApplicationContext實例化Spring的上下文
4???? ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
5? ? // 通過ApplicationContext的getBean()方法,根據(jù)id來獲取bean的實例
6? ? ?HelloSpring helloSpring = (HelloSpring) context.getBean("helloSpring");
7? ? ?// 執(zhí)行print()方法
8? ? ?helloSpring.print();
9 }
執(zhí)行程序后,控制臺的輸出結(jié)果與之前屬性setter方法注入效果一致,注意兩個HelloSpring.java的差異,使用屬性setter方法注入必須要為屬性提供getter方法實現(xiàn)屬性值得注入,使用構(gòu)造方法注入必須要為類提供對應(yīng)參數(shù)屬性值的構(gòu)造實現(xiàn)才能注入值。
理解“控制反轉(zhuǎn)”
控制反轉(zhuǎn)(Inversion of Control,IoC),也稱為依賴注入(Dependency Injection,DI),是面向?qū)ο缶幊讨械囊环N設(shè)計理念,用來降低程序代碼之間的耦合度,在MVC的設(shè)計模式中經(jīng)常使用。首先考慮什么是依賴。依賴,在代碼中一般指通過局部變量、方法參數(shù)、返回值等建立的對于其他對象的調(diào)用關(guān)系。例如,在A類的方法中,實例化了B類的對象并調(diào)用其方法以完成特定的功能,我們就說A類依賴于B類。
幾乎所有的應(yīng)用都是由兩個或更多的類通過彼此合作來實現(xiàn)完整的功能。類與類之間的依賴關(guān)系增加了程序開發(fā)的復(fù)雜程度,我們在開發(fā)一個類的時候,還要考慮對正在使用該類的其他類的影響。
例如,常見的業(yè)務(wù)層調(diào)用數(shù)據(jù)訪問層實現(xiàn)持久化操作,解決問題的步驟如下:
(1)獲取Spring開發(fā)包并為工程添加Spring支持。
(2)為業(yè)務(wù)層和數(shù)據(jù)訪問層設(shè)計接口,聲明所需方法。
(3)編寫數(shù)據(jù)訪問層接口UserDao的實現(xiàn)類,完成具體的持久化操作。
(4)在業(yè)務(wù)實現(xiàn)類中聲明UserDao接口類型的屬性,并添加適當?shù)臉?gòu)造方法為屬性賦值。
(5)在Spring的配置文件中將DAO對象以構(gòu)造注入的方式賦值給業(yè)務(wù)實例中的UserDao類型的屬性。
(6)在代碼中獲取Spring配置文件中裝配好的業(yè)務(wù)類對象,實現(xiàn)程序功能。
實現(xiàn)步驟如下:
(1)在MyEclipse中,創(chuàng)建一個Java項目,在該項目的lib目錄中加入Spring支持和依賴的JAR包。
(2)為業(yè)務(wù)層調(diào)用數(shù)據(jù)訪問層實現(xiàn)持久化操作,如示例所示。
【示例】UserDao.java
1 /**
2? * 增加DAO接口,定義了所需的持久化方法
3? */
4 public interface UserDao {
5? ? ?public void save(User user);
6 }
【示例】UserDaoImpl.java
1 /**
2? * 用戶DAO類,實現(xiàn)IDao接口,負責User類的持久化操作
3? */
4 public class UserDaoImpl implements UserDao {
5 ????public void save(User user) {
6???????? // 這里并未實現(xiàn)完整的數(shù)據(jù)庫操作,僅為說明問題
7???????? System.out.println("保存用戶信息到數(shù)據(jù)庫");
8???? }
9 }
【示例】UserService.java
1 /**
2? * 用戶業(yè)務(wù)接口,定義了所需的業(yè)務(wù)方法
3? */
4 public interface UserService {
5 ????public void addNewUser(User user);
6 }
【示例】UserServiceImpl.java
1 /**
2? * 用戶業(yè)務(wù)類,實現(xiàn)對User功能的業(yè)務(wù)管理
3? */
4 public class UserServiceImpl implements UserService {
5 ????// 聲明接口類型的引用,和具體實現(xiàn)類解耦合
6 ????private UserDao userDao;
7?
8 ????// userDao 屬性的setter訪問器,會被Spring調(diào)用,實現(xiàn)設(shè)值注入
9???? public UserDao getUserDao() {
10 ????????return userDao;
11 ????}
12 ????public void setUserDao(UserDao userDao) {
13 ????????this.userDao = userDao;
14???? }
15 ????public void addNewUser(User user) {
16 ????????// 調(diào)用用戶DAO的方法保存用戶信息
17 ????????userDao.save(user);
18 ????}
19 }
如以上代碼所示,UserServiceImpl對UserDaoImpl存在依賴關(guān)系。這樣的代碼很常見,但是存在一個嚴重的問題,即UserServiceImpl和UserDaoImpl高度耦合,如果因為需求變化需要替換UserDao的實現(xiàn)類,將導致UserServiceImpl中的代碼隨之發(fā)生修改。如此,程序?qū)⒉痪邆鋬?yōu)良的可擴展性和可維護性,甚至在開發(fā)中難以測試。
(3)這里我們改為使用Spring的IoC的方式實現(xiàn),在配置文件applicationContext.xml 中,創(chuàng)建一個id為UserService的Bean,該Bean用于實例化UserServiceImpl類的信息,并將userDao的實例注入到UserService中,其代碼如下所示。
【示例】applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3???? xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 ????xsi:schemaLocation="http://www.springframework.org/schema/beans
5???? http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
6 ????<!--添加一個id為userService的實例 -->
7 ????<bean id="userDao" class="cn.dsscm.dao.UserDaoImpl" />
8???? <!--添加一個id為userService的實例 -->
9???? <bean id="userService" class="cn.dsscm.service.UserServiceImpl">
10???? <!-- 將id為userDao的Bean實例注入到userService實例中 -->
11? ????? <property name="userDao" ref="userDao" />
12 ????</bean>
13 </beans>
在上述代碼中,<property>是<bean>元素的子元素,它用于調(diào)用Bean實例中的setUserDao()方法完成屬性賦值,從而實現(xiàn)依賴注入。其name屬性表示Bean實例中的相應(yīng)屬性名,ref屬性用于指定其屬性值。
(4)在cn.dsscm.test包中,創(chuàng)建測試類IoCTest,來對程序進行測試,編輯后其代碼如下所示。
【示例】IoCTest.java
1 import org.junit.Test;
2 import org.springframework.context.ApplicationContext;
3 import org.springframework.context.support.ClassPathXmlApplicationContext;
4?
5 import cn.dsscm.pojo.User;
6 import cn.dsscm.service.UserService;
7?
8 public class IoCTest {
9? ? ?@Test
10? ? ?public void test() {
11? ? ? ? ?// 通過ClassPathXmlApplicationContext實例化Spring的上下文
12? ? ? ? ?ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
13?? ? ? ?// 通過ApplicationContext的getBean()方法,根據(jù)id來獲取bean的實例
14? ? ? ? ?UserService userService =? (UserService) context.getBean("userService");
15? ? ? ? ?// 執(zhí)行print()方法
16?? ? ? ?userService.addNewUser(new User());
17? ? ?}
18 }
執(zhí)行程序后,控制臺的輸出結(jié)果.
可以看出,使用Spring容器通過UserService實現(xiàn)類中的addNewUser()方法,調(diào)用了UserDao實現(xiàn)類中的addNewUser()方法,并輸出了結(jié)果。這就是Spring容器屬性setter注入的方式,也是實際開發(fā)中最為常用的一種方式。
分析其使用“控制反轉(zhuǎn)”方式,其利用簡單工廠和工廠方法模式的思路分析此類問題,其代碼如下所示。
【示例】簡單工廠和工廠方法模式
1 /**
2 *增加用戶DAO工廠,負責用戶DAO實例的創(chuàng)建工作
3 */
4 public class UserDaoFactory {
5 //負責創(chuàng)建用戶DAO實例的方法
6? ?public static UserDao getInstance() {
7? ? ? //具體實現(xiàn)過程略
8? ? }
9 }
10?
11 /**
12? * 用戶業(yè)務(wù)類,實現(xiàn)對User功能的業(yè)務(wù)管理
13? */
14 public class UserServiceImpl implements UserService {
15???? private UserDao dao = UserDaoFactory.getInstance();
16???? public void addNewUser(User user) {
17 ????????// 調(diào)用用戶DAO的方法保存用戶信息
18 ????????dao.save(user);
19 ????}
20 }
這里的用戶DAO工廠類UserDaoFactory體現(xiàn)了"控制反轉(zhuǎn)"的思想:UserServiceImpl不再依靠自身的代碼去獲得所依賴的具體DAO對象,而是把這一工作轉(zhuǎn)交給了"第三方"——UserDaoFactory,從而避免了和具體UserDao實現(xiàn)類之間的耦合。由此可見,在如何獲取所依賴的對象這件事上,“控制權(quán)"發(fā)生了"反轉(zhuǎn)”——從UserServiceImpl轉(zhuǎn)移到了UserDaoFactory,這就是所謂的"控制反轉(zhuǎn)"。