Spring核心技術(shù)(二)——Spring的依賴及其注入

本文將繼續(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è)BarBaz在繼承層次上不相關(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的,這里就不多描述了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容