天天看點

Spring Framework 注入依賴

3.3. 依賴

典型的企業應用不會隻由單一的對象(或Spring的術語bean)組成。毫無疑問,即使最簡單的系統也需要多個對象共同來展示給使用者一個整體的應用。接下來的的内容除了闡述如何單獨定義一系列bean外,還将描述如何讓這些bean對象一起協同工作來實作一個完整的真實應用。

3.3.1. 注入依賴

依賴注入(DI)背後的基本原理是對象之間的依賴關系(即一起工作的其它對象)隻會通過以下幾種方式來實作:構造器的參數、工廠方法的參數,或給由構造函數或者工廠方法建立的對象設定屬性。是以,容器的工作就是建立bean時注入那些依賴關系。相對于由bean自己來控制其執行個體化、直接在構造器中指定依賴關系或者類似服務定位器(Service Locator)模式這3種自主要制依賴關系注入的方法來說,控制從根本上發生了倒轉,這也正是控制反轉(Inversion of Control, IoC) 名字的由來。

應用DI原則後,代碼将更加清晰。而且當bean自己不再擔心對象之間的依賴關系(甚至不知道依賴的定義指定地方和依賴的實際類)之後,實作更高層次的松耦合将易如反掌。DI主要有兩種注入方式,即Setter注入和構造器注入

3.3.1.1. 構造器注入

基于構造器的DI通過調用帶參數的構造器來實作,每個參數代表着一個依賴。此外,還可通過給stattic工廠方法傳參數來構造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...

}

3.3.1.1.1. 構造器參數解析

構造器參數解析根據參數類型進行比對,如果bean的構造器參數類型定義非常明确,那麼在bean被執行個體化的時候,bean定義中構造器參數的定義順序就是這些參數的順序,依次進行比對,比如下面的代碼

package x.y;

public class Foo {

    public Foo(Bar bar, Baz baz) {

        // ...

    }

}

上述例子中由于構造參數非常明确(這裡我們假定 Bar和 Baz之間不存在繼承關系)。是以下面的配置即使沒有明确指定構造參數順序(和類型),也會工作的很好。

<beans>

    <bean name="foo" class="x.y.Foo">

        <constructor-arg>

            <bean class="x.y.Bar"/>

        </constructor-arg>

        <constructor-arg>

            <bean class="x.y.Baz"/>

        </constructor-arg>

    </bean>

</beans>

我們再來看另一個bean,該bean的構造參數類型已知,比對也沒有問題(跟前面的例子一樣)。但是當使用簡單類型時,比如<value>true<value>,Spring将無法知道該值的類型。不借助其他幫助,他将無法僅僅根據參數類型進行比對,比如下面的這個例子:

package examples;

public class ExampleBean {

    // No. of years to the 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;

    }

}

3.3.1.1.1.1. 構造器參數類型比對

針對上面的場景可以通過使用'type'屬性來顯式指定那些簡單類型的構造參數的類型,比如:

<bean id="exampleBean" class="examples.ExampleBean">

  <constructor-arg type="int" value="7500000"/>

  <constructor-arg type="java.lang.String" value="42"/>

</bean>

3.3.1.1.1.2. 構造參數索引

我們還可以通過index屬性來顯式指定構造參數的索引,比如下面的例子:

<bean id="exampleBean" class="examples.ExampleBean">

  <constructor-arg index="0" value="7500000"/>

  <constructor-arg index="1" value="42"/>

</bean>

通過使用索引屬性不但可以解決多個簡單屬性的混淆問題,還可以解決有可能有相同類型的2個構造參數的混淆問題了,注意index是從0開始。

3.3.1.2. Setter注入

通過調用無參構造器或無參static工廠方法執行個體化bean之後,調用該bean的setter方法,即可實作基于setter的DI。

下面的例子将展示隻使用setter注入依賴。注意,這個類并沒有什麼特别之處,它就是普通的Java類。

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

}

構造器注入還是Setter注入?

由于大量的構造器參數可能使程式變得笨拙,特别是當某些屬性是可選的時候。是以通常情況下,Spring開發團隊提倡使用setter注入。而且setter DI在以後的某個時候還可将執行個體重新配置(或重新注入)(JMX MBean就是一個很好的例子)。

盡管如此,構造器注入還是得到很多純化論者(也有很好的理由)的青睐。一次性将所有依賴注入的做法意味着,在未完全初始化的狀态下,此對象不會傳回給客戶代碼(或被調用),此外對象也不需要再次被重新配置(或重新注入)。

對于注入類型的選擇并沒硬性的規定。隻要能适合你的應用,無論使用何種類型的DI都可以。對于那些沒有源代碼的第三方類,或者沒有提供setter方法的遺留代碼,我們則别無選擇--構造器注入将是你唯一的選擇。

BeanFactory對于它所管理的bean提供兩種注入依賴方式(實際上它也支援同時使用構造器注入和Setter方式注入依賴)。需要注入的依賴将儲存在BeanDefinition中,它能根據指定的PropertyEditor實作将屬性從一種格式轉換成另外一種格式。然而,大部份的Spring使用者并不需要直接以程式設計的方式處理這些類,而是采用XML的方式來進行定義,在内部這些定義将被轉換成相應類的執行個體,并最終得到一個Spring IoC容器執行個體。

