Spring 的依賴註入(DI)

来源:https://www.cnblogs.com/god23bin/archive/2023/06/30/spring-di.html
-Advertisement-
Play Games

歡迎來到本篇文章,書接上回,本篇說說 Spring 中的依賴註入,包括註入的方式,寫法,該選擇哪個註入方式以及可能出現的迴圈依賴問題等內容。 如果正在閱讀的朋友還不清楚什麼是「依賴」,建議先看看我第一篇文章,通過 Employee 和 Department 簡單說了什麼是所謂的依賴。 ...


前言

歡迎來到本篇文章,書接上回,本篇說說 Spring 中的依賴註入,包括註入的方式,寫法,該選擇哪個註入方式以及可能出現的迴圈依賴問題等內容。

如果正在閱讀的朋友還不清楚什麼是「依賴」,建議先看看我第一篇文章,通過 Employee 和 Department 簡單說了什麼是所謂的依賴。

什麼是依賴註入?

依賴註入是一個過程,舉個例子:

public class A {
    private B b;
    // 省略 getter 和 setter
    // 省略構造方法
}

現在 A 類 是依賴 B 類的,沒有 B,A 什麼都不是。Spring IoC 容器創建好 B 的實例對象後並賦值給 A 對象中的 b 屬性(成員變數)的過程,就是所謂的「依賴註入」。

當然,這也是所謂的控制反轉了,對象 b 的創建不是我們手動 new 創建的,而是 Spring IoC 容器創建的。

使用 DI 原則,可以讓代碼更加簡潔,當對象提供其依賴項時,解耦也更加有效。依賴項 B 註入給 A 的實例對象,A 的實例對象是不會查找它的依賴項 B 的,也不知道依賴項 B 的位置。

依賴註入主要有兩種實現方式:基於構造方法的依賴註入基於 Setter 的依賴註入

基於構造方法的依賴註入

基於構造方法的 DI 是通過 Spring IoC 容器調用帶有許多參數的構造方法來完成的,每個參數表示一個依賴項。這與上一篇中調用具有特定參數的靜態工廠方法來構造 Bean 是幾乎等價的。

public class Vehicle {
    
    // Vehicle 依賴於 Producer 對象,或者說 Vehicle 持有一個 Producer 依賴項
    private final Producer producer;
    
    // 構造方法,讓 Spring IoC 容器能夠註入 Producer 對象給 Vehicle 對象
    public Vehicle(Producer producer) {
        this.producer = producer;
    }
}

註意,這個 Vehicle 類就只是一個普通的 Java Bean,或者說 POJO,沒什麼特別之處,最普通不過的類了,沒有繼承或者實現 Spring 指定的任意類。

構造方法中參數的解析

Spring IoC 是通過構造方法參數的解析來匹配需要註入的依賴項。換句話說,解析實際上就是通過匹配參數的類型,來確定註入什麼依賴項

如果構造方法的參數中沒有存在潛在的歧義,那麼在 Bean 被實例化的時候,構造方法中參數的順序和實例化時進行賦值是一樣的。

package cn.god23bin.demo.model;

public class A {
    private B b;
    private C c;
    
    public A(B b, C c) {
        this.b = b;
        this.c = c;
    }
}

單獨寫這些類,是完成不了依賴註入的,這時候需要配置元數據了,讓它與 Java 類相結合,才能發揮 Spring IoC 的作用。

現在假設 B 和 C 是沒有任何繼承上的關聯,也沒有任何潛在的歧義,那麼我們在配置元數據中的配置是正常的,需要使用到 <constructor-arg/> 標簽,該標簽只需有一個 ref 屬性,即引用(reference),指明 A 的構造方法的參數為 B 和 C,如下:

<beans>
	<bean id="a" class="cn.god23bin.demo.model.A">
		<constructor-arg ref="b"/>
		<constructor-arg ref="c"/>
	</bean>

	<bean id="b" class="cn.god23bin.demo.model.B"/>

	<bean id="c" class="cn.god23bin.demo.model.C"/>
</beans>

我們知道,B 和 C 是屬於引用類型,類型是確切的,所以直接使用 ref 屬性

如果是基本數據類型,那麼賦值時,就可能會有歧義,Spring 不能確定這個值的類型,比如一個基本數據類型的值是 true這時候歧義就出現了,它是布爾類型還是字元串類型呢?這就需要由我們來告訴 Spring 了,就需要使用 typevalue 屬性來指定

比如:

package cn.god23bin.demo.model;

public class D {
    private final int money;
    private final String power;
    
