Spring揭秘-BeanFactory的XML之旅之(上)

BeanFactory的XML之旅

XML格式的容器信息管理方式是Spring提供的最为强大、支持最为全面的方式。从Spring的参考文档到各Spring相关书籍,都是按照XML的配置进行说明的,这部分内容可以让你充分领略到Spring的IoC容器的魅力,以致于我们也不得不带你初次或者再次踏上Spring的XML之旅。

<beans><bean>

所有使用XML文件进行配置信息加载的Spring IoC容器,包括BeanFactory和ApplicationContext的所有XML相应实现,都使用统一的XML格式。在Spring 2.0版本之前,这种格式由Spring提供的DTD规定,也就是说,所有的Spring容器加载的XML配置文件的头部,都需要以下形式的DOCTYPE声明:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" ➥ 9
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
...
</beans>

从Spring 2.0版本之后, Spring在继续保持向前兼容的前提下,既可以继续使用DTD方式的DOCTYPE进行配置文件格式的限定,又引入了基于XML Schema的文档声明。所以, Spring 2.0之后,同样可以使用如下所展示的基于XSD的文档声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-2.0.xsd http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
</beans>

不过,不管使用哪一种形式的文档声明,实际上限定的元素基本上是相同的。让我们从最顶层的元素开始,看一下这两种文档声明都限定了哪些元素吧!

所有注册到容器的业务对象,在Spring中称之为Bean。所以,每一个对象在XML中的映射也自然而然地对应一个叫做<bean>的元素。既然容器最终可以管理所有的业务对象,那么在XML中把这些叫做的元素组织起来的, 就叫做。多个组成一个很容易理解,不是吗?

<beans>之唯我独尊

<beans>是XML配置文件中最顶层的元素,它下面可以包含0或者1个<description>和多个<bean>以及<import>或者<alias>,如图所示。

<beans>作为所有<bean>的“统帅”,它拥有相应的属性(attribute)对所辖的<bean>进行统一的默认行为设置,包括如下几个。

  • default-lazy-init。 其值可以指定为true或者false,默认值为false。用来标志是否对所有的<bean>进行延迟初始化。
  • default-autowire。 可以取值为no、 byName、 byType、 constructor以及autodetect。默认值为no,如果使用自动绑定的话,用来标志全体bean使用哪一种默认绑定方式。
  • default-dependency-check。 可以取值none、 objects、 simple以及all,默认值为none,即不做依赖检查。
  • default-init-method。 如果所管辖的<bean>按照某种规则,都有同样名称的初始化方法的话,可以在这里统一指定这个初始化方法名,而不用在每一个<bean>上都重复单独指定。
  • default-destroy-method。 与default-init-method相对应,如果所管辖的bean有按照某种规则使用了相同名称的对象销毁方法,可以通过这个属性统一指定。

注:当然,如果你不清楚上面这些默认的属性具体有什么用,那也不必着急。在看完对<bean>的讲解之后,再回头来看,就会明了多了。给出这些信息,是想让你知道,如果在某个场景下需要对大部分<bean>都重复设置某些行为的话,可以回头看一下,利用<beans>是否可以减少这种不必要的工作。

<description><import><alias>

之所以把这几个元素放到一起讲解,是因为通常情况下它们不是必需的。不过,了解一下也没什么不好,不是吗?

  • <description>
    可以通过在配置的文件中指定一些描述性的信息。通常情况下,该元素是省略的。当然,如果愿意, <description>随时可以为我们效劳。
  • <import>
    通常情况下,可以根据模块功能或者层次关系,将配置信息分门别类地放到多个配置文件中。在想加载主要配置文件,并将主要配置文件所依赖的配置文件同时加载时,可以在这个主要的配置文件中通过<import>元素对其所依赖的配置文件进行引用。比如,如果A.xml中的<bean>定义可能依赖B.xml中的某些<bean>定义,那么就可以在A.xml中使用<import>将B.xml引入到A.xml,以类似于<import resource="B.xml"/>的形式。
    但是, 这个功能在我看来价值不大,因为容器实际上可以同时加载多个配置,没有必要非通过一个配置文件来加载所有配置。不过,或许在有些场景中使用这种方式比较方便也说不定。
  • <alias>
    可以通过<alias>为某些<bean>起一些“外号”(别名),通常情况下是为了减少输入。比如,假设有个<bean>,它的名称为dataSourceForMasterDatabase,你可以为其添加一个<alias>,像这样<alias name="dataSourceForMasterDatabase" alias="masterDataSource"/>。以后通过dataSourceForMasterDatabase或者masterDataSource来引用这个<bean>都可以,只要你觉得方便就行。

bean

