Spring的bean创建详解
IoC容器,又名控制反转,全称为Inverse of Control,其是Spring最为核心的一个组件,其他的组件如AOP,Spring事务等都是直接或间接的依赖于IoC容器的。本文主要讲解IoC容器所管理的bean的几种创建方式,并且详细讲解了xml配置中相关参数的配置。
在IoC容器中,bean的获取主要通过BeanFactory
和ApplicationContext
获取,这里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的主要类结构图,这里DefaultListableBeanFactory
是BeanFactory
和BeanDefinitionRegistry
的一个默认实现类:
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进行配置。