Spring的bean建立詳解

       IoC容器,又名控制反轉,全稱爲Inverse of Control,其是Spring最爲核心的一個組件,其餘的組件如AOP,Spring事務等都是直接或間接的依賴於IoC容器的。本文主要講解IoC容器所管理的bean的幾種建立方式,而且詳細講解了xml配置中相關參數的配置。java

       在IoC容器中,bean的獲取主要經過BeanFactoryApplicationContext獲取,這裏ApplicationContext其實是繼承自BeanFactory的,二者的區別在於BeanFactory對bean的初始化主要是延遲初始化的方式,而ApplicationContext對bean的初始化是在容器啓動時即將全部bean初始化完畢。以下是BeanFactory的主要接口:spring

Object getBean(String name) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
boolean containsBean(String name);
String[] getAliases(String name);
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

       能夠看到,BeanFactory中主要提供的是一些查詢bean的方法,而bean的建立和管理其實是由BeanDefinitionRegistry來進行的。BeanDefinitionRegistry會爲其管理的每一個bean都建立一個BeanDefinition實例,該實例中主要包含當前bean的名稱,類型,是否抽象類,構造函數參數等信息。BeanDefinition有兩個主要的實現類RootBeanDefinitionChildBeanDefinition,這裏RootBeanDefinition主要用於建立而且註冊一個bean到BeanDefinitionRegistry中,ChildBeanDefinition則主要用於預處理具備parent/child的bean定義。以下圖爲IoC容器管理bean的主要類結構圖,這裏DefaultListableBeanFactoryBeanFactoryBeanDefinitionRegistry的一個默認實現類:數據庫

       IoC容器建立bean主要有三種方式:硬編碼,元數據和配置文件。這裏硬編碼方式也即顯示的使用上面的類圖關係將bean以及它們之間的依賴關係註冊到IoC容器中;元數據方式即便用Java註解和spring自動掃描的功能配置bean;配置文件的方式主要有兩種:xml和properties文件,這裏主要講解使用更普遍的xml文件的方式。app

       這了以零售超市的例子來說解bean的建立,SuperMarket表示零售超市,其有DrinkProvider合FruitProvider兩個供應商,而且這兩個供應商分別有兩個實現類Milk和Apple。以下是各個類的結構:ide

public class SuperMarket {
  private DrinkProvider drink;
  private FruitProvider fruit;
  
  public SuperMarket() {}

  public SuperMarket(DrinkProvider drink, FruitProvider fruit) {
    this.drink = drink;
    this.fruit = fruit;
  }

  public void setDrink(DrinkProvider drink) {
    this.drink = drink;
  }

  public void setFruit(FruitProvider fruit) {
    this.fruit = fruit;
  }
  
  @Override
  public String toString() {
    return "drink: " + drink + ", fruit: " + fruit;
  }
}
public interface DrinkProvider {}
public class Milk implements DrinkProvider {
  @Override
  public String toString() {
    return "this is milk";
  }
}
public interface FruitProvider {}
public class Apple implements FruitProvider {
  @Override
  public String toString() {
    return "this is an apple";
  }
}

1. 硬編碼

       根據上面對IoC容器對bean進行管理的幾個類的講解,這裏硬編碼的方式實際上很好實現,以下是bean建立的代碼:函數

public class BeanApp {
  public static void main(String[] args) {
    DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
    BeanFactory beanFactory = bindViaCode(beanRegistry);
    SuperMarket superMarket = beanFactory.getBean(SuperMarket.class);
    System.out.println(superMarket);
  }

