依賴注入

依賴注入(DI)是一個過程,其中物件僅透過建構函式引數、工廠方法引數或在物件例項構造後或從工廠方法返回後設置的屬性來定義它們的依賴項(即它們與之協作的其他物件)。然後容器在建立 bean 時注入這些依賴項。這個過程本質上是與 bean 自己透過直接構建類或使用服務定位器模式來控制其依賴項的例項化或位置相反的(因此得名:控制反轉)。遵循 DI 原則使程式碼更整潔,並且在為物件提供依賴項時,解耦更有效。物件不會查詢其依賴項,也不知道依賴項的位置或類。因此,您的類變得更容易測試,尤其當依賴項是介面或抽象基類時,這允許在單元測試中使用樁(stub)或模擬(mock)實現。

運用依賴注入(DI)原則可使程式碼更簡潔,當物件被提供其依賴項時,解耦效果更佳。物件無需查詢其依賴項,也不知道依賴項的位置或型別。這樣一來,你的類就更容易進行測試,特別是當依賴項基於介面或抽象基類時,這使得在單元測試中使用存根或模擬實現成為可能。

基於建構函式的依賴注入

基於建構函式的 DI 是透過容器呼叫帶有多個引數(每個引數代表一個依賴項)的建構函式來實現的。呼叫帶有特定引數的 static 工廠方法來構造 bean 幾乎等同,並且這裡的討論將建構函式引數與 static 工廠方法引數類似處理。以下示例顯示了一個只能透過建構函式注入進行依賴注入的類

  • Java

  • Kotlin

public class SimpleMovieLister {

	// the SimpleMovieLister has a dependency on a MovieFinder
	private final MovieFinder movieFinder;

	// a constructor so that the Spring container can inject a MovieFinder
	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// business logic that actually uses the injected MovieFinder is omitted...
}
// a constructor so that the Spring container can inject a MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
	// business logic that actually uses the injected MovieFinder is omitted...
}

請注意,這個類沒有什麼特別之處。它是一個 POJO,不依賴於容器特定的介面、基類或註解。

建構函式引數解析

建構函式引數解析匹配是使用引數型別進行的。如果 bean 定義的建構函式引數中不存在潛在的歧義,則 bean 定義中建構函式引數的定義順序就是例項化 bean 時將這些引數提供給相應建構函式的順序。考慮以下類

  • Java

  • Kotlin

package x.y;

public class ThingOne {

	public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
		// ...
	}
}
package x.y

class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)

假設 ThingTwoThingThree 類之間沒有繼承關係,則不存在潛在的歧義。因此,以下配置可以正常工作,您無需在 <constructor-arg/> 元素中顯式指定建構函式引數索引或型別。

<beans>
	<bean id="beanOne" class="x.y.ThingOne">
		<constructor-arg ref="beanTwo"/>
		<constructor-arg ref="beanThree"/>
	</bean>

	<bean id="beanTwo" class="x.y.ThingTwo"/>

	<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

當引用另一個 bean 時,型別是已知的,可以進行匹配(如前一個示例所示)。當使用簡單型別時,例如 <value>true</value>,Spring 無法確定值的型別,因此無法在沒有幫助的情況下按型別匹配。考慮以下類

  • Java

  • Kotlin

package examples;

public class ExampleBean {

	// Number of years to calculate the Ultimate Answer
	private final int years;

	// The Answer to Life, the Universe, and Everything
	private final String ultimateAnswer;

	public ExampleBean(int years, String ultimateAnswer) {
		this.years = years;
		this.ultimateAnswer = ultimateAnswer;
	}
}
package examples

class ExampleBean(
	private val years: Int, // Number of years to calculate the Ultimate Answer
	private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything
)

建構函式引數型別匹配

在前面的場景中,如果您透過 type 屬性顯式指定建構函式引數的型別,容器可以使用簡單型別進行型別匹配,如下例所示