每个业务对象作为个体,在Spring的XML配置文件中是与<bean>元素一一对应的。窥一斑而知全豹,只要我们了解单个的业务对象是如何配置的,剩下的就可以“依葫芦画瓢”了。所以,我们先从最简单的单一对象配置开始吧!如下代码演示了最基础的对象配置形式:

1
2
<bean id="djNewsListener" class="..impl.DowJonesNewsListener"> 
</bean>
  • id属性
    通常,每个注册到容器的对象都需要一个唯一标志来将其与“同处一室”的“兄弟们”区分开来,就好像我们每一个人都有一个身份证号一样(重号的话就比较麻烦)。通过id属性来指定当前注册对象的beanName是什么。这里,通过id指定beanName为djNewsListener。实际上,并非任何情况下都需要指定每个<bean>的id,有些情况下, id可以省略,比如后面会提到的内部<bean>以及不需要根据beanName明确依赖关系的场合等。
    除了可以使用id来指定<bean>在容器中的标志,还可以使用name属性来指定<bean>的别名(alias)。比如,以上定义,我们还可以像如下代码这样,为其添加别名:
    1
    2
    3
    4
    <bean id="djNewsListener"
    name="/news/djNewsListener,dowJonesNewsListener"
    class="..impl.DowJonesNewsListener">
    </bean>
    与id属性相比,name属性的灵活之处在于,name可以使用id不能使用的一些字符,比如/。而且还可以通过逗号、空格或者冒号分割指定多个name。 name的作用跟使用为id指定多个别名基本相同:
    1
    2
    <alias name="djNewsListener" alias="/news/djNewsListener"/>
    <alias name="djNewsListener" alias="dowJonesNewsListener"/>
  • class属性
    每个注册到容器的对象都需要通过<bean>元素的class属性指定其类型,否则,容器可不知道这个对象到底是何方神圣。在大部分情况下,该属性是必须的。仅在少数情况下不需要指定,如后面将提到的在使用抽象配置模板的情况下。

Help Me, Help You

在大部分情况下,我们都不太可能选择单独“作战”,业务对象也是;各个业务对象之间会相互协作来更好地完成同一使命。这时,各个业务对象之间的相互依赖就是无法避免的。对象之间需要相互协作,在横向上它们存在一定的依赖性。而现在我们就是要看一下,在Spring的IoC容器的XML配置中,应该如何表达这种依赖性。
既然业务对象现在都符合IoC的规则,那么要了解的表达方式其实也很简单,无非就是看一下构造方法注入和setter方法注入通过XML是如何表达的而已。那么,让我们开始吧!

构造方法注入的XML之道

按照Spring的IoC容器配置格式,要通过构造方法注入方式,为当前业务对象注入其所依赖的对象,需要使用<constructor-arg>。正常情况下,如以下代码所示:

1
2
3
4
5
6
7
8
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg>
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg>
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>

对于<ref>元素,稍后会进行详细说明。这里你只需要知道,通过这个元素来指明容器将为djNewsProvider这个<bean>注入通过<ref>所引用的Bean实例。这种方式可能看起来或者编写起来不是很简洁,最新版本的Spring也支持配置简写形式,如以下代码所示:

1
2
3
4
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg ref="djNewsListener"/>
<constructor-arg ref="djNewsPersister"/>
</bean>