  private static BeanFactory bindViaCode(BeanDefinitionRegistry beanRegistry) {
    AbstractBeanDefinition fruit = new RootBeanDefinition(Apple.class);
    AbstractBeanDefinition drink = new RootBeanDefinition(Milk.class);
    AbstractBeanDefinition superMarket = new RootBeanDefinition(SuperMarket.class);

    beanRegistry.registerBeanDefinition("fruit", fruit);
    beanRegistry.registerBeanDefinition("drink", drink);
    beanRegistry.registerBeanDefinition("superMarket", superMarket);

    // 使用構造方法對屬性進行設值
    ConstructorArgumentValues argumentValues = new ConstructorArgumentValues();
    argumentValues.addIndexedArgumentValue(0, drink);
    argumentValues.addIndexedArgumentValue(1, fruit);
    superMarket.setConstructorArgumentValues(argumentValues);

    // 使用setter方法對屬性進行設值
    MutablePropertyValues propertyValues = new MutablePropertyValues();
    propertyValues.addPropertyValue("fruit", fruit);
    propertyValues.addPropertyValue("drink", drink);
    superMarket.setPropertyValues(propertyValues);
    
    return (BeanFactory) beanRegistry;
  }
}

以下是輸出結果:測試

drink: this is milk. , fruit: this is an apple.

       在示例中,咱們首先聲明瞭一個DefaultListableBeanFactory實例,須要注意,DefaultListableBeanFactory既實現了BeanFactory接口,也實現了BeanDefinitionRegistry接口,於是這裏將該實例傳入bindViaCode()方法做爲bean註冊器使用。在bindViaCode()方法中,咱們首先爲每一個須要建立的bean建立一個BeanDefinition對其進行管理,而後將每一個BeanDefinition註冊到BeanDefinitionRegistry中。註冊完以後,咱們使用ConstructorArgumentValues類來指定建立的三個bean之間的相互依賴關係(這裏咱們也提供了使用setter方法對屬性進行設值的代碼)。從最後的輸出咱們能夠看出,SuperMarket,Milk和Apple三個類都成功建立了。ui

2. 元數據

       元數據的方式也即註解方式,Spring IoC主要提供了兩個註解用於bean的建立和屬性的注入,即@Component@Autowired。這裏@Component用在類聲明上,用於告知Spring,其須要爲當前類建立一個實例,實例名爲當前類名首字母小寫的形式。@Autowired則用在屬性上,Spring檢測到該註解以後就會在IoC容器中查找是否有與該屬性相匹配的類或子類實例,有的話就注入到當前屬性中,不然就會報錯。以下是使用元數據方式建立的bean的示例,示例的類結構中部分代碼與前述類結構一致,這裏對其進行了省略:this

@Component
public class SuperMarket {
  @Autowired
  private DrinkProvider drink;
  
  @Autowired
  private FruitProvider fruit;
  
  // getter和setter,以及toString()等方法
}
@Component
public class Milk implements DrinkProvider {
}
@Component
public class Apple implements FruitProvider {
}

       能夠看到,這裏建立了分別建立了Milk,Apple和SuperMarket的實例,而且將Milk和Apple實例經過@Autowired注入到SuperMarket實例中了。這裏須要注意的是,對於IoC容器而言,單純使用了上述註解還不能讓其自動建立這些bean,還須要經過配置文件用來指明須要對哪些包下的類進行掃描,以檢測相關的註解,並註冊相應的實例。以下是xml文件的配置方式:編碼

<context:component-scan base-package="com.market"/>

以下是測試驅動類的代碼:

public class BeanApp {
  public static void main(String[] args) {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("com/market/application.xml");
    SuperMarket superMarket = beanFactory.getBean(SuperMarket.class);
    System.out.println(superMarket);
  }
}

結果輸出以下:

drink: this is milk, fruit: this is an apple

3. 配置文件

       xml配置文件是bean實例化使用最爲普遍的一種方式,其主要包括兩種形式的bean建立:構造方法和屬性注入。這裏咱們會對着兩種方式進行詳細講解,而且還會講解如何注入List,Set,Map等類型屬性值的方式,另外,咱們也會講解具備初始化順序的bean的初始化和具備父子類關係的bean的初始化等方式。

1. 構造方法注入

       構造方法注入主要使用constructor-arg標籤,具體使用方式有如下幾種類型

  • 引用類型