<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg type="int" value="7500000"/>
	<constructor-arg type="java.lang.String" value="42"/>
</bean>

建構函式引數索引

您可以使用 index 屬性顯式指定建構函式引數的索引,如下例所示

<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg index="0" value="7500000"/>
	<constructor-arg index="1" value="42"/>
</bean>

除了解決多個簡單值的歧義外,指定索引還可以解決建構函式具有兩個相同型別引數時的歧義。

索引從 0 開始。

建構函式引數名稱

您還可以使用建構函式引數名稱來消除值的歧義,如下例所示

<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg name="years" value="7500000"/>
	<constructor-arg name="ultimateAnswer" value="42"/>
</bean>

請記住,為了使此功能開箱即用,您的程式碼必須使用啟用 -parameters 標誌進行編譯,以便 Spring 可以從建構函式中查詢引數名稱。如果您不能或不想使用 -parameters 標誌編譯程式碼,您可以使用 @ConstructorProperties JDK 註解顯式命名您的建構函式引數。然後示例類將如下所示

  • Java

  • Kotlin

package examples;

public class ExampleBean {

	// Fields omitted

	@ConstructorProperties({"years", "ultimateAnswer"})
	public ExampleBean(int years, String ultimateAnswer) {
		this.years = years;
		this.ultimateAnswer = ultimateAnswer;
	}
}
package examples

class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)

基於 Setter 的依賴注入

基於 Setter 的 DI 是透過容器在呼叫無參建構函式或無參 static 工廠方法例項化 bean 後,呼叫 bean 的 setter 方法來實現的。

以下示例顯示了一個只能透過純 Setter 注入進行依賴注入的類。這個類是傳統的 Java 類。它是一個 POJO,不依賴於容器特定的介面、基類或註解。

  • Java

  • Kotlin

public class SimpleMovieLister {

	// the SimpleMovieLister has a dependency on the MovieFinder
	private MovieFinder movieFinder;

	// a setter method so that the Spring container can inject a MovieFinder
	public void setMovieFinder(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// business logic that actually uses the injected MovieFinder is omitted...
}
class SimpleMovieLister {

	// a late-initialized property so that the Spring container can inject a MovieFinder
	lateinit var movieFinder: MovieFinder

	// business logic that actually uses the injected MovieFinder is omitted...
}

ApplicationContext 支援對其管理的 bean 進行基於建構函式和基於 Setter 的 DI。它也支援在透過建構函式方式注入一些依賴項後,進行基於 Setter 的 DI。您以 BeanDefinition 的形式配置依賴項,並結合 PropertyEditor 例項將屬性從一種格式轉換為另一種格式。然而,大多數 Spring 使用者不直接使用這些類(即以程式設計方式),而是使用 XML bean 定義、註解元件(即帶有 @Component@Controller 等註解的類)或基於 Java 的 @Configuration 類中的 @Bean 方法。然後這些來源在內部轉換為 BeanDefinition 例項,並用於載入整個 Spring IoC 容器例項。

基於建構函式還是基於 Setter 的 DI?

由於可以混合使用基於建構函式和基於 Setter 的 DI,一個好的經驗法則是對強制依賴項使用建構函式,對可選依賴項使用 setter 方法或配置方法。請注意,在 setter 方法上使用 @Autowired 註解可以使該屬性成為必需的依賴項;然而,帶有引數程式設計驗證的建構函式注入更可取。

Spring 團隊通常提倡建構函式注入,因為它允許您將應用程式元件實現為不可變物件,並確保必需的依賴項不是 null。此外,建構函式注入的元件始終以完全初始化的狀態返回給客戶端(呼叫)程式碼。順帶一提,大量的建構函式引數是一種不好的程式碼氣味,暗示著該類可能承擔了過多的職責,應該重構以更好地實現關注點分離。

Setter 注入主要應僅用於可在類內分配合理預設值的可選依賴項。否則,程式碼中使用依賴項的任何地方都必須執行非 null 檢查。Setter 注入的一個好處是 setter 方法使得該類的物件可以在稍後重新配置或重新注入。因此,透過 JMX MBeans 進行管理是使用 setter 注入的一個令人信服的用例。

為特定類選擇最適合的 DI 風格。有時,在處理沒有原始碼的第三方類時,選擇是為您做出的。例如,如果第三方類沒有暴露任何 setter 方法,則建構函式注入可能是唯一可用的 DI 形式。

依賴解析過程

容器按如下方式執行 bean 依賴解析

