本文將繼續(xù)前文,描述Spring IoC中的依賴處理。
依賴
一般情況下企業(yè)應(yīng)用不會只有一個對象(或者是Spring Bean)。甚至最簡單的應(yīng)用都要多個對象來協(xié)同工作來讓終端用戶看到一個完整的應(yīng)用的。下一部分將解釋開發(fā)者如何從僅僅定義單獨的Bean,到讓這些Bean在一個應(yīng)用中協(xié)同工作。
依賴注入
依賴注入是一個讓對象只通過構(gòu)造參數(shù),工廠方法的參數(shù)或者配置的屬性來定義他們的依賴的過程。這些依賴也是對象所需要協(xié)同工作的對象。容器會在創(chuàng)建Bean的時候注入這些依賴。整個過程完全反轉(zhuǎn)了由Bean自己控制實例化或者引用依賴,所以這個過程也稱之為控制反轉(zhuǎn)。
當(dāng)使用了依賴注入的準(zhǔn)則以后,會更易于管理和解耦對象之間的依賴,使得代碼更加的簡單。對象不再關(guān)注依賴,也不需要知道依賴類的位置。這樣的話,開發(fā)者的類更加易于測試,尤其是當(dāng)開發(fā)者的依賴是接口或者抽象類的情況,開發(fā)者可以輕易在單元測試中mock對象。
依賴注入主要使用兩種方式,一種是基于構(gòu)造函數(shù)的注入,另一種的基于Setter方法的依賴注入。
基于構(gòu)造函數(shù)的依賴注入
基于構(gòu)造函數(shù)的依賴注入是由IoC容器來調(diào)用類的構(gòu)造函數(shù),構(gòu)造函數(shù)的參數(shù)代表這個Bean所依賴的對象。跟調(diào)用帶參數(shù)的靜態(tài)工廠方法基本一樣。下面的例子展示了一個類通過構(gòu)造函數(shù)來實現(xiàn)依賴注入的。需要注意的是,這個類沒有任何特殊的地方,只是一個簡單的,不依賴于任何容器特殊接口,基類或者注解的普通類。
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...
}
構(gòu)造函數(shù)的參數(shù)解析
構(gòu)造函數(shù)的參數(shù)解析是通過參數(shù)的類型來匹配的。如果在Bean的構(gòu)造函數(shù)參數(shù)不存在歧義,那么構(gòu)造器參數(shù)的順序也就是就是這些參數(shù)實例化以及裝載的順序。參考如下代碼:
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
假設(shè)Bar和Baz在繼承層次上不相關(guān),也沒有什么歧義的話,下面的配置完全可以工作正常,開發(fā)者不需要再去<constructor-arg>元素中指定構(gòu)造函數(shù)參數(shù)的索引或類型信息。
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
</bean>
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
</beans>
當(dāng)引用另一個Bean的時候,如果類型確定的話,匹配會工作正常(如上面的例子).當(dāng)使用簡單的類型的時候,比如說<value>true</value>,Spring IoC容器是無法判斷值的類型的,所以是無法匹配的??紤]代碼如下:
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;
}
}
在上面代碼這種情況下,容器可以通過使用構(gòu)造函數(shù)參數(shù)的type屬性來實現(xiàn)簡單類型的匹配。比如:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
或者使用index屬性來指定構(gòu)造參數(shù)的位置,比如:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
這個索引也同時是為了解決構(gòu)造函數(shù)中有多個相同類型的參數(shù)無法精確匹配的問題。需要注意的是,索引是基于0開始的。
開發(fā)者也可以通過參數(shù)的名稱來去除二義性。
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
需要注意的是,做這項工作的代碼必須啟用了調(diào)試標(biāo)記編譯,這樣Spring才可以從構(gòu)造函數(shù)查找參數(shù)名稱。開發(fā)者也可以使用@ConstructorProperties注解來顯式聲明構(gòu)造函數(shù)的名稱,比如如下代碼:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于Setter方式的依賴注入
基于Setter函數(shù)的依賴注入則是容器會調(diào)用Bean的無參構(gòu)造函數(shù),或者無參數(shù)的工廠方法,然后再來調(diào)用Setter方法來實現(xiàn)的依賴注入。
下面的例子展示了使用Setter方法進(jìn)行的依賴注入,下面的類對象只是簡單的POJO對象,不依賴于任何Spring的特殊的接口,基類或者注解。
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對于基于構(gòu)造函數(shù)的依賴注入,或者基于Setter方式的依賴注入都是支持的。同時也支持使用Setter方式在通過構(gòu)造函數(shù)注入依賴之后再次注入依賴。開發(fā)者在BeanDefinition中可以使用PropertyEditor實例來自由選擇注入的方式。然而,大多數(shù)的開發(fā)者并不直接使用這些類,而是跟喜歡XML形式的bean定義,或者基于注解的組件(比如使用@Component,@Controller等)或者在配置了@Configuration的類上面使用@Bean的方法。
基于構(gòu)造函數(shù)還是基于Setter方法?
因為開發(fā)者可以混用兩者,所以通常比較好的方式是通過構(gòu)造函數(shù)注入必要的依賴通過Setter方式來注入一些可選的依賴。其中,在Setter方法上面的@Required注解可用來構(gòu)造必要的依賴。
Spring隊伍推薦基于構(gòu)造函數(shù)的注入,因為這種方式會促使開發(fā)者將組件開發(fā)成不可變對象而且確保了注入的依賴不為null。而且,基于構(gòu)造函數(shù)的注入的組件被客戶端調(diào)用的時候也是完全構(gòu)造好的。當(dāng)然,從另一方面來說,過多的構(gòu)造函數(shù)參數(shù)也是非常差的代碼方式,這種方式說明類貌似有了太多的功能,最好重構(gòu)將不同職能分離。
基于Setter的注入只是用于可選的依賴,但是也最好配置一些合理的默認(rèn)值。否則,需要對代碼的依賴進(jìn)行非NULL的檢查了?;赟etter方法的注入有一個便利之處在于這種方式的注入是可以進(jìn)行重配置和重新注入的。
依賴注入的兩種風(fēng)格適合大多數(shù)的情況,但是有時使用第三方的庫的時候,開發(fā)者可能并沒有源碼,而第三方的代碼也沒有setter方法,那么就只能使用基于構(gòu)造函數(shù)的依賴注入了。
依賴解析過程
容器對Bean的解析如下:
- 創(chuàng)建并根據(jù)描述的元數(shù)據(jù)來實例化
ApplicationContext。配置元數(shù)據(jù)可以通過XML, Java 代碼,或者注解。 - 每一個Bean的依賴通過構(gòu)造函數(shù)參數(shù)或者屬性或者靜態(tài)工廠方法的參數(shù)等來表示。這些依賴會在Bean創(chuàng)建的的時候注入和裝載。
- 每一個屬性或者構(gòu)造函數(shù)的參數(shù)都是實際定義的值或者引用容器中其他的Bean。
- 每一個屬性或者構(gòu)造參數(shù)可以根據(jù)其指定的類型轉(zhuǎn)換而成。Spring也可以將String轉(zhuǎn)成默認(rèn)的Java內(nèi)在的類型,比如
int,long,String,boolean等。
Spring容器會在容器創(chuàng)建的時候針對每一個Bean進(jìn)行校驗。然而,Bean的屬性在Bean沒有真正創(chuàng)建的時候是不會配置進(jìn)去的。單例類型的Bean是容器創(chuàng)建的時候配置成預(yù)實例狀態(tài)的。Bean的Scope在后續(xù)有介紹。其他的Bean都只有在請求的時候,才會創(chuàng)建。顯然創(chuàng)建Bean對象會有一個依賴的圖。這個圖表示Bean之間的依賴關(guān)系,容器根據(jù)此來決定創(chuàng)建和配置Bean的順序。
循環(huán)依賴
如果開發(fā)者主要使用基于構(gòu)造函數(shù)的依賴注入,那么很有可能出現(xiàn)一個循環(huán)依賴的場景。
比如說:類A在構(gòu)造函數(shù)中依賴于類B的實例,而類B的構(gòu)造函數(shù)依賴類A的實例。如果你這么配置類A和類B相互注入的話,Spring IoC容器會發(fā)現(xiàn)這個運行時的循環(huán)依賴,并且拋出BeanCurrentlyInCreationException。
開發(fā)者可以通過使用Setter方法來配置依賴注入,這樣可以解決這個問題?;蛘呔筒皇褂没跇?gòu)造函數(shù)的依賴注入,僅僅使用基于Setter方法的依賴注入。換言之,盡管不推薦,但是開發(fā)者可以將循環(huán)依賴配置為基于Setter方法的依賴注入。
開發(fā)者可以相信Spring能正確處理Bean。Spring能夠在加載的過程中發(fā)現(xiàn)配置的問題,比如引用到不存在的Bean或者是循環(huán)依賴。Spring會盡可能晚的在Bean創(chuàng)建的時候裝載屬性或者解析依賴。這也意味著Spring容器加載正確后會在Bean注入依賴出錯的時候拋出異常。比如,Bean拋出缺少屬性或者屬性不合法。這延遲的解析也是為什么ApplicationContext的實現(xiàn)會令單例Bean處于預(yù)實例化狀態(tài)。這樣,通過ApplicationContext的創(chuàng)建,可以在真正使用Bean之前消耗一些內(nèi)存代價發(fā)現(xiàn)配置的問題。開發(fā)者也可以覆蓋默認(rèn)的行為讓單例Bean延遲加載,而不是處于預(yù)實例化狀態(tài)。
如果不存在循環(huán)依賴的話,Bean所引用的依賴會優(yōu)先完全構(gòu)造依賴的。舉例來說,如果Bean A依賴于Bean B,那么Spring IoC容器會先配置Bean B,然后調(diào)用Bean A的Setter方法來構(gòu)造Bean A。換言之,Bean先會實例化,然后注入依賴,然后才是相關(guān)的生命周期方法的調(diào)用。
依賴注入的例子
下面的例子會使用基于XML配置的元數(shù)據(jù),然后使用Setter方式進(jìn)行依賴注入。代碼如下:
<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"/>
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;
}
}
在上面的例子當(dāng)中,Setter方法的聲明和XML文件中相一致,下面的例子是基于構(gòu)造函數(shù)的依賴注入
<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"/>
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定義之中的構(gòu)造函數(shù)參數(shù)就是用來構(gòu)造ExampleBean的依賴。
下面的例子,是通過靜態(tài)的工廠方法來返回Bean實例的。
<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"/>
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;
}
}
工廠方法的參數(shù),也是通過 <constructor-arg/>標(biāo)簽來指定的,和基于構(gòu)造函數(shù)的依賴注入是一致的。之前有提到過,返回的類型不需要跟exampleBean中的class屬性一致的,class指定的是包含工廠方法的類。當(dāng)然了,上面的例子是一致的。使用factory-bean的實例工廠方法構(gòu)造Bean的,這里就不多描述了。