處理bean依賴關系通常按以下步驟進行:

根據定義bean的配置(檔案)建立并初始化BeanFactory執行個體(大部份的Spring使用者使用支援XML格式配置檔案的BeanFactory或ApplicationContext實作)。

每個bean的依賴将以屬性、構造器參數、或靜态工廠方法參數的形式出現。當這些bean被實際建立時,這些依賴也将會提供給該bean。

每個屬性或構造器參數既可以是一個實際的值,也可以是對該容器中另一個bean的引用。

每個指定的屬性或構造器參數值必須能夠被轉換成特定的格式或構造參數所需的類型。預設情況下,Spring會以String類型提供值轉換成各種内置類型,比如int、long、String、boolean等。

Spring會在容器被建立時驗證容器中每個bean的配置,包括驗證那些bean所引用的屬性是否指向一個有效的bean(即被引用的bean也在容器中被定義)。然而,在bean被實際建立之前,bean的屬性并不會被設定。對于那些singleton類型和被設定為提前執行個體化的bean(比如ApplicationContext中的singleton bean)而言,bean執行個體将與容器同時被建立。而另外一些bean則會在需要的時候被建立,伴随着bean被實際建立,作為該bean的依賴bean以及依賴bean的依賴bean(依此類推)也将被建立和配置設定。

循環依賴

在采用構造器注入的方式配置bean時,很有可能會産生循環依賴的情況。

比如說,一個類A,需要通過構造器注入類B,而類B又需要通過構造器注入類A。如果為類A和B配置的bean被互相注入的話,那麼Spring IoC容器将檢測出循環引用,并抛出 BeanCurrentlyInCreationException異常。

對于此問題,一個可能的解決方法就是修改源代碼,将某些構造器注入改為setter注入。另一個解決方法就是完全放棄構造器注入,隻使用setter注入。換句話說,除了極少數例外,大部分的循環依賴都是可以避免的,不過采用setter注入産生循環依賴的可能性也是存在的。

與通常我們見到的非循環依賴的情況有所不同,在兩個bean之間的循環依賴将導緻一個bean在被完全初始化的時候被注入到另一個bean中(如同我們常說的先有蛋還是先有雞的情況)。

通常情況下,你可以信賴Spring,它會在容器加載時發現配置錯誤(比如對無效bean的引用以及循環依賴)。Spring會在bean建立時才去設定屬性和依賴關系(隻在需要時建立所依賴的其他對象)。這意味着即使Spring容器被正确加載,當擷取一個bean執行個體時,如果在建立bean或者設定依賴時出現問題,仍然會抛出一個異常。因缺少或設定了一個無效屬性而導緻抛出一個異常的情況的确是存在的。因為一些配置問題而導緻潛在的可見性被延遲,是以在預設情況下,ApplicationContext實作中的bean采用提前執行個體化的singleton模式。在實際需要之前建立這些bean将帶來時間與記憶體的開銷。而這樣做的好處就是ApplicationContext被加載的時候可以盡早的發現一些配置的問題。不過使用者也可以根據需要采用延遲執行個體化來替代預設的singleton模式。

如果撇開循環依賴不談,當協作bean被注入到依賴bean時,協作bean必須在依賴bean之前完全配置好。例如bean A對bean B存在依賴關系,那麼Spring IoC容器在調用bean A的setter方法之前,bean B必須被完全配置,這裡所謂完全配置的意思就是bean将被執行個體化(如果不是采用提前執行個體化的singleton模式),相關的依賴也将被設定好,而且所有相關的lifecycle方法(如IntializingBean的init方法以及callback方法)也将被調用。

3.3.1.3. 一些例子

首先是一個用XML格式定義的Setter DI例子。相關的XML配置如下:

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

    }   

}

正如你所看到的,bean類中的setter方法與xml檔案中配置的屬性是一一對應的。接着是構造器注入的例子:

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

    }

}

如你所見,在xml bean定義中指定的構造器參數将被用來作為傳遞給類ExampleBean構造器的參數。

現在來研究一個替代構造器的方法,采用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"/>

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;

    }

}

請注意,傳給static工廠方法的參數由constructor-arg元素提供,這與使用構造器注入時完全一樣。而且,重要的是,工廠方法所傳回的執行個體的類型并不一定要與包含static工廠方法的類類型一緻。盡管在此例子中它的确是這樣。非靜态的執行個體工廠方法與此相同(除了使用factory-bean屬性替代class屬性外),因而不在此細述。

3.3.2. 依賴配置詳解

正如前面章節所提到的,bean的屬性及構造器參數既可以引用容器中的其他bean,也可以是内聯(inline)bean。在spring的XML配置中使用<property/>和<constructor-arg/>元素定義。

3.3.2.1. 直接變量(基本類型、Strings類型等。)

<value/>元素通過人可以了解的字元串來指定屬性或構造器參數的值。正如前面所提到的,JavaBean PropertyEditor将用于把字元串從java.lang.String類型轉化為實際的屬性或參數類型。

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

  <!-- results in a setDriverClassName(String) call -->

  <property name="driverClassName">

    <value>com.mysql.jdbc.Driver</value>

  </property>

  <property name="url">

    <value>jdbc:mysql://localhost:3306/mydb</value>

  </property>

  <property name="username">

    <value>root</value>

  </property>

  <property name="password">

    <value>masterkaoli</value>

  </property>

