Spring基礎二:依賴注入

一個應用系統必然包含大量的bean,這些bean之間存在依賴關係。 依賴注入(Dependency injection)是Spring容器的核心功能。Bean可以幾種方式來聲明自己的依賴: 構造方法參數、工廠方法參數、Setter屬性;容器在構造、初始化bean的過程中,將適當的bean引用注入進去。

本章的內容大體位於Spring官方文檔的這個位置

構造參數注入

假設我們有一個bean類聲明如下:

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

我們可以用以下方式定義這個bean:

<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>

只要類型ThingTwo和ThingThree之間沒有繼承關係,容器就能推測出這注入的兩個構造函數參數的順序。
否則的話,我們可能需要顯示指明順序:

<bean id="beanOne" class="x.y.ThingOne">
    <constructor-arg index="0" ref="beanTwo"/>
    <constructor-arg  index="1" ref="beanThree"/>
</bean>

我們也可以注入基本類型字面量,此時需要通過type屬性來指明字面量類型,以告知容器如何匹配參數:

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;
    }
}

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>

還有一種方式是通過參數名字來匹配,如下:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

這種方式能工作的前提是,要麼java代碼以debug模式編譯(否則參數的名字會被抹去),要麼通過註解@ConstructorProperties聲明瞭參數名字:

public class ExampleBean {

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

工廠方法參數注入

工廠方法參數的注入的方式和構造方法參數注入的方式是非常類似的,直接舉例:

public class ExampleBean {

    private ExampleBean(...) {
        ...
    }
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

對應的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"/>

基於屬性Setter方法的注入

顧名思義,代碼示例如下:

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;
    }
}

對應的bean聲明:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne" ref="anotherExampleBean"/>
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

依賴注入的幾個問題

一個bean的依賴注入發生在bean的創建初始化過程當中,如果bean A依賴於B,那麼容器會完成B的初始化,如果B依賴於其他bean,那麼以此類推;容器會給A一個完全初始化好的B實例,換句話說,A的初始化會引發它所依賴的bean都被初始化。

構造參數依賴 VS setter屬性依賴

這兩種方式到底哪種更好呢?構造方法的好處是,確保bean對象一旦被創建,就是完整的,因爲如果它的依賴不被滿足的話,構造方法就不會成功。 而setter屬性依賴沒有這個保證,因此存在這樣的風險:使用者拿到的bean引用尚未完成依賴注入。

因此Spring官方的建議是,對比必需的依賴,採用構造參數方式;可選的依賴採用setter屬性,並且在使用過程中做null檢查。

循環依賴問題

如果A和B相互依賴會怎麼辦呢?答案是,如果A、B都以構造參數來依賴對方,那麼二者的初始化過程是無法完成的,容器會拋出一個異常。這種情況下,只能使用setter屬性依賴。

idref標籤

當我們想用用某個bean的標識符本身,而不是對象引用時,可以使用這個標籤:

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean"/>
    </property>
</bean>

它和下面的配置是等價的:

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>

但是前者更安全,容器會檢查idref指向的bean是否存在。

引用bean的範圍

通常<ref bean="someBean"/>引用的bean是該容器或者父容器中存在的bean。
通過<ref parent="accountService"/>可以指定引用父容器的bean。

如果你想在子容器中創建父容器某個bean的proxy,並且使用相同的bean名字,那麼上面的技術就派上用場。

內部bean

<bean id="outer" class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

如上面的定義所示,這bean直接定義在引用它的地方。這種bean沒有名字,它的scope與外層bean一致,其地方也無法引用它(即使定義了名字和scope也會被忽略)。

Depends On依賴

通常bean之間的依賴是直接引用了目標bean,但有時候存在一些非直接的依賴,需要聲明如下:

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

一種可能的用途是,beanOne雖然不直接使用manager,但是manager初始化過程中執行的某些操作是beanOne正確工作的前提。

“depends on”不但指明瞭初始化順序,如果這兩個bean都是單例bean,也規定了bean銷燬的順序,即manager在beanOne之前。

延遲初始化

對於singleton作用域的bean(作用域的概念在後面,這章示例的所有bean都是singleton的),容器加載配置之後,就會立即初始化它們,除非加上lazy-init屬性。

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>

這樣,這個叫lazy的bean只有在有人嘗試獲取它的時候,纔會初始化。因此如果有另外一個非lazy-init的bean依賴該bean,那麼lazy-init=true的設置就沒太大意義了。

自動注入(Autowire)

在bean的定義中,不顯式指定所依賴的bean,而是讓容器來查找合適的bean。
通過給標籤添加autowire=mode屬性開啓,mode的取值爲:

  1. no 不允許自動注入;
  2. byName 通過查找和屬性同名的bean來注入;
  3. byType 通過查找和屬性同類型的bean來注入;
  4. constructor 同byType,適用於構造參數;

官方文檔沒有提供示例,我嘗試了一下,大概如下:

//類定義
public class MailSystem {
    private MailMaker mailMaker;
    private MailSender sender;

