大數據

Spring 5 中文解析核心篇-IoC容器之依賴關係

一個典型的企業應用不是由一個簡單的對象(在Spring中叫bean)組成。即使是最簡單的應用程序,也有一些對象協同工作,以呈現最終用戶視為一致的應用程序。(備註:相當於所有的bean一起協同工作對於用戶是無感知的)。下一部分將說明如何從定義多個獨立的Bean對象協作去實現應用程序的目標。

1.4.1 依賴注入

依賴注入是從工廠方法構造或返回的實例並通過設置對象實例的構造參數、工廠方法參數或者屬性去定義它的依賴關係(與它一起工作的對象)的過程。當創建bean的時候容器注入這些依賴。從根本上講,此過程是通過使用類的直接構造或服務定位器模式來控制bean自身依賴關係的實例化或位置的bean本身的逆過程(因此稱為控制反轉)。

DI(依賴注入)使代碼更簡潔和解偶,當為這些對象提供依賴時候是更高效的。(通過依賴注入來注入對象更高效)。對象不用去主動查找它的依賴項並且不用知道依賴類的位置。這樣的結果是我們的類變成了更容易被測試,特別地,當這些依賴在接口或者抽象類上時,在單元測試中去使用stub或者mock方式去實現這些接口和抽象類。

DI(依賴注入)存在兩個重要的變體:基於構造函數的依賴注入基於Setter的依賴注入

  • 基於構造函數依賴注入

基於構造函數的DI(依賴注入)是通過容器調用構造函數完成的,每一個構造函數參數代表一個依賴。調用帶有特定參數的靜態工廠方法來構造Bean幾乎是等效的,並且本次討論將構函數和靜態工廠方法的參數視為類似的。下面的例子展示了只能通過構造函數進行對象注入:

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private MovieFinder movieFinder;

    //構造函數注入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) {
        // ...
    }
}

假設ThingTwoThingThree類沒有繼承關係,不存在潛在的歧義。因此,這個配置工作的很好並且我們沒有必要顯示的在元素中指定構造函數參數的索引或類型。

<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時,類型是已知的,可以進行匹配。當一個簡單類型被使用,例如true,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>

除了解決多個簡單值的歧義性之外,指定索引還可以解決歧義,其中構造函數具有兩個相同類型的參數。

index索引從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依賴注入

基於SetterDI(依賴注入)是在bean調用無參構造函數或無參static工廠函數去實例化bean後被容器調用函數去完成的。

下面的例子展示了一個只能通過使用Setter注入的類。這個類是常規的Java。它是一個POJO並且沒有依賴容器特定接口、基礎類或者註解。

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // 通過Setter進行注入
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

ApplicationContext支持基於構造函數和Setter的依賴注入的bean管理。也支持在通過構造函數注入後再通過基於Setter方法注入。可以使用BeanDefinition的形式配置依賴項,將其與PropertyEditor實例結合使用以從一種格式轉換為另一種格式。然後,大多數Spring用戶不會直接使用這些類,而是使用XML的bean定義、註解這些組件(類似@Component, @Controller等等),或者基於Java被標註@Configuration類的方法使用@Bean標註。這些配置數據源內部地轉換為BeanDefinition實例並且被使用於加載整個Spring IoC容器實例。

基於構造函數和Setter函數注入選擇

由於可以混合基於構造函數和Setter函數的依賴注入,將構造函數用於強制性依賴項,將Setter方法或配置方法用於可選性依賴項是一個很好的經驗法則。需要注意在Setter方法上使用 @Required表明這個屬性需要一個依賴;然而,構造函數注入可以通過編程方式校驗參數是可取的。

Spring團隊一般推薦使用構建函數注入,因為可以允許我們去實現不可變的對象組件並且確保需要的依賴不為null。此外,構造函數注入組件總是被返回一個完整初始化的狀態。構造函數大量的參數是一個壞代碼味道,暗示著這個有太多的責任並且應該去重構以更好的分離關注點問題。

Setter注入應該主要使用在可選依賴因為可以在在類中設置一個默認值。否則,必須在代碼使用依賴項的任何地方執行非空檢查。Setter注入的一種好處是可以在後面對Setter方法進行重新配置或重新注入。