</bean>

<property/> 和<constructor-arg/> 元素中也可以使用'value' 屬性,這樣會使我們的配置更簡潔,比如下面的配置:

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

  <!-- results in a setDriverClassName(String) call -->

  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

  <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>

  <property name="username" value="root"/>

  <property name="password" value="masterkaoli"/>

</bean>

Spring團隊更傾向采用屬性方式(使用<value/>元素)來定義value值。當然我們也可以按照下面這種方式配置一個java.util.Properties執行個體:

<bean id="mappings" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

   <!-- typed as a java.util.Properties -->

   <property name="properties">

      <value>

         jdbc.driver.className=com.mysql.jdbc.Driver

         jdbc.url=jdbc:mysql://localhost:3306/mydb

      </value>

   </property>

</bean>

看到什麼了嗎?如果采用上面的配置,Spring容器将使用JavaBean PropertyEditor把<value/>元素中的文本轉換為一個java.util.Properties執行個體。由于這種做法的簡單,是以Spring團隊在很多地方也會采用内嵌的<value/>元素來代替value屬性。

3.3.2.1.1. idref元素

idref元素用來将容器内其它bean的id傳給<constructor-arg/> 或 <property/>元素,同時提供錯誤驗證功能。

<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">

    <property name="targetName">

        <idref bean="theTargetBean" />

    </property>

</bean>

上述bean定義片段完全地等同于(在運作時)以下的片段:

<bean id="theTargetBean" class="..." />

<bean id="client" class="...">

    <property name="targetName" value="theTargetBean" />

</bean>

第一種形式比第二種更可取的主要原因是,使用idref标記允許容器在部署時 驗證所被引用的bean是否存在。而第二種方式中,傳給client bean的targetName屬性值并沒有被驗證。任何的輸入錯誤僅在client bean實際執行個體化時才會被發現(可能伴随着緻命的錯誤)。如果client bean 是prototype類型的bean,則此輸入錯誤(及由此導緻的異常)可能在容器部署很久以後才會被發現。

此外,如果被引用的bean在同一XML檔案内,且bean名字就是bean id,那麼可以使用local屬性,此屬性允許XML解析器在解析XML檔案時對引用的bean進行驗證。

<property name="targetName">

   <!-- a bean with an id of 'theTargetBean' must exist; otherwise an XML exception will be thrown -->

   <idref local="theTargetBean"/>

</property>

上面的例子中,與在ProxyFactoryBean bean定義中使用<idref/>元素指定AOP interceptor的相同之處在于:如果使用<idref/>元素指定攔截器名字,可以避免因一時疏忽導緻的攔截器ID拼寫錯誤。

3.3.2.2. 引用其它的bean(協作者)

在<constructor-arg/>或<property/>元素内部還可以使用ref元素。該元素用來将bean中指定屬性的值設定為對容器中的另外一個bean的引用。如前所述,該引用bean将被作為依賴注入,而且在注入之前會被初始化(如果是singleton bean則已被容器初始化)。盡管都是對另外一個對象的引用,但是通過id/name指向另外一個對象卻有三種不同的形式,不同的形式将決定如何處理作用域及驗證。

第一種形式也是最常見的形式是通過使用<ref/>标記指定bean屬性的目标bean,通過該标簽可以引用同一容器或父容器内的任何bean(無論是否在同一XML檔案中)。XML 'bean'元素的值既可以是指定bean的id值也可以是其name值。

<ref bean="someBean"/>

第二種形式是使用ref的local屬性指定目标bean,它可以利用XML解析器來驗證所引用的bean是否存在同一檔案中。local屬性值必須是目标bean的id屬性值。如果在同一配置檔案中沒有找到引用的bean,XML解析器将抛出一個例外。如果目标bean是在同一檔案内,使用local方式就是最好的選擇(為了盡早地發現錯誤)。

<ref local="someBean"/>

第三種方式是通過使用ref的parent屬性來引用目前容器的父容器中的bean。parent屬性值既可以是目标bean的id值,也可以是name屬性值。而且目标bean必須在目前容器的父容器中。使用parent屬性的主要用途是為了用某個與父容器中的bean同名的代理來包裝父容器中的一個bean(例如,子上下文中的一個bean定義覆寫了他的父bean)。

<!-- in the parent context -->

<bean id="accountService" class="com.foo.SimpleAccountService">

    <!-- insert dependencies as required as here -->

</bean>

<!-- in the child (descendant) context -->

<bean id="accountService"  <-- notice that the name of this bean is the same as the name of the 'parent' bean

      class="org.springframework.aop.framework.ProxyFactoryBean">

      <property name="target">

          <ref parent="accountService"/>  <-- notice how we refer to the parent bean

      </property>

    <!-- insert other configuration and dependencies as required as here -->

</bean>

3.3.2.3. 内部bean

所謂的内部bean(inner bean)是指在一個bean的<property/>或 <constructor-arg/>元素中使用<bean/>元素定義的bean。内部bean定義不需要有id或name屬性,即使指定id 或 name屬性值也将會被容器忽略。

<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中的scope标記及id或name屬性将被忽略。内部bean總是匿名的且它們總是prototype模式的。同時将内部bean注入到包含該内部bean之外的bean是不可能的。