    public MailSystem(MailMaker mailMaker) {
        this.mailMaker = mailMaker;
    }

    public void setSender(MailSender sender) {
        this.sender = sender;
    }

    public void work() {
        sender.sendMail(mailMaker.makeMail());
    }
}

//bean定義
<bean id="mailSender" class="beans.MailSender"/>
<bean id="mailMaker" class="beans.MailMaker"/>

<bean id="mailSystem1" class="beans.MailSystem" autowire="byType">
    <constructor-arg index="0" ref="mailMaker"/>
</bean>

<bean id="mailSystem2" class="beans.MailSystem" autowire="constructor">
    <property name="sender" ref="mailSender"/>
</bean>

一旦選擇autowire,所有的setter方法會被嘗試自動注入。而且屬性和構造參數不能同時啓用autowire。

Autowire的優點是配置更精簡,缺點是配置不夠明確,易產生模糊性。比如按byType方式,如果有多個匹配的bean怎麼辦?

  1. 如果某個bean的autowire-candidate屬性設置成false,那麼autowire會忽略它;
  2. 如果某個bean的primary屬性設置成true,那麼autowire會優先選擇它;
  3. <beans>可以添加一個叫做default-autowire-candidates屬性,比如爲"*Repository",規定只有名字以Repository結尾的bean才能被autowire;
  4. 如果仍然無法消除不確定性,容器會拋出異常。

注:這種autowire的功能太多,反而容易產生混淆,不如後面要講的@Autowire註解。

方法注入(Method Injection)

假設Bean A依賴Bean B,A是singleTon作用域,B是prototype作用域。 那麼如何實現A需要使用B的時候,獲取一個新的實例呢?

假設A的定義聲明對B的依賴,那麼無論是屬性依賴和構造參數依賴,都只會獲得B的單一實例,因爲容器只會在初始化是做一次依賴注入,無法達到我們的目的。一種解決方案是A不聲明對B的依賴,而是每次需要B的時候從容器中去獲取。

class A {
	protected B getB() {
		return context.getB("bname",B.class)。
	}
}

這種方式導致A直接耦合於spring容器,不是一種好方式,第二種方式就是依賴方法注入(LookUp Method Injection)。

依賴方法注入

abstact class A {
	protected abstract B getB();
}

<bean id="b" class="B" scope="prototype">
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="a" class="A">
    <lookup-method name="getB" bean="b"/>
</bean>

類A的getB方法被聲明爲abstract,然後在bean的聲明中,通過lookup-method標籤注入了getB方法的實現:“查找名爲b的bean實例”。

能夠使用此種注入方式的方法簽名必須符合:<public|protected> [abstract] <return-type> theMethodName(no-arguments);

通過java註解實現查找方法註解的方式是:

abstact class A {
	@Lookup("b")
	protected abstract B getB();
}

或者通過返回類型來自動匹配:

abstact class A {
	@Lookup
	protected abstract B getB();
}

任意方法注入

我們可以實現對任意方法的注入或覆蓋。首先要實現MethodReplacer接口,然後使用replaced-method標籤。

public class MyValueCalculator {
    public String computeValue(String input) {
        // some real code...
    }
}
public class ReplacementComputeValue implements MethodReplacer {
    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        String input = (String) args[0];
        ...
        return ...;
    }
}


<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- arbitrary method replacement -->
    <replaced-method name="computeValue" replacer="replacementComputeValue">
        <arg-type>String</arg-type>
    </replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

replacementComputeValue實現了MethodReplacer接口,提供了方法體的實現。標籤通過name和arg-type限定了要替換方法的簽名。

屬性值配置

我們可以在bean的定義中,對屬性的字面值進行配置,基本類型的屬性值配置就不講了,這裏過一下集合類型的屬性值配置;

標籤<list/>, <set/>, <map/>和 分別可以配置List, Set, Map, 以及Properties類型的屬性。
請看示例:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">[email protected]</prop>
            <prop key="support">[email protected]</prop>
            <prop key="development">[email protected]</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

集合屬性的合併、覆蓋

bean之間可以有父子關係(與容器的父子關係無關),子bean的定義可以繼承parent bean的定義,自然也可以繼承集合類型的屬性定義。 此時子bean可以覆蓋、合併某個屬性的定義。

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">[email protected]</prop>
            <prop key="support">[email protected]</prop>
            <prop key="development">[email protected]</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

總結

DI(依賴注入)是Spring容器的核心功能,幫助我們管理bean之間的關係,降低系統模塊之間的耦合性。值得注意的是,Spring的DI是一種完全無侵入的機制,Bean的編寫者,就像寫普通java代碼一樣,在構造參數或setter方法裏來聲明對其他Bean接口的依賴;然後系統的構建者在Spring配置文件裏面配置依賴關係。

本章描述了很多xml格式的依賴注入配置方法;在java註解方式佔主流的今天,我們已無需在花功夫記住這些配置方法,通過示例瞭解原理即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章