使用對特定類最有意義的DI樣式,有時候,當處理第三方類庫沒有源碼的時候,這個選擇是非常適合的。例如:如果第三方類庫沒有暴露任何的Setter方法,構造函數注入可能是依賴注入的唯一有效方式。

  • 依賴解析處理

容器執行bean依賴解析過程:

  • ApplicationContext通過對所有bean的配置元數據描述進行創建和初始化。配置元數據通過XMLJavaConfig或者註解描述。
  • 對於每個bean,它的依賴形式通過屬性、構造函數參數或者static-factory(如果使用常規的構造函數替換)方法參數表達。當這個bean被創建的時候,這些依賴被提供給bean。(備註:被依賴bean先創建)
  • 每一個屬性或者構造函數參數的都要設置一個實際的定義,或引用容器其他bean
  • 每一個屬性或構造函數參數的值從指定的格式轉換為屬性或構造函數參數的真實類型。默認情況下,Spring提供一個字符串格式轉換為所有內建類型的值,例如:int、long、String、boolan等等。

Spring容器驗證每一個創建bean的配置。然而,bean屬性本身沒有被設置,直到bean被真正創建。在創建容器時,將創建單例作用域的bean並將其設置為預實例化(缺省值)。Scope被定義在 Bean Scopes。除此之外,其他的bean創建僅僅在請求容器的時候。bean的創建潛在的導致一些bean的圖被創建(備註:意思是bean所依賴的bean被創建,類似於bean的依賴拓撲圖),類似bean的依賴和它的依賴的依賴bean創建和被賦值。注意:這些依賴項之間的解析不匹配可能會在第一次創建受影響的bean時出現。

​ 循環依賴

如果主要使用構造函數注入,則可能會創建無法解決的循環依賴場景。

例如:類A通過構造函數需要依賴注入類B並且類B通過構造函數依賴注入A。如果配置類A和類B相互依賴注入,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中之前都已完全配置。也就是,如果bean A有依賴bean BSpring IoC容器在調用bean ASetter方法之前完整的配置bean B。換句話說,這個bean被實例化,他的依賴被設置並且關聯的生命週期函數(例如:init方法或者InitializingBean 回調函數)已經被調用。

  • 依賴注入例子

下面的例子基於XML配置元數據去配置基於Setter的依賴注入。Spring XML配置文件指定一些bean的定義:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- 屬性注入:依賴一個bean -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>
    <!-- 屬性注入:依賴一個bean-->
    <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;

    // 指定屬性需要注入bean類型
    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

   // 指定屬性需要注入bean類型
    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

上面的例子,在XML文件中通過Setter聲明屬性匹配類型。下面例子使用構造函數注入:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- 構造函數注入bean  anotherExampleBean-->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>
    <!-- 構造函數注入bean yetAnotherBean-->
    <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構造函數的參數(意思是xml中指定的構造函數引用作為ExampleBean構造函數的參數)。

現在考慮這個例子的變體,使用構造函數替換,Spring調用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"/>

ExampleBean對應的類定義:

public class ExampleBean {

    // a private constructor
  private ExampleBean(...) {
        ...
    }

    //靜態工廠方法
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

靜態工廠方法的參數由元素提供,與實際使用構造函數時完全相同。通過工廠方法返回的類的類型不必與包含靜態工廠方法的類的類型相同(儘管在此例中為相同)。實例(非靜態)工廠方法可以以基本上相同的方式使用(除了使用factory-bean屬性代替class屬性之外),因此不在這裡詳細討論。

參考代碼:com.liyong.ioccontainer.starter.XmlDependecyInjectContainer

1.4.2 詳細介紹依賴項和配置

在前面的章節提到,我們可以定義bean的屬性和構造函數參數去引用其他被管理的bean(協同者)或者作為一個內聯值定義。Spring的XML元數據配置支持子元素類型和這個為了這個設計目的。