    public D(int money, String power) {
        this.money = money;
        this.power = power;
    }
}
<bean id="d" class="cn.god23bin.demo.model.D">
	<constructor-arg type="int" value="25000000"/>
	<constructor-arg type="java.lang.String" value="25"/>
</bean>

當然,除了使用 type 指定基本數據類型,還有兩個屬性可以解決歧義,分別是 indexname 屬性。

index 屬性:

使用 index 屬性指定構造方法參數的下標,下標是從 0 開始的,來解決參數類型出現歧義的情況。

<bean id="d" class="cn.god23bin.demo.model.D">
	<constructor-arg index="0" value="25000000"/>
	<constructor-arg index="1" value="25"/>
</bean>

name 屬性:

使用 name 屬性指定構造方法參數的名稱來解決歧義。

<bean id="d" class="cn.god23bin.demo.model.D">
	<constructor-arg name="money" value="25000000"/>
	<constructor-arg name="power" value="25"/>
</bean>

基於 Setter 的依賴註入

基於 Setter 的 DI,是在 Spring IoC 容器調用了 Bean 的無參構造方法或者無參的靜態工廠方法實例化了 Bean 之後,再來調用 Bean 的 Setter 方法來實現的。

下麵這個依舊是普通的 Java Bean,當然你加上相關業務邏輯,就不是純粹的 Java Bean 了,不過 Spring 依舊能夠管理這種對象。

Vehicle:

public class Vehicle {
    
    // Vehicle 持有的 Producer 依賴項
    private Producer pro;
    
    // 同理
    private Author aut;
    
    // 同理
    private int money;
    
    // setter 方法,可以讓 Spring IoC 容器調用註入 Producer 對象
    public void setProducer(Producer pro) {
        this.pro = pro;
    }
    
    // 同理
    public void setAut(Author aut) {
        this.aut = aut;
    }
    
    // 同理
    public void setMoney(int money) {
        this.money = money
    }
    
    // 業務邏輯,有關 Producer 的...
}

對應的配置元數據:

<bean id="vehicle" class="cn.god23bin.demo.model.Vehicle">
    <!-- 使用 property 標簽的 ref 屬性註入引用類型的依賴項 -->
    <property name="pro" ref="producer"/>
    <!-- 使用內嵌 ref 標簽 註入 -->
    <property name="aut">
    	<ref bean="author"/>
    </property>
    <property name="money" value="255"/>
</bean>

<bean id="producer" class="cn.god23bin.demo.model.Producer"/>
<bean id="author" class="cn.god23bin.demo.model.Author"/>

Spring IoC 容器(ApplicationContext)支持基於構造方法的 DI 和基於 Setter 的 DI,也支持使用構造方法註入了一部分依賴項後,再使用 Setter 的方式註入其他的依賴項。

我們可以通過配置一個 BeanDefinitionPropertyEditor 來實現這些屬性的註入。但是,我們基本不會這樣用,而是使用 XML 的 Bean 定義,使用註解的 Bean 定義(比如 @Component@Controller 等),又或者使用 @Bean 這種基於 Java 配置類的方式(@Configuration)來定義。

實際上,這些最終都會轉成 BeanDefinition 對象的並被 Spring IoC 使用。

選擇哪個 DI 方式?

在代碼的編寫中,選擇使用構造方法註入還是使用 Setter 註入呢?

官網上是這麼說的:

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Autowired annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

簡而言之,Spring 團隊是推薦用構造方法來完成 DI 的

  • 對於強依賴項,直接用基於構造方法的 DI,這種註入方式能夠保證註入的 Bean 對象不可變,並且確保需要的依賴不為空。此外,構造方法註入的依賴總是能夠在返回客戶端(調用方)的時候保證完全初始化的狀態。

  • 對於可選的弱依賴項,使用基於 Setter 的 DI。當使用 @Autowired 註解,並將註解用到 Setter 方法上的話,那麼這個 Setter 設置的屬性就變成強依賴了。

對於構造方法進行 DI ,其中:

  • 依賴不可變:其實說的就是 final 關鍵字。
  • 依賴不為空:省去了我們對註入的 Bean 的檢查,當要實例化 Bean 的時候,由於自己實現了有參數的構造方法,所以不會調用預設構造方法,那麼就需要 Spring IoC 容器傳入所需要的參數給有參構造方法,所以就兩種情況,有該類型的參數傳入,那麼 OK ;無該類型的參數,那麼報錯。
  • 完全初始化的狀態:這個可以跟上面的依賴不為空結合起來,向構造方法傳參之前,要確保註入的內容不為空,那麼肯定要調用依賴對象的構造方法完成實例化。而在 Java 類載入實例化的過程中,構造方法是最後一步才執行的,所以返回來的實例化對象都是完全初始化之後的狀態。