  • 建立 ApplicationContext 並使用描述所有 bean 的配置元資料進行初始化。配置元資料可以透過 XML、Java 程式碼或註解指定。

  • 對於每個 bean,其依賴項以屬性、建構函式引數或靜態工廠方法引數(如果您使用它代替普通建構函式)的形式表達。當 bean 實際建立時,將這些依賴項提供給 bean。

  • 每個屬性或建構函式引數都是要設定的值的實際定義,或者容器中另一個 bean 的引用。

  • 將每個作為值的屬性或建構函式引數從其指定格式轉換為該屬性或建構函式引數的實際型別。預設情況下,Spring 可以將以字串格式提供的值轉換為所有內建型別,例如 intlongStringboolean 等。

Spring 容器在建立時會驗證每個 bean 的配置。然而,bean 屬性本身直到 bean 實際建立時才設定。作用域為 singleton 並設定為預例項化(預設)的 bean 在容器建立時被建立。Bean 作用域 中定義了作用域。否則,bean 僅在請求時才被建立。建立 bean 可能會導致建立 bean 圖,因為 bean 的依賴項及其依賴項的依賴項(依此類推)被建立並分配。請注意,這些依賴項之間的解析不匹配可能會延遲出現——即在受影響的 bean 首次建立時。

迴圈依賴

如果您主要使用建構函式注入,可能會建立無法解決的迴圈依賴場景。

例如:類 A 透過建構函式注入需要類 B 的例項,而類 B 透過建構函式注入需要類 A 的例項。如果您將類 A 和類 B 的 bean 配置為互相注入,Spring IoC 容器會在執行時檢測到此迴圈引用,並丟擲 BeanCurrentlyInCreationException

一種可能的解決方案是編輯某些類的原始碼,改為透過 setter 而不是建構函式進行配置。或者,避免建構函式注入,僅使用 setter 注入。換句話說,儘管不推薦,您可以使用 setter 注入配置迴圈依賴。

與典型情況(沒有迴圈依賴)不同,bean A 和 bean B 之間的迴圈依賴會迫使其中一個 bean 在自身完全初始化之前就被注入到另一個 bean 中(一個經典的雞生蛋還是蛋生雞的問題)。

通常可以信任 Spring 做好事情。它在容器載入時檢測配置問題,例如引用不存在的 bean 和迴圈依賴。Spring 儘可能晚地設定屬性和解析依賴,即在 bean 實際建立時。這意味著正確載入的 Spring 容器在您請求物件時可能會稍後生成異常,如果建立該物件或其依賴項存在問題——例如,bean 由於缺少或無效的屬性而丟擲異常。一些配置問題可能延遲可見,這就是 ApplicationContext 實現預設預例項化 singleton bean 的原因。雖然在實際需要這些 bean 之前會花費一些前期時間和記憶體來建立它們,但您可以在建立 ApplicationContext 時發現配置問題,而不是稍後。您仍然可以覆蓋此預設行為,以便 singleton bean 延遲初始化,而不是被急切地預例項化。

如果不存在迴圈依賴,當一個或多個協作 bean 被注入到依賴 bean 中時,每個協作 bean 在被注入到依賴 bean 之前都已完全配置。這意味著,如果 bean A 依賴於 bean B,Spring IoC 容器會在呼叫 bean A 的 setter 方法之前完全配置 bean B。換句話說,bean 被例項化(如果它不是預例項化的 singleton),其依賴項被設定,並且相關的生命週期方法(例如 配置的 init 方法InitializingBean 回撥方法)被呼叫。

依賴注入示例

以下示例使用基於 XML 的配置元資料進行基於 Setter 的 DI。Spring XML 配置檔案的一小部分指定了一些 bean 定義如下

<bean id="exampleBean" class="examples.ExampleBean">
	<!-- setter injection using the nested ref element -->
	<property name="beanOne">
		<ref bean="anotherExampleBean"/>
	</property>