  • 直接值(原生類型、字符串等)

元素的value屬性將屬性或構造函數參數指定為人可讀的字符串表示形式。Spring的conversion service 被使用從字符串轉換屬性或參數的實際類型。下面的例子展示各種值的設置:

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

下面的例子使用p命名空間使xml配置方式更簡潔:

<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
    https://www.springframework.org/schema/beans/spring-beans.xsd">

   <!--使用p命名空間 注意: xmlns:p="http://www.springframework.org/schema/p -->
    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="root"
        p:password="masterkaoli"/>

</beans>

前面的XML配置非常的簡潔,但是,拼寫錯誤在運行時被發現而不是在設計時,除非我們使用IDE(例如:Intellij IDEA或者Spring Tools)支持自動屬性完成當我們創建bean定義的時候。IDE助手是非常推薦的。

我們可以配置java.util.Properties實例,例如:

<bean id="mappings"
    class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <!-- 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容器通過使用JavaBeansPropertyEditor機制轉換元素值為java.util.Properties。這是一個不錯的捷徑,並且是Spring更喜歡使用嵌套的元素而不是value屬性樣式。

  • idref元素

idref元素是一個簡單的容錯方式,將容器中另外bean的id傳遞(字符串值-不是引用)到 或元素。下面的例子展示怎樣去使用:

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

參考代碼:com.liyong.ioccontainer.starter.XmlIocContainer

第一種形式優於第二種形式,因為使用idref標記可使容器在部署時驗證所引用的名Bean實際上是否存在。在第二個變體中,不對傳遞給客戶端bean的targetName屬性的值執行驗證。拼寫錯誤僅在實際實例化客戶端bean時才發現(最有可能導致致命的結果)。如果這個客戶端bean是原型bean,這個拼寫和結果異常可能在這個容器部署後很久才被發現。

idref標籤元素上的的local屬性在spring-beans.xsd 4.0後不在支持。因為它沒有提供常規的bean引用值。當升級到spring-beans.xsd 4.0更改idref localidref bean

idref標籤元素帶來的價值的地方是在ProxyFactoryBean bean定義中配置AOP攔截器。指定攔截器名稱時使用元素可防止你拼寫錯誤的攔截器ID。

  • 引用其他的bean

ref元素是<constructor-arg/> 或中定義的最後一個元素。在這裡,我們通過設置一個bean的指定屬性的值引用被容器管理的其他bean。引用的bean是要設置其屬性的bean的依賴關係,並且在設置屬性之前根據需要對其進行初始化。(如果這個協同者是一個單例bean,它可能已經被容器初始化)所有的引用最終引用其他對象。bean的範圍和校驗依賴你是否有指定其他對象通過bean或者parent屬性指定的id或者name

指定目標bean通過標籤的bean屬性是最常見的形式並且允許在同一個容器或父容器引用任何的被創建的bean,而不管是否在同一個XML配置文件。bean屬性的值可以與目標bean的id屬性相同,也可以與目標bean的name屬性中的值之一相同。下面例子展示怎樣使用ref元素。

    <bean id="userService" class="com.liyong.ioccontainer.service.UserService"> 
        <!--屬性注入 保存一種方式就可以-->
        <property name="bookService">
            <ref bean="bookService"/>
        </property>
    </bean>

通過parent屬性指定目標bean的引用,這個bean在當前容器的父容器中。parent屬性的值可以與目標Beanid屬性或目標Beanname屬性中的值之一相同(id或者name指定引用)。這個目標bean必須在父容器中。當你有一個分層的容器並且你想去包裝一個在父容器中存在的bean為代理對象同時有一個相同的名字作為這個父bean,你應該主要的使用這個bean應用的變體。下面的兩個例子展示類怎樣使用parent屬性。

