7.4.2 依赖和配置的细节

优质
小牛编辑
123浏览
2023-12-01

7.4.2 依赖和配置的细节

如上一节所述,可以将bean的属性或构造方法参数定义为到其他托管bean(即协作者)的引用,或者定义为已内置定义的值。因此,Spring的基于XML配置元数据支持在<property/><constructor-arg/>元素中定义子元素。

数值(基本类型、字符串等)

<property/>元素的value属性给bean的属性或构造函数参数指定了一个可读的字符串。Spring的转换服务用于将这些值由字符串转换为属性或参数的实际类型。

<bean 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-namespace进行更简洁的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
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean 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更加简洁,不过,除非您在创建bean定义时使用支持属性自动完成功能的IDE(比如IntelliJ IDEASpring Tool Suite(STS)),否则错别字在运行时而不是在设计时才能发现。强烈推荐使用此类IDE的辅助功能。

还可以将一个java.util.Properties的实例配置为:

<bean
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

    <!-- typed as a 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容器通过使用JavaBeans PropertyEditor机制将<value/>元素中的文本转换为java.util.Properties的一个实例。这是一个非常好的捷径,也是相比于使用value属性方式Spring团队更喜欢使用嵌套的<value/>元素的原因之一。

idref元素

idref元素只是将容器中另一个bean的id(字符串,非引用)传给<constructor-arg/>元素或<property/>元素的一种防错方法。

<bean class="..."/>

<bean class="...">
    <property name="targetName">
        <idref bean="theTargetBean" />
    </property>
</bean>

上面这段bean定义代码片段(在运行时)与以下代码片段完全相同:

<bean class="..." />

<bean class="...">
    <property name="targetName" value="theTargetBean" />
</bean>

第一种形式优于第二种形式,因为idref标签的使用使得容器可以在部署时检验被引用的命名bean是否真实存在。在第二种变体中,不会对传递给beanclient的属性targetName的值进行校验。只有当beanclient被真正实例化时,错别字才会被发现(很可能伴随着严重的后果)。如果beanclient是一个原型bean,那么可能只有在容器部署完的很长时间以后才会发现这个错别字和导致的异常。

在4.0 beans xsd中,不再支持idref元素的local属性,因为idref元素不再提供超过常规的bean引用的值。只需在升级到4.0模式时,将现有的idref local引用改为idref bean即可。

<idref/>元素带来价值的一个常见之处(至少在Spring 2.0之前的版本中)是在ProxyFactoryBeanbean定义的AOP拦截器的配置中。当指定拦截器名称时,使用<idref/>元素可防止拼错拦截器的ID。

对其他bean(协作者)的引用

ref元素是<constructor-arg/><property/>定义中的终极元素。在这里,将一个bean的指定属性的值赋为对容器管理的其他bean(协作者)的引用。被引用的bean是属性赋为该引用的bean的一个依赖,在属性赋值前被引用的bean已经按需完成初始化(如果协作者是一个单例bean,可能已经被容器初始化)。所有的引用最终都是对另一个bean的引用。作用域和校验取决于是否通过beanlocalparent属性指定了其他对象的id/name。

通过<ref/>标签的bean属性指定目标bean是最常见的形式,而且允许创建对同一容器或父容器中任何bean的引用,而不用考虑是否在同一个XML文件中。bean属性的值可能和目标bean的id属性相同,或者是目标bean的name属性值中的一个。

<ref bean="someBean"/>

通过parent属性指定目标bean会创建一个对当前容器的父容器中的bean的引用。parent属性的值可能和目标bean的id属性相同,或者是目标bean的name属性值中的一个,并且目标bean必须在当前容器的一个父容器中。使用这种bean引用的形式主要是当您有多个分层级的容器并且想用一个同名的代理包装父容器中已经存在的bean。

<!-- in the parent context -->
<bean class="com.foo.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean <!-- bean name is the same as the parent bean -->
    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

<property/><constructor-arg/>元素内的<bean/>元素定义了一种所谓的内部bean。

<bean class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

内部bean的定义不需要定义id或名称;如果指定了,容器也不会用这个值作标识符。容器还会忽略创建时的scope标志:内部bean总是匿名的,它们总是随着外部bean创建。将内部bean注入到协作bean中是可能的,只能注入到外部bean中;也可能独立的访问内部bean。

作为一种特殊情况,有可能从自定义作用域接收销毁回调方法,例如,对于一个包含在单例bean中的request作用域的内部bean:内部bean实例的创建将绑定到它的外部bean上,但是销毁回调方法使内部bean可以参与到request作用域的生命周期中。这并非常见情况,内部bean通常只是共享它们的外部bean的作用域。

集合

<list/><set/><map/><props/>元素中,可以分别设置Java集合类型ListSetMapProperties的属性和参数。

<bean class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- 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>
    <!-- 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>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

map的键或值的值或者set的值也可以是以下任何元素:

bean | ref | idref | list | set | map | props | value | null
集合的合并

Spring容器还支持集合的合并。程序开发者能够定义父样式的<list/><map/><set/><props/元素,并能使得子样式的<list/><map/><set/><props/元素继承并覆盖父集合的值。也就是说,子集合的值是在子集合元素覆盖了父集合中指定的值之后父集合元素和子集合元素合并的结果。

关于合并的这一小节讨论父子bean机制。不熟悉父子bean定义的读者在继续之前可能希望阅读相关章节

