依賴注入

依賴注入(DI)是一個過程,透過它,物件僅透過建構函式引數、工廠方法的引數或在物件例項構建或從工廠方法返回後設置的屬性來定義其依賴項(即它們協同工作的其他物件)。然後,容器在建立 bean 時注入這些依賴項。這個過程與 bean 自身透過直接構造類或服務定位器模式來控制其依賴項的例項化或定位是根本相反的(因此得名“控制反轉”)。

採用 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 註解來明確命名您的建構函式引數。示例類 then 必須如下所示:

  • 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 注入主要應僅用於那些可以在類中分配合理預設值的可選依賴項。否則,程式碼使用依賴項的每個地方都必須執行非空檢查。Setter 注入的一個好處是 setter 方法使該類的物件易於後續重新配置或重新注入。因此,透過 JMX MBeans 進行管理是 Setter 注入的一個引人注目的用例。

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

依賴解析過程

容器按以下方式執行 Bean 依賴解析:

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

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

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

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

Spring 容器在建立時驗證每個 bean 的配置。但是,bean 屬性本身在 bean 實際建立之前不會設定。單例作用域並設定為預例項化(預設)的 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 實現預設預例項化單例 bean 的原因。以一些前期時間和記憶體為代價,在這些 bean 實際需要之前建立它們,您可以在建立 ApplicationContext 時發現配置問題,而不是稍後。您仍然可以覆蓋此預設行為,以便單例 bean 延遲初始化,而不是急切地預例項化。

如果不存在迴圈依賴,當一個或多個協作 bean 被注入到依賴 bean 中時,每個協作 bean 在被注入到依賴 bean 之前都會完全配置。這意味著,如果 bean A 依賴於 bean B,Spring IoC 容器會在呼叫 bean A 上的 setter 方法之前完全配置 bean B。換句話說,bean 被例項化(如果它不是預例項化的單例),其依賴項被設定,並且呼叫相關的生命週期方法(例如 配置的 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 屬性),因此我們在此不討論這些細節。

© . This site is unofficial and not affiliated with VMware.