简洁多了不是嘛?其实,无非就是表达方式不同而已,实际达到的效果是一样的。有些时候,容器在加载XML配置的时候,因为某些原因,无法明确配置项与对象的构造方法参数列表的一一对应关系,就需要请<constructor-arg>的type或者index属性出马。比如,对象存在多个构造方法,当参数列表数目相同而类型不同的时候,容器无法区分应该使用哪个构造方法来实例化对象,或者构造方法可能同时传入最少两个类型相同的对象。

  • type属性
    假设有一个对象定义如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class MockBusinessObject { 
    private String dependency1;
    private int dependency2;

    public MockBusinessObject(String dependency)
    {
    this.dependency1 = dependency;
    }
    public MockBusinessObject(int dependency)
    {
    this.dependency2 = dependency;
    }
    ...
    @Override
    public String toString() {
    return new ToStringBuilder(this) .append("dependency1", dependency1)
    .append("dependency2", dependency2).toString();
    }
    }
    该类声明了两个构造方法,分别都只是传入一个参数,且参数类型不同。这时,我们可以进行配置,如以下代码所示:
    1
    2
    3
    4
    5
    <bean id="mockBO" class="..MockBusinessObject">
    <constructor-arg>
    <value>111111</value>
    </constructor-arg>
    </bean>
    如果从BeanFactory取得该对象并调用toString()查看的话,我们会发现Spring调用的是第一个构造方法,因为输出是如下内容:
    1
    ..MockBusinessObject@f73c1[dependency1=111111,dependency2=0]
    但是,如果我们想调用的却是第二个传入int类型参数的构造方法,又该如何呢?可以使用type属性,通过指定构造方法的参数类型来解决这一问题,配置内容如下代码所示:
    1
    2
    3
    4
    5
    <bean id="mockBO" class="..MockBusinessObject">
    <constructor-arg type="int">
    <value>111111</value>
    </constructor-arg>
    </bean>
    现在,我们得到了自己想要的对象实例,如下的控制台输出信息印证了这一点:
    1
    ..MockBusinessObject@f73c1[dependency1=<null>,dependency2=111111]
  • index属性
    当某个业务对象的构造方法同时传入了多个类型相同的参数时, Spring又该如何将这些配置中的信息与实际对象的参数一一对应呢?好在,如果配置项信息和对象参数可以按照顺序初步对应的话,Spring还是可以正常工作的,如下所示。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MockBusinessObject {
    private String dependency1;
    private String dependency2;
    public MockBusinessObject(String dependency1,String dependency2)
    {
    this.dependency1 = dependency1;
    this.dependency2 = dependency2;
    }
    ...
    @Override
    public String toString() {
    return new ToStringBuilder(this)
    .append("dependency1", dependency1)
    .append("dependency2", dependency2).toString();
    }
    }
    并且,配置内容如以下代码所示:
    1
    2
    3
    4
    <bean id="mockBO" class="..MockBusinessObject">
    <constructor-arg value="11111"/>
    <constructor-arg value="22222"/>
    </bean>
    那么,我们可以得到如下对象:
    1
    ..MockBusinessObject@1ef8cf3[dependency1=11111,dependency2=22222]
    但是,如果要让“ 11111”作为对象的第二个参数,而将“ 22222”作为第一个参数来构造对象,又该如何呢?好!可以颠倒配置项,如以下代码所示:
    1
    2
    3
    4
    <bean id="mockBO" class="..MockBusinessObject">
    <constructor-arg value="22222"/>
    <constructor-arg value="11111"/>
    </bean>
    不过,还有一种方式,那就是像如下代码所示的那样,使用index属性:
    1
    2
    3
    4
    <bean id="mockBO" class="..MockBusinessObject">
    <constructor-arg index="1" value="11111"/>
    <constructor-arg index="0" value="22222"/>
    </bean>
    这时,同样可以得到想要的对象实例,以下控制台输出表明了这一点:
    1
    ..MockBusinessObject@ecd7e[dependency1=22222,dependency2=11111]

setter方法注入的XML之道

与构造方法注入可以使用<constructor-arg>注入配置相对应, Spring为setter方法注入提供了<property>元素。

<property>有一个name属性,用来指定该<property>将会注入的对象所对应的实例变量名称。之后通过value或者ref属性或者内嵌的其他元素来指定具体的依赖对象引用或者值,如以下代码所示:

1
2
3
4
5
6
7
8
<bean id="djNewsProvider" class="..FXNewsProvider"> 
<property name="newsListener">
<ref bean="djNewsListener"/>
</property>
<property name="newPersistener">
<ref bean="djNewsPersister"/>
</property>
</bean>

当然,如果只是使用进行依赖注入的话,请确保你的对象提供了默认的构造方法,也就是一个参数都没有的那个。
以上配置形式还可以简化为如下形式:

1
2
3
4
<bean id="djNewsProvider" class="..FXNewsProvider">
<property name="newsListener" ref="djNewsListener"/>
<property name="newPersistener" ref="djNewsPersister"/>
</bean>

使用<property>的setter方法注入和使用<constructor-arg>的构造方法注入并不是水火不容的。实际上,如果需要,可以同时使用这两个元素:

1
2
3
4
<bean id="mockBO" class="..MockBusinessObject"> 
<constructor-arg value="11111"/>
<property name="dependency2" value="22222"/>
</bean>

当然,现在需要MockBusinessObject提供一个只有一个String类型参数的构造方法,并且为dependency2提供了相应的setter方法。如下代码演示了符合条件的一个业务对象定义。

1
2
3
4
5
6
7
8
9
10
11
12
public class MockBusinessObject {
private String dependency1;
private String dependency2;
public MockBusinessObject(String dependency){
this.dependency1 = dependency;
}

public void setDependency2(String dependency2) {
this.dependency2 = dependency2;
}
...
}

<property><constructor-arg>中可用的配置项