以下示例示范了集合合并:

<beans>
    <bean abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

请注意在子bean定义的adminEmails属性的<props/>元素上的merge=true的用法。当子bean由容器解析并实例化时,生成的实例具有一个名为adminEmailsProperties类型集合,该集合是子bean的adminEmails集合与父bean的adminEmails集合合并的结果。

administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk

子项的Properties集合的值继承了父项的<props/>中的全部属性元素,而且子项值中的support值覆盖了父集合中的值。

这种合并行为对集合类型<list/><map/><set/>同样适用。在<list/>元素的具体情况下,与List集合类型相关联的语义即有序值集合的概念得到维护,父项的值先于所有子列表的值。如果是MapSetProperties集合类型,则不存在排序。因此,对于容器内部使用的构成MapSetProperties实现类型的集合类型,排序语义无效。

集合合并的限制

无法合并不同的集合类型(比如一个Map和一个List),如果您试图这样做,一个Exception会被抛出。merge属性必须在继承层次较低的子定义上指定;在父集合定义上指定merge属性是多余的,想得到的合并并不会发生。

强类型集合

随着Java 5中引入的泛型,可以使用强类型集合。也就是说,可以声明一个Collection类型,使它只能包含String元素(举例)。如果使用Spring将一个强类型的Collection依赖注入到一个bean中,就可以利用Spring的类型转换支持,以便将强类型Collection实例元素转换为适当的类型,然后才能将其添加到Collection中。

public class Foo {

    private Map<String, Float> accounts;

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

foobean的accounts属性准备注入时,可以通过反射获得关于强类型Map<String, Float>的元素类型的泛型信息。因此,Spring的类型转换基础组件将各种值元素识别为Float类型,并将字符串值9.992.753.99转换为实际的Float类型。

Null和空字符串

Spring将空参数等情况处理为空字符串。下面基于XML的配置元数据片段将email属性设置为空字符串值("")。

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

上面的例子相当于下面的Java代码:

exampleBean.setEmail("")

<null/>元素处理null值。比如:

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

上面的配置相当于下面的Java代码:

exampleBean.setEmail(null)

使用p命名空间的XML快捷方式

p命名空间使您能够直接使用bean元素的属性而不是嵌套的<property/>元素来描述属性的值和/或协作的bean。

Spring支持带有命名空间的、可扩展的、基于XML模式定义的配置格式。本章讨论的bean配置格式在XML模式文档中定义。但是,p命名空间没有在XSD文件中定义,只存在于Spring的核心。

下面的示例展示了结果相同的两段XML片段:第一个使用标准XML格式,第二个使用p命名空间。

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

    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="foo@bar.com"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="foo@bar.com"/>
</beans>

示例展示了bean定义中一个名为email的p命名空间属性,它告诉Spring要包含一个属性声明。如前所述,p命名空间没有模式定义,所以可以将属性(attribute)的名称设置为属性(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
        http://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命名空间的属性值,还使用特殊格式声明了属性引用。第一个bean定义使用<property name =“spouse”ref =“jane”/>来创建从beanjohn到beanjane的引用,第二个bean定义使用p:spouse-ref =“jane”作为属性达到了同样的目的。在此例中,spouse是属性名,而-ref部分表示这并不是一个直接的值而是到另一个bean的引用。

p命名空间不如标准XML格式那么灵活,比如,声明属性引用的格式会与以Ref结尾的属性相冲突,而标准XML格式则不会。我们建议您谨慎地选择实现方式,并将其传达给团队成员,以避免同时使用全部三种方式生成XML文档。

使用c命名空间的XML快捷方式

类似于“使用p命名空间的XML快捷方式”一节,Spring 3.1中新引入的c命名空间允许使用内联属性来配置构造方法参数,而不是使用嵌套的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
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="x.y.Bar"/>
    <bean class="x.y.Baz"/>

    <!-- traditional declaration -->
    <bean class="x.y.Foo">
        <constructor-arg ref="bar"/>
        <constructor-arg ref="baz"/>
        <constructor-arg value="foo@bar.com"/>
    </bean>

    <!-- c-namespace declaration -->
    <bean class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="foo@bar.com"/>

</beans>

c:命名空间使用与p:命名空间相同的约定(使用-ref后缀表示bean引用)用于通过名称来设置构造方法参数。同样,即使没有在XSD模式中定义(但是存在于Spring内核中),c命名空间也需要声明才能使用。

在构造方法的参数名不可用的罕见情况(通常如果字节码在没有调试信息的情况下被编译)下,可以使用参数索引:

<!-- c-namespace index declaration -->
<bean class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>

由于XML语法,索引符号需要前面有个_,因为XML属性名不能以数字开头(即使某些IDE允许)。

在实践中,构造方法解析机制在参数匹配方面十分低效(英文是efficient,估计写错了),所以,除非真的确实需要,我们建议在配置中通篇使用名称符号。

复合属性名称

当设置bean属性时,只要除了最终属性名外路径中的所有组件都不是null,则可以使用复合或嵌套属性名。考虑下面的bean定义:

<bean class="foo.Bar">
    <property name="fred.bob.sammy" value="123" />
</bean>

foobean有一个fred属性,fred有一个bob属性,bob有一个sammy属性,最终sammy属性被设置为值123。为了使该配置生效,foofred属性和fredbob属性在bean被构造之后必须不能为null或抛出NullPointerException异常。