 <!--在父容器上下文-->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- 在子容器上下文 -->
 <!-- 產生一個代理bean,bean name is the same as the parent bean -->
<bean id="accountService"
    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 here -->
</bean>
  • 內部bean

或元素在內定義內部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定義 -->
        <bean class="com.example.Person"> 
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

一個內部bean定義不需要定義一個id或名稱。如果指定名稱,這個容器不會使用這個值作為標識(不會使用定義的作為idname標識)。容器在創建內部bean或忽略Scope(作用域),因為內部bean總是匿名的並且總是依賴外部bean的創建。不可能獨立訪問內部bean或將它們注入到協作bean中而是封裝在bean中。一個極端的情況,可能接受定製的Scope的銷燬回調-例如:一個請求域內部bean包含在一個單例bean中。內部bean實例的創建與其包含的bean綁定在一起,但是銷燬回調使它可以參與請求範圍的生命週期。這不是常見的情況。內部bean通常只共享其包含bean的作用域。

參考代碼:com.liyong.ioccontainer.starter.XmlOutterInnerBeanIocContainer

  • 集合

、、和元素分別設置Java集合類型List、Set、Map和Properties的屬性和參數。下面例子展示怎樣使用:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- adminEmails屬性為Properties類型。 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>
    <!--someList屬性為java.util.List類型。 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>
    <!--someMap屬性類型為:java.util.Map。 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>
    <!--someSet屬性類型為:java.util.Set。 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的值、值、或者set的值,可以是任何下面元素:

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

參考代碼:com.liyong.ioccontainer.starter.XmlCollectionsIocContainer

  • 集合合併

Spring容器也支持對集合合併。應用程序開發人員可以定義、、或元素有一個子元素集合、、或繼承和覆蓋父集合元素。因此,子集合的值是父集合和子集合合併元素後的結果,也就是子集合元素會覆蓋父集合的元素值。

在合併章節討論父-子bean的機制。不熟悉父bean和子bean定義的讀者可能希望先閱讀相關部分,然後再繼續。

下面的例子展示集合的合併:

<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的元素的屬性是merge=true。當這個child bean被容器解析和初始化的時候,這個結果實例有adminEmails Properties集合,這個集合包含了子集合和父集合的adminEmails合併集。結果列表:

[email protected]
[email protected]
[email protected]

為了支持在父Properties值被覆蓋,子Properties集合的值從父中繼承所有的值和子Properties的值(備註:意思是子Properties會覆蓋父Properties中重複的值)。

這個合併行為適用類似、、集合類型 。在元素的特定情況下,將維護與List集合類型關聯的語義(即,值有序集合的概念)。父元素的值優先與所有的子元素值。在MapSetProperties集合類型中不存在順序。因此,對於容器內部使用的關聯MapSetProperties實現類型下的集合類型,沒有有效的排序語義。

  • 集合合併限制

我們不能合併不同集合類型(例如:Map和List)。如果嘗試去合併將會拋出一個Exception異常。這個merge屬性必須被指定在子類中。在父集合定義中指定merge屬性是多餘的並且不會達到預期結果。

  • 強類型集合

在Java 5中泛型被引入,我們可以使用強類型集合。因此,僅僅包含String元素的集合聲明成為可能。如果我們使用Spring去依賴注入一個強類型的Collection到一個bean中,可以利用Spring的類型轉換在添加到Collection集合前對集合實例元素轉換為適合的類型。下面的java代碼和bean定義展示了怎樣去使用:

public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
<beans>
    <bean id="something" class="x.y.SomeClass">
        <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>

當準備註入something beanaccounts屬性時,可以通過反射獲得有關強類型Map <String,Float>的元素類型的泛型信息。因此,Spring的類型轉換基礎設施識別各種元素Float類型的值並且這些字符串值能夠被轉換為真實的Float類型。

