接上篇:Spring 核心技術(2)
version 5.1.8.RELEASE
1.4 依賴
典型的企業應用程序不會只包含單個對象(或 Spring 術語中的 bean)。即使是最簡單的應用程序也是由很多對象進行協同工作,以呈現出最終用戶所看到的有條理的應用程序。下一節將介紹如何從定義多個獨立的 bean 到實現對象之間相互協作從而實現可達成具體目標的應用程序。
1.4.1 依賴注入
依賴注入(DI)是一鍾對象處理方式,通過這個過程,對象只能通過構造函數參數、工廠方法參數或對象實例化后設置的屬性來定義它們的依賴關系(即它們使用的其他對象)。然后容器在創建 bean 時注入這些依賴項。這個過程從本質上逆轉了 bean 靠自己本身通過直接使用類的構造函數或服務定位模式來控制實例化或定位其依賴的情況,因此稱之為控制反轉。
使用 DI 原則的代碼更清晰,當對象和其依賴項一起提供時,解耦更有效。對象不查找其依賴項,也不知道依賴項的位置或類。因此,尤其是依賴允許在單元測試中使用模擬實現的接口或抽象基類時,類會變得更容易測試。
DI 存在兩個主要變體:基於構造函數的依賴注入和基於 Setter 的依賴注入。
基於構造函數的依賴注入
基於構造函數的 DI 由容器調用具有多個參數的構造函數來完成,每個參數表示一個依賴項。和調用具有特定參數的靜態工廠方法來構造 bean 幾乎一樣,本次討論用相同的方式處理構造函數和靜態工廠方法的參數。以下示例顯示了一個只能通過構造函數注入進行依賴注入的類:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private 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...
}
請注意,這個類沒有什么特別之處。它是一個不依賴於特定容器接口、基類或注釋的POJO。
構造函數參數解析
通過使用的參數類型進行構造函數參數解析匹配。如果 bean 定義的構造函數參數中不存在潛在的歧義,那么在 bean 定義中定義構造函數參數的順序就是在實例化 bean 時將這些參數提供給適當的構造函數的順序。參考以下類:
package x.y;
public class ThingOne {
public 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 無法確定值的類型,因此無法在沒有幫助的情況下按類型進行匹配。參考以下類:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
構造函數參數類型匹配
在前面的場景中,如果使用 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>
請記住,為了使這項工作開箱即用,必須在啟用調試標志的情況下編譯代碼,以便 Spring 可以從構造函數中查找參數名稱。如果您不能或不想使用 debug 標志編譯代碼,則可以使用 JDK 批注 @ConstructorProperties 顯式命名構造函數參數。然后,示例類必須如下所示:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基於 Setter 的依賴注入
基於 setter 的 DI 由容器在調用無參數構造函數或無參數靜態工廠方法來實例化 bean 之后調用 setter 方法完成。
以下示例顯示了一個只能通過使用純 setter 注入進行依賴注入的類。這個類是傳統的 Java 類。它是一個POJO,它不依賴於特定容器接口、基類或注釋。
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...
}
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 方法上使用 @Required 注解可使屬性成為必需的依賴項,然而更推薦使用編程式參數驗證的構造函數注入。
Spring 團隊通常提倡構造函數注入,因為它允許你將應用程序組件實現為不可變對象,並確保所需的依賴項不是
null。此外,構造函數注入的組件始終以完全初始化的狀態返回給客戶端(調用)代碼。旁注:大量的構造函數參數是一個糟糕的代碼味道,意味着該類可能有太多的責任,應該重構以更好地進行關注點的分離。Setter 注入應僅用於可在類中指定合理默認值的可選依賴項。否則,必須在代碼使用依賴項的所有位置執行非空檢查。setter 注入的一個好處是 setter 方法使該類的對象可以在以后重新配置或重新注入。因此,通過 JMX MBean 進行管理是 setter 注入的一個很好的使用場景。
使用對特定類最有意義的 DI 方式。有時在處理沒有源碼的第三方類時需要你自己做選擇。例如,如果第三方類沒有暴露任何 setter 方法,那么構造函數注入可能是唯一可用的 DI 方式。
依賴處理過程
容器執行 bean 依賴性解析過程如下:
- 創建
ApplicationContext,之后根據描述所有 Bean 的配置元數據進行初始化。配置元數據可以由 XML、Java代碼或注解指定。 - 每個 bean 的依賴關系都以屬性、構造函數參數或靜態工廠方法參數(如果使用它而不是普通的構造函數)的形式表示。實際創建 bean 時,會將這些依賴項提供給 bean。
- 每個屬性或構造函數參數都實際定義了需要設置的值或對容器中另一個 bean 的引用。
- 每個屬性或構造函數參數都是一個從其指定的格式轉換為該屬性或構造函數參數實際類型的值。默認情況下,Spring 能夠將提供的字符串格式轉換成所有內置類型的值,例如
int、long、String、boolean等等。
Spring 容器在創建時驗證每個 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 和循環依賴關系。當實際創建 bean 時,Spring 會盡可能晚地設置屬性並解析依賴關系。這意味着容器正常加載后,如果在創建對象或其中一個依賴項時出現問題,Spring 容器會捕獲一個異常 - 例如,bean 因屬性缺失或無效而拋出異常。可能會稍后發現一些配置問題,所以 ApplicationContext 默認情況下實現預實例化單例 bean。在實際需要之前創建這些 bean 是以前期時間和內存為代價的,ApplicationContext 會在創建時發現配置問題,而不是更晚。你仍然可以覆蓋此默認行為,以便單例 bean 可以延遲初始化,而不是預先實例化。
如果不存在循環依賴關系,當一個或多個協作 bean 被注入到依賴 bean 時,每個協作 bean 在注入到依賴 bean 之前會被完全配置。這意味着,如果 bean A 依賴於 bean B,那么 Spring IoC 容器在調用 bean A 上的 setter 方法之前會完全配置 bean B。換句話說,bean 已經被實例化(如果它不是預先實例化的單例),依賴項已經被設置,並調用了相關的生命周期方法(如配置初始化方法 或 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 類:
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;
}
}
在前面的示例中,聲明 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 類:
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;
}
}
bean 定義中指定的構造函數參數將作為 ExampleBean 的構造函數的參數 。
現在思考這個例子的變體,不使用構造函數,而是告訴 Spring 調用靜態工廠方法來返回對象的實例:
<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 類:
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;
}
}
靜態工廠方法的參數由 <constructor-arg/> 元素提供,與實際使用的構造函數完全相同。工廠方法返回的類的類型不必與包含靜態工廠方法的類相同(盡管在本例中是)。實例(非靜態)工廠方法可以以基本相同的方式使用(除了使用 factory-bean 屬性而不是 class 屬性),因此我們不在此討論這些細節。