3.3.2.4. 集合

通過<list/>、<set/>、<map/>及<props/>元素可以定義和設定與Java Collection類型對應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>

                <value>an entry</value>

            </key>

            <value>just some string</value>

        </entry>

        <entry>

            <key>

                <value>a ref</value>

            </key>

            <ref bean="myDataSource" />

        </entry>

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

注意:map的key或value值,或set的value值還可以是以下元素:

bean | ref | idref | list | set | map | props | value | null

3.3.2.4.1. 集合的合并

從2.0開始,Spring IoC容器将支援集合的合并。這樣我們可以定義parent-style和child-style的<list/>、<map/>、<set/>或<props/>元素,子集合的值從其父集合繼承和覆寫而來;也就是說,父子集合元素合并後的值就是子集合中的最終結果,而且子集合中的元素值将覆寫父集全中對應的值。

請注意,關于合并的這部分利用了parent-child bean機制。此内容将在後面介紹,不熟悉父子bean的讀者可參見第 3.6 節 “bean定義的繼承”。

Find below an example of the collection merging feature:

下面的例子展示了集合合并特性:

<beans>

<bean id="parent" abstract="true" class="example.ComplexObject">

    <property name="adminEmails">

        <props>

            <prop key="administrator">[email protected]</prop>

            <prop key="support">[email protected]</prop>

        </props>

    </property>

</bean>

<bean id="child" parent="parent">

    <property name="adminEmails">

        <!-- the merge is specified on the *child* collection definition -->

        <props merge="true">

            <prop key="sales">[email protected]</prop>

            <prop key="support">[email protected]</prop>

        </props>

    </property>

</bean>

<beans>

在上面的例子中,childbean的adminEmails屬性的<props/>元素上使用了merge=true屬性。當child bean被容器實際解析及執行個體化時,其 adminEmails将與父集合的adminEmails屬性進行合并。

[email protected]

[email protected]

[email protected]

注意到這裡子bean的Properties集合将從父<props/>繼承所有屬性元素。同時子bean的support值将覆寫父集合的相應值。

對于<list/>、<map/>及<set/>集合類型的合并處理都基本類似,在某個方面<list/>元素比較特殊,這涉及到List集合本身的語義學,就拿維護一個有序集合中的值來說,父bean的清單内容将排在子bean清單内容的前面。對于Map、Set及Properties集合類型沒有順序的概念,是以作為相關的Map、Set及Properties實作基礎的集合類型在容器内部沒有排序的語義。

最後需要指出的一點就是,合并功能僅在Spring 2.0(及随後的版本中)可用。不同的集合類型是不能合并(如map和 list是不能合并的),否則将會抛出相應的Exception。merge屬性必須在繼承的子bean中定義,而在父bean的集合屬性上指定的merge屬性将被忽略。

3.3.2.4.2. 強類型集合(僅适用于Java5+)

你若有幸在使用Java5 或Java 6,那麼你可以使用強類型集合(支援泛型)。比如,聲明一個隻能包含String類型元素的Collection。假若使用Spring來給bean注入強類型的Collection,那就可以利用Spring的類型轉換能,當向強類型Collection中添加元素前,這些元素将被轉換。

public class Foo {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {

        this.accounts = accounts;

    }

}

<beans>

    <bean id="foo" class="x.y.Foo">

        <property name="accounts">

            <map>

                <entry key="one" value="9.99"/>

                <entry key="two" value="2.75"/>

                <entry key="six" value="3.99"/>

            </map>

        </property>

    </bean>

</beans>

在foobean的accounts屬性被注入之前,通過反射,利用強類型Map<String, Float>的泛型資訊,Spring的底層類型轉換機制将會把各種value元素值轉換為Float類型,是以字元串9.99、2.75及3.99就會被轉換為實際的Float類型。

3.3.2.5. Nulls

<null/>用于處理null值。Spring會把屬性的空參數當作空字元串處理。以下的xml片斷将email屬性設為空字元串。

<bean class="ExampleBean">

  <property name="email"><value/></property>

</bean>

這等同于Java代碼: exampleBean.setEmail("")。而null值則可以使用<null>元素可用來表示。例如:

<bean class="ExampleBean">

  <property name="email"><null/></property>

</bean>

上述的配置等同于Java代碼:exampleBean.setEmail(null)。

3.3.2.6. XML配置檔案的簡寫及其他

配置中繼資料冗長不是什麼好事情,是以我們将通過下面的方式來對配置進行“減肥”,第一種做法就是通過使用<property/>來定義值和對其他bean的引用,另一個做法就是采用不同的屬性定義格式。

3.3.2.6.1. XML-based configuration metadata shortcuts

<property/>、<constructor-arg/>及<entry/>元素都支援value屬性(attribute),它可以用來替代内嵌的<value/>元素。因而,以下的代碼:

<property name="myProperty">

  <value>hello</value>

</property>

<constructor-arg>

  <value>hello</value>

</constructor-arg>

<entry key="myKey">

  <value>hello</value>

</entry>

等同于:

<property name="myProperty" value="hello"/>

<constructor-arg value="hello"/>

<entry key="myKey" value="hello"/>

