1. 程式人生 > >Spring的bean創建詳解

Spring的bean創建詳解

nsh tex void 零售商 nco 強制 ram provider 如果

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

在IoC容器中,bean的獲取主要通過BeanFactoryApplicationContext獲取,這裏ApplicationContext實際上是繼承自BeanFactory的,兩者的區別在於BeanFactory對bean的初始化主要是延遲初始化的方式,而ApplicationContext

對bean的初始化是在容器啟動時即將所有bean初始化完畢。如下是BeanFactory的主要接口:

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有兩個主要的實現類RootBeanDefinition

ChildBeanDefinition,這裏RootBeanDefinition主要用於創建並且註冊一個bean到BeanDefinitionRegistry中,ChildBeanDefinition則主要用於預處理具有parent/child的bean定義。如下圖為IoC容器管理bean的主要類結構圖,這裏DefaultListableBeanFactoryBeanFactoryBeanDefinitionRegistry的一個默認實現類:

技術分享圖片

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

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

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三個類都成功創建了。

2. 元數據

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

@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進行配置。

Spring的bean創建詳解