  • Null和空字符串值

Spring將屬性等的空參數視為空字符串。下面基於XML的配置元數據片段設置了email屬性為空字符串

<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>

和下面java代碼相等:

exampleBean.setEmail("");

元素被處理為null值,下面展示例子:

<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>

和下面java代碼相等:

exampleBean.setEmail(null);
  • p-namespace快捷方式

p-namespace讓你使用bean元素的屬性(嵌入元素替換)去描述你的屬性值協同者bean,或者兩種都使用。

Spring支持可擴展的namespace配置格式,基於XML Schema定義。本章討論的bean配置格式在XML Schema文檔中定義。然而,p-namespaceXSD文件中沒有被定義僅僅在Spring Core中存在。

下面例子展示兩個XML片段其解析結果是一致的.

<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
        https://www.springframework.org/schema/beans/spring-beans.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="[email protected]"/>
</beans>

該示例顯示了p-namespace中的一個屬性,該屬性在bean定義中稱為email。這告訴Spring包含一個屬性的聲明。前面提到,p-namespace沒有schema定義,因此你可以設置屬性名稱為property(類字段)名稱。

下一個示例包括另外兩個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
        https://www.springframework.org/schema/beans/spring-beans.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>

這個例子包含了不僅屬性值使用p-namespace而且還使用指定格式去聲明屬性引用。第一個定義使用去創建一個從bean john到bean jane的引用,第二個bean定義使用p:spouse-ref="jane"作為一個屬性去做相同的事情。在這個例子中,spouse是屬性名稱,ref表示不是一個直接值而是一個引用值。

p-namespace不像標準的XML格式靈活。例如,聲明屬性引用的格式與以Ref結尾的屬性衝突,而標準XML格式則不會。我們推薦你選擇你的方式小心地並和你的團隊交流去避免在用一時間同時使用XML三種方式。

參考代碼:com.liyong.ioccontainer.starter.XmlPNameSpaceIocContainer

  • c-namespace快捷方式

類似p-namespace的快捷方式,在Spring3.1引入,c-namespace允許配置構造函數參數內聯屬性而不是嵌入constructor-arg元素。

下面的例子使用c:命名空間去做相同的基於構造函數依賴注入事情:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

    <!-- 傳統聲明可選參數 -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="[email protected]"/>
    </bean>

    <!-- c-namespace聲明參數名稱 -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="[email protected]"/>

</beans>

c:命名空使用類似於p:相同的約束(bean引用為跟隨-ref)去通過它們的名字設置構造參數。類似地,即使它沒有在XSD schema中定義,也需要在XML文件中去聲明,(存在Spring Core中)。

一個少見的場景,構造函數參數名字不能使用(通常如果編譯字節碼時沒有調試信息)可以使用回退參數索引,如下:

  <!-- c-namespace index declaration -->
  <bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
   c:_2="[email protected]"/>

由於XML語法,要求這個索引符號_必須存在,因為XML屬性名稱不能以數字開頭(即使IDE工具允許也是不行的)。對應索引的符號在元素也是有效的但是不常用,因為這個聲明順序通常已經足夠。

在實踐中,構造函數解析機制在匹配參數是非常高效的,因此,除非你真的需要,我們推薦整個配置都使用名字符號。

參考代碼:com.liyong.ioccontainer.starter.XmlCNameSpaceIocContainer