The <property/> and <constructor-arg/> elements support a similar shortcut 'ref' attribute which may be used instead of a full nested <ref/> element. Therefore, the following:

<property/>和<constructor-arg/>支援類似ref的簡寫屬性,它可用來替代整個内嵌的<ref/>元素。因而,以下的代碼:

<property name="myProperty">

  <ref bean="myBean">

</property>

<constructor-arg>

  <ref bean="myBean">

</constructor-arg>

等同于:

<property name="myProperty" ref="myBean"/>

<constructor-arg ref="myBean"/>

注意,盡管存在等同于<ref bean="xxx"> 元素的簡寫形式,但并沒有<ref local="xxx">的簡寫形式,為了對目前xml中bean的引用,你隻能使用完整的形式。

最後,map中entry元素的簡寫形式為key/key-ref 和 value /value-ref屬性,因而,以下的代碼:

<entry>

  <key>

    <ref bean="myKeyBean" />

  </key>

  <ref bean="myValueBean" />

</entry>

等同于:

<entry key-ref="myKeyBean" value-ref="myValueBean"/>

再次強調,隻有<ref bean="xxx">元素的簡寫形式,沒有<ref local="xxx">的簡寫形式。

3.3.2.6.2. 使用p名稱空間配置屬性

給XML配置檔案"減肥"的另一個選擇就是使用p名稱空間,從 2.0開始,Spring支援使用名稱空間的可擴充配置格式。這些名稱空間都是基于一種XML Schema定義。事實上,我們所看到的所有bean的配置格式都是基于一個 XML Schema文檔。

特定的名稱空間并不需要定義在一個XSD檔案中,它隻在Spring核心中存在。我們所說的p名稱空間就是這樣,它不需要一個schema定義,與我們前面采用<property/>元素定義bean的屬性不同的是,當我們采用了p名稱空間,我們就可以在bean元素中使用屬性(attribute)來描述bean的property值。

下面的兩段XML配置檔案中都是用來定義同一個bean:一個采用的是标準的XML格式,一個是采用p名稱空間。

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:p="http://www.springframework.org/schema/p"

    xsi:schemaLocation="http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean name="classic" class="com.example.ExampleBean">

        <property name="email" value="[email protected]/>

    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"

          p:email=">

</beans>

從上面的bean定義中,我們采用p名稱空間的方式包含了一個叫email的屬性,而Spring會知道我們的bean包含了一個屬性(property)定義。我們前面說了,p名稱空間是不需要schema定義的,是以屬性(attribute)的名字就是你bean的property的名字。

This next example includes two more bean definitions that both have a reference to another bean:

下面的例子包含了兩個bean定義,它們都引用了另一個bean

<beans xmlns="

http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:p="http://www.springframework.org/schema/p"

    xsi:schemaLocation="http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean name="john-classic" class="com.example.Person">

        <property name="name" value="John Doe"/>

        <property name="spouse" ref="jane"/>

    </bean>

    <bean name="john-modern"

        class="com.example.Person"

        p:name="John Doe"

        p:spouse-ref="jane"/>

    <bean name="jane" class="com.example.Person">

        <property name="name" value="Jane Doe"/>

    </bean>

</beans>

As you can see, this example doesn't only include a property value using the p-namespace, but also uses a special format to declare property references. Whereas the first bean definition uses <property name="spouse" ref="jane"/> to create a reference from bean john to bean jane, the second bean definition uses p:spouse-ref="jane" as an attribute to do the exact same thing. In this case 'spouse' is the property name whereas the '-ref' part indicates that this is not a straight value but rather a reference to another bean.

上面的例子不僅使用p名稱空間包含了一個屬性(property)值,而且使用了一個特殊的格式聲明了一個屬性引用。在第一個bean定義中使用了<property name="spouse" ref="jane"/>來建立beanjohn到beanjane的引用,而第二個bean定義則采用p:spouse-ref="jane"屬性(attribute)的方式達到了同樣的目的。在這個例子中,"spouse"是屬性(property)名,而"-ref“則用來說明該屬性不是一個具體的值而是對另外一個bean的引用。

注意

需要注意的是,p名稱空間沒有标準的XML格式定義靈活,比如說,bean的屬性名是以Ref結尾的,那麼采用p名稱空間定義就會導緻沖突,而采用标準的XML格式定義則不會出現這種問題。這裡我們提醒大家在項目中還是仔細權衡來決定到底采用那種方式,同時也可以在團隊成員都了解不同的定義方式的基礎上,在項目中根據需要同時選擇三種定義方式。

3.3.2.7. 組合屬性名稱

當設定bean的組合屬性時,除了最後一個屬性外,隻要其他屬性值不為null,組合或嵌套屬性名是完全合法的。例如,下面bean的定義:

<bean id="foo" class="foo.Bar">

  <property name="fred.bob.sammy" value="123" />

</bean>

foo bean有個fred屬性,此屬性有個bob屬性,而bob屬性又有個sammy屬性,最後把sammy屬性設定為123。為了讓此定義能工作, foo的fred屬性及fred的bob屬性在bean被構造後都必須非空,否則将抛出NullPointerException異常。

3.3.3. 使用depends-on

