相依性注入
相依性注入 (DI) 是一種程序,物件僅透過建構子引數、工廠方法的引數或在物件實例建構或從工廠方法傳回後設定的屬性來定義其相依性 (即它們與之協同運作的其他物件)。然後,容器會在建立 Bean 時注入這些相依性。此程序從根本上來說是 Bean 本身透過使用類別的直接建構或服務定位器模式來控制其相依性的實例化或位置的反向 (因此得名為控制反轉)。
使用 DI 原則,程式碼更簡潔,當物件具備其相依性時,解耦更有效。物件不會查找其相依性,也不知道相依性的位置或類別。因此,您的類別變得更容易測試,尤其是在相依性位於介面或抽象基底類別上時,這允許在單元測試中使用 Stub 或 Mock 實作。
DI 存在兩種主要變體:基於建構子的相依性注入和基於 Setter 的相依性注入。
基於建構子的相依性注入
基於建構子的 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)
假設 ThingTwo
和 ThingThree
類別沒有繼承關係,則不存在潛在的歧義。因此,以下組態運作良好,您不需要在 <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 容器實例。
相依性解析程序
容器執行 Bean 相依性解析的方式如下
-
ApplicationContext
是使用描述所有 Bean 的組態中繼資料建立和初始化的。組態中繼資料可以由 XML、Java 程式碼或註解指定。 -
對於每個 Bean,其相依性以屬性、建構子引數或靜態工廠方法的引數 (如果您使用靜態工廠方法而不是一般建構子) 的形式表示。當實際建立 Bean 時,這些相依性會提供給 Bean。
-
每個屬性或建構子引數都是要設定的值的實際定義,或是對容器中另一個 Bean 的參考。
-
每個作為值的屬性或建構子引數都會從其指定的格式轉換為該屬性或建構子引數的實際類型。預設情況下,Spring 可以將字串格式提供的值轉換為所有內建類型,例如
int
、long
、String
、boolean
等。
Spring 容器會在建立容器時驗證每個 Bean 的組態。但是,Bean 屬性本身要等到實際建立 Bean 後才會設定。單例作用域且設定為預先實例化 (預設) 的 Bean 會在建立容器時建立。Bean 作用域中定義了作用域。否則,Bean 僅在被請求時建立。Bean 的建立可能會導致建立 Bean 圖,因為 Bean 的相依性及其相依性的相依性 (依此類推) 會被建立和指派。請注意,這些相依性之間解析不符的情況可能會稍後顯示 - 也就是在首次建立受影響的 Bean 時。
您通常可以信任 Spring 會做正確的事情。它會在容器載入時偵測組態問題,例如對不存在的 Bean 和循環相依性的參考。Spring 會盡可能延遲地設定屬性和解析相依性,即在實際建立 Bean 時。這表示,如果建立物件或其相依性之一時出現問題 (例如,Bean 由於缺少或無效的屬性而擲回例外),則已正確載入的 Spring 容器稍後可能會在您請求物件時產生例外。這種某些組態問題的潛在延遲可見性是 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
屬性之外),因此我們在這裡不討論這些細節。