  • 複合屬性名

當設置bean屬性的時候,我們可以使用複合或嵌入屬性名,只要這個path(點式引用)下面所有組件期望這個最終屬性名不為null。考慮下面的bean定義:

<bean id="something" class="things.ThingOne">
    <property name="fred.bob.sammy" value="123" />
</bean>

這個something bean有一個fred屬性,fred有一個bob屬性,bob擁有一個sammy屬性並且最終sammy屬性被設置值為123。為了能正常工作,something的屬性fredfred的屬性bob在這個bean構造之前必須不能為null。否則,會拋出一個NullPointerException

1.4.3 使用depends-on

如果bean依賴其他bean,也就是意味著bean需要設置依賴的bean屬性。典型地,我們可以基於XML配置元數據使用ref去完成。然而,一些bean之間的依賴不是直接的。一個例子是在類中一個靜態的初始化器需要被觸發,例如:數據庫驅動註冊。depends-on屬性能夠顯示地強制一個或多個bean在依賴bean初始化之前初始化。下面的例子使用depends-on屬性去表達對一個簡單bean的依賴。

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

為了表達多個bean的依賴,提供一個bean名稱的集合列表作為depends-on屬性值(,;空格是有效的分隔符)。

<!--beanOne依賴manager,accountDao-->
<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" />

僅僅在單例bean場景下,depends-on屬性能夠指定初始化時間依賴和對應的銷燬時間依賴。與給定bean定義依賴關係的從屬bean首先被銷燬,然後再銷燬給定bean本身(備註:被依賴的bean先銷燬,在銷燬宿主bean)。因此,depends-on可以控制關閉順序。

參考代碼:com.liyong.ioccontainer.starter.XmlDependOnSpaceIocContainer

1.4.4 bean的懶加載

默認情況下,ApplicationContext實現更早的創建和配置所有的單例bean作為初始化過程的一部分。通常地,這個前置初始化是可取的,因為錯誤的配置或環境變量被立即的發現,而不是幾個小時甚至幾天後才被發現。當這個行為不是可取的時候,我們可以通過標記bean作為一個懶加載的單例bean去阻止提前初始化。一個懶加載bean告訴容器當第一次請求的時候去創建實例而不是在容器啟動時候。

在XML配置中,這個行為通過在元素的屬性lazy-init控制的。下面例子展示:

<!--設置bean延遲初始化 注意:Spring中的bean默認是單例的-->
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

當前面的配置通過ApplicationContext加載並啟動時,這個lazy bean沒有被提前的初始化,而not.lazy bean被儘早的初始化。

然而,當一個懶加載bean是另一個單例bean的依賴時候,這個懶加載不是懶加載的。ApplicationContext在啟動時創建這個懶加載bean,因為它必須滿足這個單例bean的依賴。這個懶加載bean被注入到一個單例bean所以它不是懶加載的。

我們也可以在容器級別通過使用元素的default-lazy-init屬性控制懶加載,下面例子展示怎樣使用:

<beans default-lazy-init="true">
    <!-- no beans will be pre-instantiated... -->
</beans>

參考代碼:com.liyong.ioccontainer.starter.XmlLazyInitilaizedIocContainer

1.4.5自動裝配協調者

Spring容器能自動裝配協調者bean之間的關係。通過檢查ApplicationContext的內容,Spring自動為你的bean解析協同者(其他依賴bean)。自動裝配有下面的優勢:

  • 自動裝配能顯著的降低對屬性和構造函數參數的需要。(其他機制例如:在這方面,其他機制(例如本章其他地方討論的bean模板)也很有價值)。
  • 自動裝配可以隨著對象的演化而更新配置。例如:如果你需要添加一個類的依賴,依賴能夠被自動地被滿足不需要修改配置。因此,自動裝配在開發過程中特別有用,當代碼庫變得更加穩定時,自動裝配可以避免切換到顯式連接的選項。

當使用基於XML元數據配置,我們可以為這個bean指定自動裝配模式通過元素的autowire屬性。自動裝配有4種模式。你可以對每個bean指定自動裝配因此可以選擇自動裝配哪些bean。下面的表格描述了4種裝配模式:

Mode Explanation
no 默認不自動裝配. bean引用必須通過ref定義. 對於較大的部署,建議不要更改默認設置,因為明確指定協同者可以提供更好的控制和清晰度。 在某種程度上,它記錄了系統的結構。
byName 通過屬性名自動裝配。Spring查找一個bean與自動裝配屬性名相同的bean名字。例如:如果bean定義是被設置為通過名字自動注入並且包含了一個master屬性(也就是,有一個setMaster(..)方法),Spring查找一個master名字的bean並且使用它設置到屬性上。
byType 如果在容器中和屬性具有相同類型的唯一bean存在會被自動注入到屬性。如果有多個bean存在,一個致命的異常被拋出,表示你不能使用byType為bean自動裝配。如果沒有匹配的bean,不發生任何事情(屬性不被設置)。
constructor 類似於byType,但是使用構造函數參數。如果在容器中沒有一個bean被匹配到會拋出一個致命的error

使用byType或構造函數自動裝配模式,你可以結合數組和類型化的集合,在這種情況下,提供容器中與期望類型匹配的所有自動裝配候選,以滿足相關性。如果期望key的 類型是String,你可以自動裝配強類型的Map實例。一個自動裝配Map實例的值由所有匹配期望類型實例組成,並且這個Map實例的這些keybean名稱對應。

