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

新夢想IT發表於2022-08-01


相關概念

依賴注入( 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

 

1 public class HelloSpring {

2     // 定義who屬性,該屬性的值將透過Spring框架進行設定

3     private String who = null;

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類的例項,並透過構造方法為who屬性注入屬性值。Spring配置檔案內容示例程式碼如下:

【示例】 applicationContext.xml

1 <?xml version="1.0" encoding="UTF-8"?>

2 <beans xmlns="

3     xmlns:xsi="

4     xsi:schemaLocation="

5     /spring-beans-3.2.xsd">

6     <bean id="helloSpring" class="cn.springdemo.HelloSpring">

7         <!-- 透過定義的單參構造為helloSpring的who屬性賦值 -->

8         <constructor-arg index="0" value="Spring" />

9     </bean>

10 </beans>

 

4)在cn.test包下,建立測試類HelloTest,並在類中編寫test()方法,示例程式碼如下:

【示例】 HelloTest.java

1 @Test

2 public void test() {

3     // 透過ClassPathXmlApplicationContext例項化Spring的上下文

4     ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

5    // 透過ApplicationContext的getBean()方法,根據id來獲取bean的例項

6     HelloSpring helloSpring = (HelloSpring) context.getBean("helloSpring");

7     // 執行print()方法

8     helloSpring.print();

9 }

 

執行程式後,控制檯的輸出結果與之前屬性 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

1 /**

2  * 增加DAO介面,定義了所需的持久化方法

3  */

4 public interface UserDao {

5     public void save(User user);

6 }

 

【示例】 UserDaoImpl.java

1 /**

2  * 使用者DAO類,實現IDao介面,負責User類的持久化操作

3  */

4 public class UserDaoImpl implements UserDao {

5     public void save(User user) {

6         // 這裡並未實現完整的資料庫操作,僅為說明問題

7         System.out.println("儲存使用者資訊到資料庫");

8     }

9 }

 

【示例】 UserService.java

1 /**

2  * 使用者業務介面,定義了所需的業務方法

3  */

4 public interface UserService {

5     public void addNewUser(User user);

6 }

 

【示例】 UserServiceImpl.java

1 /**

2  * 使用者業務類,實現對User功能的業務管理

3  */

4 public class UserServiceImpl implements UserService {

5     // 宣告介面型別的引用,和具體實現類解耦合

6     private UserDao userDao;

8     // userDao 屬性的setter訪問器,會被Spring呼叫,實現設值注入

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         // 呼叫使用者DAO的方法儲存使用者資訊

17         userDao.save(user);

18     }

19 }

 

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

 

3)這裡我們改為使用Spring的IoC的方式實現,在配置檔案applicationContext.xml 中,建立一個id為UserService的Bean,該Bean用於例項化UserServiceImpl類的資訊,並將userDao的例項注入到UserService中,其程式碼如下所示。

【示例】 applicationContext.xml

1 <?xml version="1.0" encoding="UTF-8"?>

2 <beans xmlns="

3     xmlns:xsi="

4     xsi:schemaLocation="

5     /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>元素的子元素,它用於呼叫Bean例項中的setUserDao()方法完成屬性賦值,從而實現依賴注入。其name屬性表示Bean例項中的相應屬性名,ref屬性用於指定其屬性值。

 

4)在cn.dsscm.test包中,建立測試類IoCTest,來對程式進行測試,編輯後其程式碼如下所示。

【示例】 IoCTest.java

1 import org.junit.Test;

2 import org.springframework.context.ApplicationContext;

3 import org.springframework.context.support.ClassPathXmlApplicationContext;

5 import cn.dsscm.pojo.User;

6 import cn.dsscm.service.UserService;

8 public class IoCTest {

9     @Test

10     public void test() {

11         // 透過ClassPathXmlApplicationContext例項化Spring的上下文

12         ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

13        // 透過ApplicationContext的getBean()方法,根據id來獲取bean的例項

14         UserService userService =  (UserService) context.getBean("userService");

15         // 執行print()方法

16        userService.addNewUser(new User());

17     }

18 }

 

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

可以看出,使用 Spring容器透過UserService實現類中的addNewUser()方法,呼叫了UserDao實現類中的addNewUser()方法,並輸出了結果。這就是Spring容器屬性setter注入的方式,也是實際開發中最為常用的一種方式。

分析其使用 “控制反轉”方式,其利用簡單工廠和工廠方法模式的思路分析此類問題,其程式碼如下所示。

 

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

1 /**

2 *增加使用者DAO工廠,負責使用者DAO例項的建立工作

3 */

4 public class UserDaoFactory {

5 //負責建立使用者DAO例項的方法

6   public static UserDao getInstance() {

7      //具體實現過程略

8    }

9 }

10 

11 /**

12  * 使用者業務類,實現對User功能的業務管理

13  */

14 public class UserServiceImpl implements UserService {

15     private UserDao dao = UserDaoFactory.getInstance();

16     public void addNewUser(User user) {

17         // 呼叫使用者DAO的方法儲存使用者資訊

18         dao.save(user);

19     }

20 }

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69940641/viewspace-2908448/,如需轉載,請註明出處,否則將追究法律責任。

相關文章