多數情況下,一個bean對另一個bean的依賴最簡單的做法就是将一個bean設定為另外一個bean的屬性。在xml配置檔案中最常見的就是使用 <ref/>元素。在少數情況下,有時候bean之間的依賴關系并不是那麼的直接(例如,當類中的靜态塊的初始化被時,如資料庫驅動的注冊)。depends-on屬性可以用于目前bean初始化之前顯式地強制一個或多個bean被初始化。下面的例子中使用了depends-on屬性來指定一個bean的依賴。

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

<bean id="manager" class="ManagerBean" />

若需要表達對多個bean的依賴,可以在'depends-on'中将指定的多個bean名字用分隔符進行分隔,分隔符可以是逗号、空格及分号等。下面的例子中使用了'depends-on'來表達對多個bean的依賴。

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">

  <property name="manager" ref="manager" />

</bean>

<bean id="manager" class="ManagerBean" />

<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

注意

“depends-on”屬性不僅用來指定初始化時的依賴,同時也用來指定相應的銷毀時的依賴(該依賴隻針對singletonbean)。depends-on屬性中指定的依賴bean會在相關bean銷毀之前被銷毀,進而可以讓使用者控制銷毀順序。

3.3.4. 延遲初始化bean

ApplicationContext實作的預設行為就是在啟動時将所有singleton bean提前進行執行個體化。提前執行個體化意味着作為初始化過程的一部分,ApplicationContext執行個體會建立并配置所有的singleton bean。通常情況下這是件好事,因為這樣在配置中的任何錯誤就會即刻被發現(否則的話可能要花幾個小時甚至幾天)。

有時候這種預設處理可能并不是你想要的。如果你不想讓一個singleton bean在ApplicationContext初始化時被提前執行個體化,那麼可以将bean設定為延遲執行個體化。一個延遲初始化bean将告訴IoC 容器是在啟動時還是在第一次被用到時執行個體化。

在XML配置檔案中,延遲初始化将通過<bean/>元素中的lazy-init屬性來進行控制。例如:

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

<bean name="not.lazy" class="com.foo.AnotherBean"/>

當ApplicationContext實作加載上述配置時,設定為lazy的bean将不會在ApplicationContext啟動時提前被執行個體化,而not.lazy卻會被提前執行個體化。

需要說明的是,如果一個bean被設定為延遲初始化,而另一個非延遲初始化的singleton bean依賴于它,那麼當ApplicationContext提前執行個體化singleton bean時,它必須也確定所有上述singleton 依賴bean也被預先初始化,當然也包括設定為延遲執行個體化的bean。是以,如果Ioc容器在啟動的時候建立了那些設定為延遲執行個體化的bean的執行個體,你也不要覺得奇怪,因為那些延遲初始化的bean可能在配置的某個地方被注入到了一個非延遲初始化singleton bean裡面。

在容器層次上通過在<beans/>元素上使用'default-lazy-init'屬性來控制延遲初始化也是可能的。如下面的配置:

<beans default-lazy-init="true">

    <!-- no beans will be pre-instantiated... -->

</beans>

3.3.5. 自動裝配(autowire)協作者

Spring IoC容器可以自動裝配(autowire)互相協作bean之間的關聯關系。是以,如果可能的話,可以自動讓Spring通過檢查BeanFactory中的内容,來替我們指定bean的協作者(其他被依賴的bean)。autowire一共有五種類型。由于autowire可以針對單個bean進行設定,是以可以讓有些bean使用autowire,有些bean不采用。autowire的友善之處在減少或者消除屬性或構造器參數的設定,這樣可以給我們的配置檔案減減肥![2] 在xml配置檔案中,可以在<bean/>元素中使用autowire屬性指定:

表 3.2. Autowiring modes

模式 說明

no  

byName 根據屬性名自動裝配。此選項将檢查容器并根據名字查找與屬性完全一緻的bean,并将其與屬性自動裝配。例如,在bean定義中将autowire設定為by name,而該bean包含master屬性(同時提供setMaster(..)方法),Spring就會查找名為master的bean定義,并用它來裝配給master屬性。

byType 如果容器中存在一個與指定屬性類型相同的bean,那麼将與該屬性自動裝配。如果存在多個該類型的bean,那麼将會抛出異常,并指出不能使用byType方式進行自動裝配。若沒有找到相比對的bean,則什麼事都不發生,屬性也不會被設定。如果你不希望這樣,那麼可以通過設定dependency-check="objects"讓Spring抛出異常。

constructor 與byType的方式類似,不同之處在于它應用于構造器參數。如果在容器中沒有找到與構造器參數類型一緻的bean,那麼将會抛出異常。

autodetect 通過bean類的自省機制(introspection)來決定是使用constructor還是byType方式進行自動裝配。如果發現預設的構造器,那麼将使用byType方式。

如果直接使用property和constructor-arg注入依賴的話,那麼将總是覆寫自動裝配。而且目前也不支援簡單類型的自動裝配,這裡所說的簡單類型包括基本類型、String、Class以及簡單類型的數組(這一點已經被設計,将考慮作為一個功能提供)。byType和constructor自動裝配模式也可用于數組和指定類型的集合。在這種情況下容器中的所有比對的自動裝配對象将被用于滿足各種依賴。對于key值類型為 String的強類型Map也可以被自動裝配。一個自動裝配的Map的value值将由所比對類型的bean所填充。而Map的key值則是相應的bean的名字。