  • 自動裝配的優勢和限制

在一個系統中一致地使用自動裝配將工作的更好。如果通常不使用自動裝配,則可能使開發人員僅使用自動裝配來連接一個或兩個bean定義而感到困惑。

考慮自動裝配的限制和優勢:

  • propertyconstructor-arg中顯示依賴設置總是覆蓋自動裝配。你不能自動裝配簡單的屬性例如:原生類型,StringClass(簡單屬性數組)。這種限制是由設計造成的。
  • 自動裝配沒有顯示裝配精確。儘管如前面的表中所述,Spring小心地避免猜測,以免產生可能產生意外結果的歧義。Spring管理對象之間的關係不再明確記錄。
  • 裝配信息可能對從Spring容器生成文檔的工具不可用。
  • 容器中的多個bean定義可能與要自動裝配的setter方法或構造函數參數指定的類型相匹配。對於數組、集合或Map實例,這不一定是問題。然而,為了依賴期望一個簡單值,這種歧義不會被任意解決(意思是期望一個bean容器中確有多個匹配的bean)。如果沒有唯一的有效bean的定義會拋出一個異常。

在最後的場景中,你有一些可選項:

  • 顯示的裝配放棄自動裝配。
  • 通過設置bean定義的autowire-candidate屬性為false去避免自動裝配,在下一個章節描述。
  • 通過設置元素屬性primarytrue指定一個bean定義作為主要的候選者。
  • 實現更細粒度的有效控制通過基於註解的配置,在基於註解容器配置中描述。
  • 自動裝配排除bean

在每個bean的基礎上,你可以從自動裝配中排除一個bean。在Spring的XML格式中,設置元素的autowire-candidate屬性為false。容器使該特定的bean定義不適用於自動裝配基礎結構(包括註釋樣式配置,例如@Autowired)。

autowire-candidate屬性被設計僅僅通過基於類型自動裝配有影響。它不會影響通過名字來顯示引用的方式,即使這個指定bean沒有被標記作為一個自動裝配候選者這個名字也會被解析。因此,如果名稱匹配,按名稱自動裝配仍然會注入Bean。

可以基於與Bean名稱的模式匹配來限制自動裝配候選者。頂層元素接受一個或多個表達式在default-autowire-candidates 屬性中。例如,去限制自動裝配候選者任意狀態的bean,它的名字以Repository結尾,提供一個值為*Repository的表達式。提供多個表達式可通過;號分割。為一個bean定義的autowire-candidate屬性顯示指定truefalse總是優先級最高(比如default-autowire-candidates優先級高),指定的規則被覆蓋。

當這些bean不想自動裝配注入到其他bean中時,這些技術是非常有用的。這並不意味著一個被排除的bean本身不能通過使用自動裝配來配置。而是,bean本身不是一個候選者不會被注入到其他bean中。

參考代碼:com.liyong.ioccontainer.service.AutowireCandidateService

1.4.6 方法注入

在大多數應用場景中,在容器中大多數bean是單例的。當單例Bean需要與另一個單例Bean協作或非單例Bean需要與另一個非單例Bean協作時,典型的處理依賴通過定義一個bean作為其他bean的屬性。當bean的生命週期不同時會出現問題。假設單例bean A需要使用非單例bean B(原型),假設A的每個方法被調用。這個容器僅僅創建單例bean A一次並且只有一次機會去設置屬性。容器無法每次為bean A提供一個新的bean B實例(單例A每次從容器獲取bean B不能每次提供一個新bean)。

一個解決方案時放棄控制反轉。我們也可以通過實現ApplicationContextAware接口讓bean A意識到容器。並在bean A每次需要bean B時,通過使用getBean("B")獲取一個新實例bean。下面例子展示使用方式:

package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

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

    protected Command createCommand() {
        // 通過從容器獲取bean
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(
            ApplicationContext applicationContext) throws BeansException {
      //容器注入ApplicationContext
        this.applicationContext = applicationContext;
    }
}

上面的例子不是可取的,因為這個業務代碼需要容器回調耦合了Spring 框架(備註:這個我不敢苟同,上面我也發表了觀點)。方法注入,Spring IoC容器高級特性,讓我們處理這種場景更簡潔。(備註:上面Command配置為原型才能達到效果)

可以閱讀更多關於方法注入的動機在 博客入口