<bean id="drink" class="com.market.Milk"/>
<bean id="fruit" class="com.market.Apple"/>
<bean id="superMarket" class="com.market.SuperMarket">
  <constructor-arg ref="drink"/>
  <constructor-arg ref="fruit"/>
</bean>

       這裏首先建立Milk和Apple類的對象,而後在建立SuperMarket對象時,向其構造函數傳入了先前建立的Milk和Apple對象。這裏ref節點用於表示當前參數是引用的其餘的bean。

  • 數值類型
public class SequenceFile { 
  private int dependency1;
  private String dependency2;

  public SequenceFile(int dependency1) {
    this.dependency1 = dependency1;
  }
}
<bean id="sequenceFile" class="com.market.SequenceFile">
  <constructor-arg value="123"/>
</bean>

       這裏使用constructor-arg的value節點來爲只有一個參數的構造函數指定值。因爲SequenceFile只有一個構造函數,於是這裏IoC容器知道應該使用該構造函數,而且會進行強制類型轉換以使參數值符合參數類型。

  • 指定參數類型
public class SequenceFile {
  private int dependency1;
  private String dependency2;

  public SequenceFile(String dependency2) {
    this.dependency2 = dependency2;
  }
  
  public SequenceFile(int dependency1) {
    this.dependency1 = dependency1;
  }
}
<bean id="sequenceFile" class="com.market.SequenceFile">
  <constructor-arg value="123" type="int"/>
</bean>

       這裏有兩個只有一個參數的構造函數,此時若是配置文件仍是按照上一示例中的配置,那麼IoC容器是不知道應該使用哪一個構造函數的,於是其會默認使用第一個構造函數,也就是dependency2會被注入123。這裏若是使用type節點指定了參數類型爲int,那麼IoC容器就會找只有一個參數,而且參數類型爲int類型的構造函數進行bean的實例化,這裏也就是dependency1會被初始化爲123。

  • 指定參數順序
public class SequenceFile {
  private int dependency1;
  private String dependency2;

  public SequenceFile(int dependency1, String dependency2) {
    this.dependency1 = dependency1;
    this.dependency2 = dependency2;
  }
}
<bean id="sequenceFile" class="com.market.SequenceFile">
  <constructor-arg value="abc" index="1"/>
  <constructor-arg value="123" index="0"/>
</bean>

       這裏SequenceFile有一個包含兩個參數的構造函數,在聲明bean指定參數的時候,若是不指定當前注入的參數對應於構造函數的第幾個參數,那麼IoC容器就會按照聲明的順序爲構造函數的參數注值,這每每是有問題的。示例中咱們使用index節點爲當前的參數值指定了對應的構造函數的參數位,注意構造函數的參數索引是從0開始的。

2. 屬性注入

       屬性注入也就是使用setter方法注入,注入的參數名與setter方法後綴部分是一致的,而與實際參數名無關。setter方法注入在類的聲明上主要有兩個地方須要注意:①若是配置文件沒有顯示使用顯示的聲明構造函數,那麼類中必定要聲明默認的構造函數;②類中必定要包含有要注入屬性的setter方法。以下是一個setter方法進行數值注入的示例:

public class SequenceFile {
  private int dependency;

  public void setDependency(int dependency) {
    this.dependency = dependency;
  }
}
<bean id="sequenceFile" class="com.market.SequenceFile">
  <property name="dependency" value="123"/>
</bean>

setter方法也能夠進行引用注入,以下所示:

<bean id="fruit" class="com.market.Apple"/>
<bean id="drink" class="com.market.Milk"/>
<bean id="superMarket" class="com.market.SuperMarket">
  <property name="drink" ref="drink"/>
  <property name="fruit" ref="fruit"/>
</bean>

       這裏屬性注入的使用方式和構造函數中參數的注入方式在配置文件的配置上基本是一致的,這裏就再也不贅述其具體的使用。

3. List,Set,Map和Properties

       對於集合參數的注入,不管是構造函數仍是屬性注入,其使用方式是一致的,只須要在相應的參數聲明節點下使用集合標籤便可。這裏集合類型與標籤對應方式以下:

集合類型 xml標籤
List
Set
Map
Properties

以下是一個聲明集合參數的示例:

public class MockDemoObject {
  private List<Integer> param1;
  private String[] param2;
  private Set<String> param3;
  private Map<Integer, String> param4;
  private Properties properties;
  private Object param5;

  public void setParam1(List<Integer> param1) {
    this.param1 = param1;
  }

  public void setParam2(String[] param2) {
    this.param2 = param2;
  }

  public void setParam3(Set<String> param3) {
    this.param3 = param3;
  }

  public void setParam4(Map<Integer, String> param4) {
    this.param4 = param4;
  }

  public void setProperties(Properties properties) {
    this.properties = properties;
  }

  public void setParam5(Object param5) {
    this.param5 = param5;
  }
}
<bean id="mdBean" class="com.market.MockDemoObject">
  <property name="param1">
    <list>
      <value>1</value>
      <value>2</value>
      <value>3</value>
    </list>
  </property>

  <property name="param2">
    <list>
      <value>string1</value>
      <value>string2</value>
      <value>string3</value>
    </list>
  </property>

  <property name="param3">
    <set>
      <value>abc</value>
      <value>def</value>
      <value>hij</value>
    </set>
  </property>

  <property name="param4">
    <map>
      <entry key="1" value="string1"/>
      <entry key="2" value="string2"/>
      <entry key="3" value="string3"/>
    </map>
  </property>

  <property name="properties">
    <props>
      <prop key="author">zhangxufeng</prop>
      <prop key="age">26</prop>
    </props>
  </property>

  <property name="param5">
    <null/>
  </property>
</bean>

       這裏須要說明的是,若是集合的元素是引用類型,那麼只須要在對應的元素聲明處使用ref節點指向另外聲明的bean。

4. depends-on依賴

       這裏depends-on依賴指的是在某些bean進行實例化時,必須保證另一個bean已經實例化完成,而且這兩個bean不必定具備屬性依賴關係。depends-on實際使用狀況好比進行dao的bean實例化時,須要先將管理數據庫鏈接池的bean進行初始化。以下是一個depends-on依賴的示例:

public class ServiceInstance {}
public class SystemConfigurationSetup {
  static {
    System.out.println("static initialization! ");
  }
}
public class SystemConfigurationSetup2 {
  static {
    System.out.println("static initialization 2 ! ");
  }
}

配置文件:

<bean id="scSetup1" class="com.market.SystemConfigurationSetup"/>
<bean id="scSetup2" class="com.market.SystemConfigurationSetup2"/>
<bean id="serviceInstance" class="com.market.ServiceInstance" depends-on="scSetup1,scSetup2"/>

       能夠看到,這裏在ServiceInstance的bean標籤中使用的depends-on,具備多個依賴的使用逗號隔開,IoC容器在進行該bean的初始化以前會保證scSetup1和scSetup2都初始化完畢。

5. autowire自動注入

       autowire自動注入指的是在聲明一個bean的時候不顯示的爲其聲明構造函數或者是屬性名的參數,而是使用autowire節點,讓IoC容器經過構造函數和屬性名自動識別當前bean所依賴的bean,從而注入進來。autowire有兩個值可選byType和byName,分別表示根據構造函數參數和屬性的類型進行自動注入,或者是根據屬性名進行自動注入。以下所示爲autowire注入的一個示例:

public class Foo {
  private Bar emphasisAttribute;

  public void setEmphasisAttribute(Bar emphasisAttribute) {
    this.emphasisAttribute = emphasisAttribute;
  }
}
public class Bar {}
<bean id="fooBean" class="com.market.Foo" autowire="byName"/>
<bean id="emphasisAttribute" class="com.market.Bar"/>

       示例中,Foo實例依賴於Bar實例,在配置文件中建立Foo實例的處並無指定其屬性值,而是使用了autowire="byName",而Bar實例的名稱則和Foo的setter方法後的名稱一致。這裏也可使用byType類型的自動注入,此時Bar實例的名稱則能夠爲任意名稱:

<bean id="fooBean" class="com.market.Foo" autowire="byType"/>
<bean id="anyName" class="com.market.Bar"/>

6. 繼承

  • bean的類之間具備繼承關係

       對於具備繼承關係的bean,因爲父類的屬性,子類也會有,於是若是直接配置,那麼兩個bean的配置將會有很大一部分趨於類似。這裏可使用parent屬性用來將父類已經注入的bean繼承給子類bean,子類bean能夠只更改其中實現與父類有區別的bean。以下示例中,SpecialSuperMarket繼承自SuperMarket類,而SpecialApple則繼承自Apple。在實例化SpecialSuperMarket實例的時候其和SuperMarket實例有部分相同的屬性,而另外一部分是有區別的。以下是SpecialSuperMarket和SpecialApple的聲明,其他的類與前面的類聲明一致:

public class SpecialSuperMarket extends SuperMarket {}
public class SpecialApple extends Apple {}
<bean id="drink" class="com.market.Milk"/>
<bean id="fruit" class="com.market.Apple"/>
<bean id="superMarket" class="com.market.SuperMarket">
  <property name="fruit" ref="fruit"/>
  <property name="drink" ref="drink"/>
</bean>

<bean id="specFruit" class="com.market.SpecialApple"/>
<bean id="specSuperMarket" parent="superMarket" class="com.market.SpecialSuperMarket">
  <property name="fruit" ref="specFruit"/>
</bean>

       從配置文件能夠看出來,父類bean只須要按照正常方式聲明便可,子類的bean只須要使用parent節點指定其繼承的父類bean,而且指明子類與父類有差別的屬性bean。

  • 提取公共bean並進行繼承

        對於兩個或多個bean,若是其大部分屬性bean都是類似的,只有少部分不一致,那麼就能夠將公共的bean提取出來做爲父bean,而後每一個bean繼承自這個bean,子bean能夠重寫本身與父bean不一致的屬性。這裏須要注意的是,提取出來的父bean並非一個真正的bean,其也沒有對應的Java類對應。

       以下例所示,假設另外有一個零售商店Outlet與SuperMarket同樣,其DrinkProvider也爲Milk,但其FruitProvider不同,是Pear,這裏就能夠將Outlet示例與SuperMarket實例的聲明中的相同部分Milk提取出來,而FruitProvider則各自本身提供(SuperMarket代碼與前面一致,這裏省略):

public class Outlet {
  private DrinkProvider drink;
  private FruitProvider fruit;

  public void setDrink(DrinkProvider drink) {
    this.drink = drink;
  }

  public void setFruit(FruitProvider fruit) {
    this.fruit = fruit;
  }
}
public class Pear implements FruitProvider {}
<bean id="drink" class="chapter2.eg1.Milk"/>
<bean id="superSales" abstract="true">
  <property name="drink" ref="drink"/>
</bean>

<bean id="marketFruit" class="chapter2.eg1.Apple"/>
<bean id="superMarket" parent="superSales" class="chapter2.eg1.SuperMarket">
  <property name="fruit" ref="marketFruit"/>
</bean>

<bean id="outletFruit" parent="superSales" class="chapter2.eg4.Pear"/>
<bean id="outlet" class="chapter2.eg4.Outlet">
  <property name="fruit" ref="outletFruit"/>
</bean>

       從配置文件中能夠看出來,這裏將SuperMarket和Outlet中drink屬性的注入提取出來,從而造成一個父bean,即superSales,而SuperMarket和Outlet的bean只須要繼承父bean,而且注入各自特有的bean便可。這裏須要注意,因爲父bean是沒有對應的class與之對應的,於是其沒有class節點,而且父bean須要設置爲abstract類型的。

4. 結語

        本文首先對IoC容器管理bean的方式進行了講解,而後分別介紹瞭如何使用硬編碼,元數據和配置文件的方式進行bean的配置,而且這裏着重講解了如何使用配置文件對bean進行配置。

相關文章
相關標籤/搜索