之前我们看到,可以通过在<property><constructor-arg>这两个元素内部嵌套<value>或者<ref>,来指定将为当前对象注入的简单数据类型或者某个对象的引用。不过,为了能够指定多种注入类型, Spring还提供了其他的元素供我们使用,这包括bean、 ref、 idref、 value、 null、 list、 set、 map、props。下面逐个详细讲述。
:以下涉及的所有内嵌元素,对于都是通用的。

  1. <value>
    可以通过value为主体对象注入简单的数据类型,不但可以指定String类型的数据,而且可以指定其他Java语言中的原始类型以及它们的包装器( wrapper)类型,比如int、 Integer等。容器在注入的时候,会做适当的转换工作(我们会在后面揭示转换的奥秘)。之前已经见过如何使用<value>了,不过让我们通过如下代码来重新认识一下它:
    1
    2
    3
    4
    5
    6
    <constructor-arg>
    <value>111111</value>
    </constructor-arg>
    <property name="attributeName">
    <value>222222</value>
    </property>
    当然,也可以使用如下的简化形式(不过这里的value是以上一层元素的属性身份出现):
    1
    2
    <constructor-arg value="111111"/>
    <property name="attributeName" value="222222"/>
    需要说明的是,<value>是最“底层”的元素,它内部不能再嵌套使用其他元素了。
  2. <ref>
    使用ref来引用容器中其他的对象实例,可以通过ref的local、 parent和bean属性来指定引用的对象的beanName是什么。如下演示了ref及其三个对应属性的使用情况。
    1
    2
    3
    <constructor-arg>
    <ref local="djNewsPersister"/>
    </constructor-arg>
    或者
    1
    2
    3
    <constructor-arg>
    <ref parent="djNewsPersister"/>
    </constructor-arg>
    或者
    1
    2
    3
    <constructor-arg>
    <ref bean="djNewsPersister"/>
    </constructor-arg>
    local、 parent和bean的区别在于:

    local只能指定与当前配置的对象在同一个配置文件的对象定义的名称(可以获得XML解析器的id约束验证支持);
    parent则只能指定位于当前容器的父容器中定义的对象引用;
    bean则基本上通吃,所以,通常情况下,直接使用bean来指定对象引用就可以了。

    <ref>的定义为<!ELEMENT ref EMPTY>,也就是说,它下面没有其他子元素可用了,别硬往人家肚子里塞东西哦。
  3. <idref>
    如果要为当前对象注入所依赖的对象的名称,而不是引用,那么通常情况下,可以使用<value>来达到这个目的,使用如下形式:
    1
    2
    3
    <property name="newsListenerBeanName"> 
    <value>djNewsListener</value>
    </property>
    但这种场合下,使用idref才是最为合适的。因为使用idref,容器在解析配置的时候就可以帮你检查这个beanName到底是否存在,而不用等到运行时才发现这个beanName对应的对象实例不存在。
    毕竟,输错名字的问题很常见。以下代码演示了idref的使用:
    1
    2
    3
    <property name="newsListenerBeanName">
    <idref bean="djNewsListener"/>
    </property>
    这段配置跟上面使用<value>达到了相同的目的,不过更加保险。如果愿意,也可以通过local而不是bean来指定最终值,不过bean比较大众化哦。
  4. 内部<bean>
    使用<ref>可以引用容器中独立定义的对象定义。但有时,可能我们所依赖的对象只有当前一个对象引用,或者某个对象定义我们不想其他对象通过<ref>引用到它。这时,我们可以使用内嵌的<bean>,将这个私有的对象定义仅局限在当前对象。 对于FX新闻系统的DowJonesNewsListener而言,实际上只有道琼斯的FXNewsProvider会使用它。而且,我们也不想让其他对象引用到它。
    为此完全可以将它配置为内部<bean>的形式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <bean id="djNewsProvider" class="..FXNewsProvider"> 
    <constructor-arg index="0">
    <bean class="..impl.DowJonesNewsListener">
    </bean>
    </constructor-arg>
    <constructor-arg index="1">
    <ref bean="djNewsPersister"/>
    </constructor-arg>
    </bean>
    这样,该对象实例就只有当前的djNewsProvider可以使用,其他对象无法取得该对象的引用。
    注意: 因为就只有当前对象引用内部所指定的对象,所以,内部的id不是必须的。当然,如果你愿意指定id,那也是无所谓的。如下所示:
    1
    2
    3
    4
    <constructor-arg index="0">
    <bean id="djNewsListener" class="..impl.DowJonesNewsListener">
    </bean>
    </constructor-arg>

Spring揭秘-BeanFactory的XML之旅之(上)
https://leehoward.cn/2019/10/28/Spring揭秘-BeanFactory的XML之旅之(上)/
作者
lihao
发布于
2019年10月28日
许可协议