  • 查找方法注入

查找方法注入是容器覆蓋容器管理bean上的方法並返回容器中另一個命名bean的查找結果的能力。在前面描述的場景中,典型地查找涉及到原型bean。Spring框架通過CGCLB庫去動態地生成一個子類去覆蓋這些方法以實現方法注入。

  • 為了動態子類能夠正常工作,Spring bean不能是final並且方法也不能是final
  • 單元測試一個具有抽象方法的類需要你自己去子類化並且提供一個抽象方法的存根實現。
  • 組件掃描也需要具體方法,這需要具體的類別。
  • 進一步關鍵限制是方法查找不能在工廠方法並且特別在configuration類被@Bean標註的方法,因為,在這種情況,容器不負責創建實例,因此無法即時創建運行時生成的子類(因為這種方法Bean是由我們自己創建處理的容器不能控制bean的生成)。

在前面的代碼片段CommandManager中,Spring容器動態的覆蓋這個createCommand方法的實現。CommandManager類沒有任何的Spring的依賴,重構如下:

package fiona.apple;

// 沒有Spring的依賴!
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);

如果這個方法是abstract,動態地生成子類實現這個方法。除此之外,動態生成子類覆蓋在源類中具體方法定義。考慮下面例子:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" 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="myCommand"/>
</bean>

每當需要新的myCommand bean實例時,標識為commandManager的bean就會調用其自己的createCommand()方法。你必須非常的小心應用myCommand bean作為一個原型,如果這是真實需要的。如果這個bean是單例的,myCommand實例每次都返回同一個bean

或者,在基於註解組件模式中,你可以聲明一個查找方法通過@Lookup註解,像下面例子:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    //指定名稱查找,在容器中查找
    @Lookup("myCommand")
    protected abstract Command createCommand();
}

或者,更習慣地說,你可以依靠針對查找方法的聲明返回類型來解析目標Bean

public abstract class CommandManager {

    public Object process(Object commandState) {
        MyCommand command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    //沒有指定名稱查找,通過查找方法的MyCommand類型去容器查找
    @Lookup
    protected abstract MyCommand createCommand();
}

注意,通常應該使用具體的存根實現聲明此類帶註解的查找方法,為了使它們與Spring的組件掃描規則兼容,默認情況下,抽象類將被忽略。此限制不適用於顯式註冊或顯式導入的Bean類。

獲取不同範圍的目標bean的其他方式是ObjectFactory/Provider注入點。查看 Scoped Beans as Dependencies.

你也可以找到ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包)去使用。

參考代碼:

com.liyong.ioccontainer.starter.XmlLookUpInjectionIocContainer

com.liyong.ioccontainer.starter.XmlLookUpInjectionByAnnotaionIocContainer

  • 任意方法替換

與查找方法注入相比,方法注入的一種不太有用的形式是能夠用另一種方式實現替換託管bean中的任意方法。你可以放心地跳過本節的其餘部分,直到你真正需要此功能為止。

基於XML元素數據配置,你可以使用replaced-method元素將現有的方法實現替換為已部署的Bean。考慮下面的類,這個類有一個叫做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">
    <!-- 替換方法 -->
    <replaced-method name="computeValue" replacer="replacementComputeValue">
        <arg-type>String</arg-type>
    </replaced-method>
</bean>

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

你可以使用一個或多個在元素中去指示這個被覆蓋的方法簽名。僅當方法重載並且類中存在多個變體時,才需要對參數進行簽名。

為了方便起見,參數的類型字符串可以是完全限定類型名稱的子字符串。例如:java.lang.String

java.lang.String
String
Str

因為參數的數量通常足以區分每個可能的選擇,所以通過讓你僅輸入與參類型匹配的最短字符串,此快捷方式可以節省很多輸入。

參考代碼:com.liyong.ioccontainer.starter.XmlMethodReplaceIocContainer

作者

個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公眾號:

技術交流群:

Leave a Reply

Your email address will not be published. Required fields are marked *