	<!-- setter injection using the neater ref attribute -->
	<property name="beanTwo" ref="yetAnotherBean"/>
	<property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

以下示例顯示了相應的 ExampleBean

  • Java

  • Kotlin

public class ExampleBean {

	private AnotherBean beanOne;

	private YetAnotherBean beanTwo;

	private int i;

	public void setBeanOne(AnotherBean beanOne) {
		this.beanOne = beanOne;
	}

	public void setBeanTwo(YetAnotherBean beanTwo) {
		this.beanTwo = beanTwo;
	}

	public void setIntegerProperty(int i) {
		this.i = i;
	}
}
class ExampleBean {
	lateinit var beanOne: AnotherBean
	lateinit var beanTwo: YetAnotherBean
	var i: Int = 0
}

在前面的示例中,聲明瞭 setter 方法以匹配 XML 檔案中指定的屬性。以下示例使用基於建構函式的 DI

<bean id="exampleBean" class="examples.ExampleBean">
	<!-- constructor injection using the nested ref element -->
	<constructor-arg>
		<ref bean="anotherExampleBean"/>
	</constructor-arg>

	<!-- constructor injection using the neater ref attribute -->
	<constructor-arg ref="yetAnotherBean"/>

	<constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

以下示例顯示了相應的 ExampleBean

  • Java

  • Kotlin

public class ExampleBean {

	private AnotherBean beanOne;

	private YetAnotherBean beanTwo;

	private int i;

	public ExampleBean(
		AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
		this.beanOne = anotherBean;
		this.beanTwo = yetAnotherBean;
		this.i = i;
	}
}
class ExampleBean(
		private val beanOne: AnotherBean,
		private val beanTwo: YetAnotherBean,
		private val i: Int)

bean 定義中指定的建構函式引數被用作 ExampleBean 的建構函式引數。

現在考慮這個示例的一個變體,其中 Spring 被告知呼叫一個 static 工廠方法來返回物件例項,而不是使用建構函式

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
	<constructor-arg ref="anotherExampleBean"/>
	<constructor-arg ref="yetAnotherBean"/>
	<constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

以下示例顯示了相應的 ExampleBean

  • Java

  • Kotlin

public class ExampleBean {

	// a private constructor
	private ExampleBean(...) {
		...
	}

	// a static factory method; the arguments to this method can be
	// considered the dependencies of the bean that is returned,
	// regardless of how those arguments are actually used.
	public static ExampleBean createInstance (
		AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

		ExampleBean eb = new ExampleBean (...);
		// some other operations...
		return eb;
	}
}
class ExampleBean private constructor() {
	companion object {
		// a static factory method; the arguments to this method can be
		// considered the dependencies of the bean that is returned,
		// regardless of how those arguments are actually used.
		@JvmStatic
		fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
			val eb = ExampleBean (...)
			// some other operations...
			return eb
		}
	}
}

static 工廠方法的引數由 <constructor-arg/> 元素提供,與實際使用建構函式的方式完全相同。工廠方法返回的類型別不必與包含 static 工廠方法的類型別相同(儘管在此示例中是相同的)。例項(非靜態)工廠方法的使用方式本質上是相同的(除了使用 factory-bean 屬性代替 class 屬性),因此我們在此不討論這些細節。