自動裝配還可以與依賴檢查結合使用,這樣依賴檢查将在自動裝配完成之後被執行。

了解自動裝配的優缺點是很重要的。其中優點包括:

自動裝配能顯著減少配置的數量。不過,采用bean模闆(見這裡)也可以達到同樣的目的。

自動裝配可以使配置與java代碼同步更新。例如,如果你需要給一個java類增加一個依賴,那麼該依賴将被自動實作而不需要修改配置。是以強烈推薦在開發過程中采用自動裝配,而在系統趨于穩定的時候改為顯式裝配的方式。

自動裝配的一些缺點:

盡管自動裝配比顯式裝配更神奇,但是,正如上面所提到的,Spring會盡量避免在裝配不明确的時候進行猜測,因為裝配不明确可能出現難以預料的結果,而且Spring所管理的對象之間的關聯關系也不再能清晰的進行文檔化。

對于那些根據Spring配置檔案生成文檔的工具來說,自動裝配将會使這些工具沒法生成依賴資訊。

另一個問題需要注意的是,當根據類型進行自動裝配的時候,容器中可能存在多個bean定義跟自動裝配的setter方法和構造器參數類型比對。雖然對于數組、集合以及Map,不存在這個問題,但是對于單值依賴來說,就會存在模棱兩可的問題。如果bean定義不唯一,裝配時就會抛出異常,面對這種場景我們有幾個方案進行選擇:第一個方案就是棄自動裝配而改用顯式裝配;第二個方案就是在bean定義中通過設定'autowire-candidate'屬性為'false'來将該bean排除在自動裝配候選名單之外(詳情見接下來的章節);第三個方案是通過在bean定義中設定'primary'屬性為'true'來将該bean設定為首選自動裝配bean。最後,對于使用Java 5的使用者來說,可能會使用注解的形式來配置bean,關于這方面的内容可見第 3.11 節 “基于注解(Annotation-based)的配置”。

但決定是否使用自動裝配式時,沒有絕對的對錯。考慮項目的實際是最好的辦法。比如項目通常不使用自動裝配,那麼使用它來僅僅裝配2個bean定義是很讓人困惑的。

3.3.5.1. 将bean排除在自動裝配之外

你也可以針對單個bean設定其是否為被自動裝配對象。當采用XML格式配置bean時,<bean/>元素的 autowire-candidate屬性可被設為false,這樣容器在查找自動裝配對象時将不考慮該bean。

另一個做法就是使用對bean名字進行模式比對來對自動裝配進行限制。其做法是在<beans/>元素的'default-autowire-candidates'屬性中進行設定。比如,将自動裝配限制在名字以'Repository'結尾的bean,那麼可以設定為"*Repository“。對于多個比對模式則可以使用逗号進行分隔。注意,如果在bean定義中的'autowire-candidate'屬性顯式的設定為'true' 或 'false',那麼該容器在自動裝配的時候優先采用該屬性的設定,而模式比對将不起作用。

對于那些從來就不會被其它bean采用自動裝配的方式來注入的bean而言,這是有用的。不過這并不意味着被排除的bean自己就不能使用自動裝配來注入其他bean,它是可以的,或者更準确地說,應該是它不會被考慮作為其他bean自動裝配的候選者。

3.3.6. 依賴檢查

Spring除了能對容器中bean的依賴設定進行檢查外,還可以檢查bean定義中實際屬性值的設定,當然也包括采用自動裝配方式設定屬性值的檢查。

當需要確定bean的所有屬性值(或者屬性類型)被正确設定的時候,那麼這個功能會非常有用。當然,在很多情況下,bean類的某些屬性會具有預設值,或者有些屬性并不會在所有場景下使用,是以這項功能會存在一定的局限性。就像自動裝配一樣,依賴檢查也可以針對每一個bean進行設定。依賴檢查預設為not,它有幾種不同的使用模式,在xml配置檔案中,可以在bean定義中為dependency-check屬性使用以下幾種值:

表 3.3. 依賴檢查方式

模式 說明

none 沒有依賴檢查,如果bean的屬性沒有值的話可以不用設定。

simple 對于原始類型及集合(除協作者外的一切東西)執行依賴檢查

object 僅對協作者執行依賴檢查

all 對協作者,原始類型及集合執行依賴檢查

假若你在使用Java 5,可以采用源代碼級的注解(annotations)來進行配置,關于這方面的内容可以在第 25.3.1 節 “@Required”這一節找到。

3.3.7. 方法注入

在大部分情況下,容器中的bean都是singleton類型的。如果一個singleton bean要引用另外一個singleton bean,或者一個非singleton bean要引用另外一個非singleton bean時,通常情況下将一個bean定義為另一個bean的property值就可以了。不過對于具有不同生命周期的bean來說這樣做就會有問題了,比如在調用一個singleton類型bean A的某個方法時,需要引用另一個非singleton(prototype)類型的bean B,對于bean A來說,容器隻會建立一次,這樣就沒法在需要的時候每次讓容器為bean A提供一個新的的bean B執行個體。

上述問題的一個解決辦法就是放棄控制反轉。通過實作BeanFactoryAware接口(見這裡)讓bean A能夠感覺bean 容器,并且在需要的時候通過使用getBean("B")方式(見這裡)向容器請求一個新的bean B執行個體。看下下面這個例子,其中故意使用了這種方法:

// a class that uses a stateful Command-style class to perform some processing

package fiona.apple;

// lots of Spring-API imports

import org.springframework.beans.BeansException;

import org.springframework.beans.factory.BeanFactory;

import org.springframework.beans.factory.BeanFactoryAware;

public class CommandManager implements BeanFactoryAware {

   private BeanFactory beanFactory;

   public Object process(Map commandState) {

      // grab a new instance of the appropriate Command

      Command command = createCommand();

      // set the state on the (hopefully brand new) Command instance

      command.setState(commandState);

      return command.execute();

   }

   // the Command returned here could be an implementation that executes asynchronously, or whatever

   protected Command createCommand() {

      return (Command) this.beanFactory.getBean("command"); // notice the Spring API dependency

   }

   public void setBeanFactory(BeanFactory beanFactory) throws BeansException {

      this.beanFactory = beanFactory;

   }

}

上面的例子顯然不是最好的,因為業務代碼和Spring Framework産生了耦合。方法注入,作為Spring IoC容器的一種進階特性,可以以一種幹淨的方法來處理這種情況。

3.3.7.1. Lookup方法注入

這究竟是不是方法注入……

有點像Tapestry 4.0的頁面,寫上abstract屬性,Tapestry會在運作時用具體實作将其覆寫。

在這篇Blog文章中你可以了解更過關于方法注入動機的内容。

Lookup方法注入利用了容器的覆寫受容器管理的bean方法的能力,進而傳回指定名字的bean執行個體。在上述場景中,Lookup方法注入适用于原型bean。Lookup方法注入的内部機制是Spring利用了CGLIB庫在運作時生成二進制代碼功能,通過動态建立Lookup方法bean的子類而達到複寫Lookup方法的目的。

如果你看下上個代碼段中的代碼(CommandManager類),Spring容器動态覆寫了createCommand()方法的實作。你的CommandManager類不會有一點對Spring的依賴,在下面這個例子中也是一樣的:

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

   public Object process(Object commandState) {

      // grab a new instance of the appropriate Command interface

      Command command = createCommand();

      // set the state on the (hopefully brand new) Command instance

      command.setState(commandState);

      return command.execute();

   }

    // okay... but where is the implementation of this method?

   protected abstract Command createCommand();

}

在包含被注入方法的客戶類中(此處是CommandManager),此方法的定義必須按以下形式進行:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是抽象的,動态生成的子類會實作該方法。否則,動态生成的子類會覆寫類裡的具體方法。讓我們來看個例子:

<!-- a stateful bean deployed as a prototype (non-singleton) -->

<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">

  <!-- inject dependencies here as required -->

</bean>

<!-- commandProcessor uses statefulCommandHelper -->

<bean id="commandManager" class="fiona.apple.CommandManager">

  <lookup-method name="createCommand" bean="command"/>

</bean>

在上面的例子中,辨別為commandManager的bean在需要一個新的command bean執行個體時,會調用createCommand方法。重要的一點是,必須将command部署為prototype。當然也可以指定為singleton,如果是這樣的話,那麼每次将傳回相同的command bean執行個體!

請注意,為了讓這個動态子類得以正常工作,需要把CGLIB的jar檔案放在classpath裡。另外,Spring容器要子類化的類不能是final的,要覆寫的方法也不能是final的。同樣的,要測試一個包含抽象方法的類也稍微有些不同,你需要自己編寫它的子類提供該抽象方法的樁實作。最後,作為方法注入目标的bean不能是序列化的(serialized)。

提示

有興趣的讀者也許已經發現ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包裡)的用法和ObjectFactoryCreatingFactoryBean的有些相似,不同的是它允許你指定自己的lookup接口,不一定非要用Spring的lookup接口,比如ObjectFactory。要詳細了解這種方法請參考ServiceLocatorFactoryBean的Javadocs(它的确減少了對Spring的耦合)。

3.3.7.2. 自定義方法的替代方案

比起Lookup 方法注入來,還有一種很少用到的方法注入形式,該注入能使用bean的另一個方法實作去替換自定義的方法。除非你真的需要該功能,否則可以略過本節。

當使用基于XML配置中繼資料檔案時,可以在bean定義中使用replaced-method元素來達到用另一個方法來取代已有方法的目的。考慮下面的類,我們将覆寫computeValue方法:

public class MyValueCalculator {

  public String computeValue(String input) {

    // some real code...

  }

  // some other methods...

}

實作org.springframework.beans.factory.support.MethodReplacer接口的類提供了新的方法定義。

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定義中指定了要配置的原始類和将要覆寫的方法:

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

在<replaced-method/>元素内可包含一個或多個<arg-type/>元素,這些元素用來标明被覆寫的方法簽名。隻有被覆寫(override)的方法存在重載(overload)的情況(同名的多個方法變體)才會使用方法簽名。為了友善,參數的類型字元串可以采用全限定類名的簡寫。例如,下面的字元串都表示參數類型為java.lang.String。

    java.lang.String

    String

    Str

參數的個數通常足夠用來差別每個可能的選擇,這個捷徑能減少很多鍵盤輸入的工作,它允許你隻輸入最短的比對參數類型的字元串。

繼續閱讀