依賴的處理過程

Spring IoC 容器按如下方式執行 Bean 的依賴處理(Dependency Resolution Process,個人認為把 Resolution 理解成處理,解決等意思比較好)。

  • 根據配置元數據的內容,ApplicationContext 被創建和初始化。這個配置元數據是用來描述所有 Bean 的,它可以是 XML、Java 代碼或註解。
  • 對於每個 Bean 來說,它的依賴是以屬性、構造方法參數或靜態工廠方法的參數(如果你用它代替構造方法)的形式表達的。在 Spring IoC 創建 Bean 的時候,這些依賴會被提供給需要的 Bean。
  • 每個屬性或構造方法參數都是要設置的值的實際定義,或對容器中另一個 Bean 的引用。
  • 每個作為值的屬性或構造方法參數都會從其指定格式轉換為該屬性或構造方法參數的實際類型。預設情況下,Spring 可以將以字元串格式提供的值轉換為所有內置類型,如 intlongStringboolean 等等。

當 Spring IoC 容器被創建時,Spring IoC 容器一開始就會校驗每個 Bean 的配置。在創建 Bean 之前,Bean 的屬性本身不會被設置。

而且當容器被創建時,那些具有單例作用域的 Bean 也預設會被創建。對於其他情況的 Bean,只有在被請求時才會創建。

一個 Bean 的創建是有可能導致一堆 Bean 被創建,這一堆的 Bean 被稱為 Bean 圖(a graph of beans),因為 Bean 與 Bean 之間可能存在相互依賴,如同社會中的人一樣,都有相關的聯繫,所以形成 Bean 圖。

Bean 作用域在進行 Bean 定義的時候可以進行定義,使用 scope 屬性即可定義 Bean 的作用域。

迴圈依賴

當你使用構造函數註入的時候,某種情況下可能產生一個無法解決的迴圈依賴問題。那什麼是迴圈依賴

舉個例子:

public class A {
    private B b;
    
    public A(B b) {
        this.b = b;
    }
}
public class B {
    private A a;
    
    public B(A a){
        this.a = a;
    }
}

如上所示,A 類通過構造方法註入需要 B 類的對象,而 B 類也通過構造方法註入 A 類的一個對象,這種情況就是所謂的迴圈依賴,A 和 B 之間相互依賴。

A 和 B 之間的迴圈依賴關係會迫使其中一個 Bean 在未完全初始化之前,被註入到另一個 Bean 中。

這時候,Spring IoC 容器會在運行時檢測到這種迴圈的引用,並拋出一個 BeanCurrentlyInCreationException

有一種解決方案就是:修改 A 類或者 B 類的源代碼,使其通過 Setter 方式註入依賴項。

當然,一般情況下,我們可以完全相信 Spring,Spring 會在容器載入時檢測配置問題,如檢查引用不存在的 Bean 或迴圈依賴等。它會儘量晚地設置 Bean 的屬性以及處理依賴的關係。

假設當你請求一個對象時,如果在創建該對象或其依賴關係時出現問題,已經正確載入的 Spring 容器就會產生異常。所以,這意味著我們需要提前發現這些問題。

預設情況下,ApplicationContext 會預先實例化單例 Bean,所以在創建 Spring IoC 容器時會花費一些時間和記憶體。但好處是可以在容器創建時發現配置問題,而不是在後續出現。

如果不存在迴圈依賴關係,一個 Bean 在被註入到另一個 Bean 之前會被完全配置。這意味著當 A 依賴於 B 時,Spring IoC 容器會先完全配置好 B(包括實例化 Bean、設置依賴關係和調用相關的生命周期方法),再調用 A 的 Setter 方法。

依賴註入示例

下麵寫下示例,快速回顧下上面的兩種註入方式,幫助大家更好理解在以 XML 作為配置元數據的情況下,使用基於構造方法的 DI 和基於 Setter 的 DI。

  1. 基於構造方法的 DI:
package cn.god23bin.demo.service;

import cn.god23bin.demo.repository.UserRepository;

public class UserService {
	
    // UserService 的依賴項 UserRepository
	private UserRepository userRepository;

    // 使用構造方法進行 DI,將 UserRepository 註入到 UserService
	public UserService(UserRepository userRepository) {
    	this.userRepository = userRepository;
	}
    
}
<bean id="userService" class="cn.god23bin.demo.service.UserService">
   <constructor-arg ref="userRepository" />
</bean>

<bean id="userRepository" class="cn.god23bin.demo.repository.UserRepository" />
  1. 基於 Setter 的 DI:
package cn.god23bin.demo.service;

import cn.god23bin.demo.repository.UserRepository;

public class UserService {
    
	private UserRepository userRepository;

	public void setUserRepository(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
    
}
<bean id="userService" class="cn.god23bin.demo.service.UserService">
   <property name="userRepository" ref="userRepository" />
</bean>

<bean id="userRepository" class="cn.god23bin.demo.repository.UserRepository" />

總結

本文開頭舉了個例子講了什麼是依賴註入,實際上就是一個過程,把一個 Bean 註入到另一個 Bean 中的過程。

這個過程,在 Spring 中有兩種方式來實現,一種是基於構造方法的 DI,另一種是基於 Setter 的DI。當然,網上講到的依賴註入,還有好幾種方式,目前對於我們來說,知道這兩種就 OK。

對於這兩種 DI,我們也說瞭如何選擇,具體看官網的說法,推薦的是基於構造方法的 DI,當然,具體問題具體分析,有時候是需要用到基於 Setter 的 DI 的,比如解決迴圈依賴的時候。

接著也說了依賴的處理過程,簡單點就是 Spring 會先根據配置元數據去創建 ApplicationContext 這個作為 Spring IoC 容器,在容器創建時,會驗證 Bean 是否正確配置了,預設單例的 Bean 會先被創建等等等的處理。同時可能遇到迴圈依賴的問題,一種解決方案就是將某一個 Bean 使用 Setter 的方式註入依賴項。

以上,便是本篇文章的內容了。下期咱們講講依賴註入的一些細節寫法,比如有的依賴項是集合,那麼配置元數據中該如何寫這個集合,進而實現集合的依賴註入等等。

希望本篇有幫助到大家,有興趣的話可以關註我,關註這個系列!

最後的最後

希望各位屏幕前的靚仔靚女們給個三連!你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!

咱們下期再見!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## HTML ### HTML歷史 HTML(Hypertext Markup Language)的歷史可以追溯到上世紀90年代初,以下是HTML的主要歷史階段: 1. HTML 1.0:在1991年發佈,是HTML的最初版本,用於創建基本的文本和鏈接結構,但功能有限。 2. HTML 2.0:於 ...
  • 本系列文章是為學習Vue的項目練習筆記,儘量詳細記錄一下一個完整項目的開發過程。面向初學者,本人也是初學者,搬磚技術還不成熟。項目在技術上前端為主,包含一些後端代碼,從基礎的資料庫(Sqlite)、到後端服務Node.js(Express),再到Web端的Vue,包含服務端、管理後臺、商城網站、小程... ...
  • 今天使用 hbuilder 運行到 ios 真機的時候,突然發現還需要 ipa 簽名,這是什麼東東呢? 1、IPA 簽名是什麼? 因蘋果公司禁止企業證書用於非企業內部開發者。所以開發者無法再使用DCloud的企業證書簽名的標準運行基座。 運行標準基座到iOS真機設備前,需要使用開發者的證書對基座簽名 ...
  • 本系列文章是為學習Vue的項目練習筆記,儘量詳細記錄一下一個完整項目的開發過程。面向初學者,本人也是初學者,搬磚技術還不成熟。項目在技術上前端為主,包含一些後端代碼,從基礎的資料庫(Sqlite)、到後端服務Node.js(Express),再到Web端的Vue,包含服務端、管理後臺、商城網站、小程... ...
  • 作為一個前端語言,Javascript從最初只是用來寫頁面,到如今的移動終端、後端服務、神經網路等等,它變得幾乎無處不在。如此廣闊的應用領域,對語言的安全性、健壯性以及可維護性都有了更高的要求。儘管ECMAScript標準在近幾年有了長足的進步,但是在類型檢查方面依然毫無建樹。在這種情況下TypeS... ...
  • 最終成果,實現了一個可運行的核心路由工程:柏成/vue-router3.x。地址如下:https://gitee.com/lbcjs/vue-router3.x ...
  • 摘要:在本文中,我們將介紹如何使用Vue3和Spring Framework進行開發,並創建一個簡單的TodoList應用程式。 本文分享自華為雲社區《Vue3搭配Spring Framework開發【Vue3應用程式實戰】》,作者:黎燃。 一、介紹 Vue3和Spring Framework都是現 ...
  • 作者總結這些年在支付寶做架構的經驗,把自己摸索成長的內容寫下來,從對架構師的認知到業務能力和架構能力多方面總結了案例經驗,希望可以幫助到大家。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...