依赖项与配置详解
依赖与配置详解
正如在上一节中提到的,你可以将 bean 属性和构造函数参数定义为对其他托管 bean(协作者)的引用,或者定义为内联值。为此,Spring 基于 XML 的配置元数据在其 <property/>
和 <constructor-arg/>
元素中支持子元素类型。
直接值(基本类型、字符串等)
<property/>
元素的 value
属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring 的 转换服务 用于将这些值从 String
转换为属性或参数的实际类型。以下示例展示了设置的各种值:
<bean id="myDataSource" 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="misterkaoli"/>
</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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" 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="misterkaoli"/>
</beans>
前面的 XML 更为简洁。然而,拼写错误只能在运行时被发现,而非设计时,除非你使用支持在创建 bean 定义时自动属性补全的 IDE(例如 IntelliJ IDEA 或 Spring Tools for Eclipse)。强烈推荐使用此类 IDE 辅助功能。
你也可以配置一个 java.util.Properties
实例,如下所示:
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<!-- 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
实例。这是一个很好的快捷方式,也是 Spring 团队在某些地方更倾向于使用嵌套的 <value/>
元素而不是 value
属性风格的原因之一。
idref
元素
idref
元素是一种防止错误的方式,用于将容器中另一个 bean 的 id
(一个字符串值,而不是引用)传递给 <constructor-arg/>
或 <property/>
元素。以下示例展示了如何使用它:
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
前面的 bean 定义片段在运行时与以下片段完全等效:
<bean id="theTargetBean" class="..." />
<bean id="theClientBean" class="...">
<property name="targetName" ref="theTargetBean"/>
</bean>
第一种形式优于第二种形式,因为使用 idref
标签可以让容器在部署时验证所引用的命名 bean 是否确实存在。在第二种形式中,传递给 client
bean 的 targetName
属性的值不会进行任何验证。只有在 client
bean 实际实例化时,才会发现拼写错误(很可能会导致致命的结果)。如果 client
bean 是一个原型 bean,这个拼写错误及其导致的异常可能在容器部署后很长时间才会被发现。
在 4.0 版本的 beans XSD 中,idref
元素上的 local
属性不再被支持,因为它不再比普通的 bean
引用提供更多价值。在升级到 4.0 模式时,请将现有的 idref local
引用更改为 idref bean
。
在 Spring 2.0 之前的版本中,<idref/>
元素的一个常见用途是在 ProxyFactoryBean
bean 定义中配置 AOP 拦截器。在指定拦截器名称时使用 <idref/>
元素可以防止拦截器 ID 的拼写错误。
对其他 Bean 的引用(协作者)
ref
元素是 <constructor-arg/>
或 <property/>
定义元素中的最后一个元素。在这里,你可以将 bean 的指定属性值设置为对容器管理的另一个 bean(协作者)的引用。被引用的 bean 是要设置属性的 bean 的依赖项,并且在设置属性之前,它会根据需要按需初始化(如果协作者是单例 bean,它可能已经被容器初始化)。所有引用最终都是对另一个对象的引用。作用域和验证取决于你是否通过 bean
或 parent
属性指定了另一个对象的 ID 或名称。
通过 <ref/>
标签的 bean
属性指定目标 bean 是最通用的形式,它允许创建对同一容器或父容器中任何 bean 的引用,无论它是否位于同一个 XML 文件中。bean
属性的值可以与目标 bean 的 id
属性相同,也可以与目标 bean 的 name
属性中的某个值相同。以下示例展示了如何使用 ref
元素:
<ref bean="someBean"/>
通过 parent
属性指定目标 bean 会创建一个对当前容器的父容器中的 bean 的引用。parent
属性的值可以与目标 bean 的 id
属性相同,也可以与目标 bean 的 name
属性中的某个值相同。目标 bean 必须位于当前容器的父容器中。当您有一个容器层次结构,并且想要在父容器中使用与父 bean 同名的代理来包装现有 bean 时,主要应使用此 bean 引用变体。以下代码对展示了如何使用 parent
属性:
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required here -->
</bean>
<!-- in the child (descendant) context, bean name is the same as the parent bean -->
<bean id="accountService"
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>
在 4.0 版本的 beans XSD 中,ref
元素上的 local
属性不再被支持,因为它不再比普通的 bean
引用提供更多价值。在升级到 4.0 版本的模式时,请将现有的 ref local
引用更改为 ref bean
。
内部 Bean
在 <property/>
或 <constructor-arg/>
元素内部的 <bean/>
元素定义了一个内部 bean,如下例所示:
<bean id="outer" 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。内部 bean 实例的创建与其包含的 bean 绑定,但销毁回调使其能够参与请求作用域的生命周期。这并不是一个常见的情况。内部 bean 通常只是简单地共享其包含 bean 的作用域。
集合
<list/>
、<set/>
、<map/>
和 <props/>
元素分别用于设置 Java 集合类型 List
、Set
、Map
和 Properties
的属性和参数。以下示例展示了如何使用它们:
<bean id="moreComplexObject" 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>
映射的键或值,或集合的值,也可以是以下任意一种元素:
bean | ref | idref | list | set | map | props | value | null
集合合并
Spring 容器还支持集合的合并。应用程序开发人员可以定义一个父级的 <list/>
、<map/>
、<set/>
或 <props/>
元素,并让子级的 <list/>
、<map/>
、<set/>
或 <props/>
元素继承并覆盖父级集合中的值。也就是说,子集合的值是父集合和子集合元素合并的结果,其中子集合的元素会覆盖父集合中指定的值。
本节关于合并的讨论涉及父子 bean 机制。不熟悉父 bean 和子 bean 定义的读者可能希望在继续之前阅读相关章节。
以下示例展示了集合合并:
<beans>
<bean id="parent" 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 id="child" 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>
请注意 child
bean 定义中 adminEmails
属性的 <props/>
元素上使用的 merge=true
属性。当容器解析并实例化 child
bean 时,生成的实例将拥有一个 adminEmails
的 Properties
集合,该集合包含了子 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
集合类型相关的语义(即值的有序集合的概念)得以保留。父级的值会排在所有子级列表的值之前。对于 Map
、Set
和 Properties
集合类型,不存在顺序的概念。因此,对于容器内部使用的与 Map
、Set
和 Properties
实现类型相关的集合类型,没有有效的顺序语义。
集合合并的限制
你无法合并不同类型的集合(例如 Map
和 List
)。如果你尝试这样做,将会抛出一个适当的 Exception
。merge
属性必须在较低层次、继承的子定义上指定。在父集合定义上指定 merge
属性是多余的,并且不会实现所需的合并效果。
强类型集合
得益于 Java 对泛型的支持,你可以使用强类型的集合。也就是说,可以声明一个 Collection
类型,使其只能包含(例如)String
类型的元素。如果你使用 Spring 将强类型的 Collection
依赖注入到一个 bean 中,你可以利用 Spring 的类型转换支持,使得在将元素添加到 Collection
之前,强类型 Collection
实例的元素会被转换为适当的类型。下面的 Java 类和 bean 定义展示了如何做到这一点:
- Java
- Kotlin
public class SomeClass {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
class SomeClass {
lateinit var accounts: Map<String, Float>
}
<beans>
<bean id="something" class="x.y.SomeClass">
<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>
当 something
bean 的 accounts
属性准备好进行注入时,关于强类型 Map<String, Float>
的元素类型的泛型信息可以通过反射获得。因此,Spring 的类型转换基础设施能够识别各种值元素为 Float
类型,并将字符串值(9.99
、2.75
和 3.99
)转换为实际的 Float
类型。
空值和空字符串值
Spring 将属性等的空参数视为空 String
。以下基于 XML 的配置元数据片段将 email
属性设置为空 String
值("")。
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
前面的例子等同于以下 Java 代码:
- Java
- Kotlin
exampleBean.setEmail("");
exampleBean.email = ""
<null/>
元素用于处理 null
值。以下示例展示了其用法:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
上述配置等同于以下 Java 代码:
- Java
- Kotlin
exampleBean.setEmail(null);
exampleBean.email = null
使用 p-命名空间的 XML 快捷方式
p-namespace 允许你使用 bean
元素的属性(而不是嵌套的 <property/>
元素)来描述你的属性值、协作的 bean,或两者兼而有之。
Spring 支持基于 XML 模式定义的可扩展配置格式。本章讨论的 beans
配置格式定义在 XML Schema 文档中。然而,p-namespace 并未在 XSD 文件中定义,仅存在于 Spring 的核心中。
以下示例展示了两个 XML 片段(第一个使用标准 XML 格式,第二个使用 p-namespace),它们解析为相同的结果:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
</beans>
该示例展示了在 bean 定义中使用了一个名为 email
的 p-命名空间属性。这告诉 Spring 包含一个属性声明。如前所述,p-命名空间没有模式定义,因此您可以将属性名称设置为属性名。
下一个示例包含了两个更多的 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
https://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-namespace 来设置属性值,还采用了一种特殊格式来声明属性引用。第一个 bean 定义使用 <property name="spouse" ref="jane"/>
来创建从 bean john
到 bean jane
的引用,而第二个 bean 定义则使用 p:spouse-ref="jane"
作为属性来完成完全相同的事情。在这种情况下,spouse
是属性名称,而 -ref
部分表示这不是一个直接的值,而是对另一个 bean 的引用。
p-namespace 不如标准的 XML 格式灵活。例如,声明属性引用的格式与以 Ref
结尾的属性冲突,而标准的 XML 格式则不会。我们建议你谨慎选择方法,并与团队成员沟通,以避免生成的 XML 文档同时使用所有三种方法。
使用 c-namespace 的 XML 快捷方式
类似于 使用 p-namespace 的 XML 快捷方式,Spring 3.1 引入的 c-namespace 允许使用内联属性来配置构造函数参数,而不是嵌套的 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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>
<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
</beans>
c:
命名空间使用与 p:
命名空间相同的约定(通过名称设置构造函数参数时使用尾随的 -ref
表示 bean 引用)。同样,它需要在 XML 文件中声明,尽管它没有在 XSD 模式中定义(它存在于 Spring 核心中)。
在极少数情况下,如果构造函数参数的名称不可用(通常是因为字节码在没有 -parameters
标志的情况下编译),你可以回退到使用参数索引,如下所示:
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="something@somewhere.com"/>
由于 XML 语法规则,索引符号要求必须带有前导的 _
,因为 XML 属性名称不能以数字开头(尽管某些 IDE 允许这样做)。<constructor-arg>
元素也可以使用相应的索引符号,但由于通常声明的顺序已经足够,因此并不常用。
在实践中,构造函数解析 机制 在匹配参数方面非常高效,因此除非你确实需要,否则我们建议在整个配置中使用名称标注。
复合属性名称
在设置 bean 属性时,你可以使用复合或嵌套的属性名称,只要路径中除最后一个属性名称外的所有组件都不为 null
。考虑以下 bean 定义:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
something
bean 有一个 fred
属性,而 fred
属性又有一个 bob
属性,bob
属性又有一个 sammy
属性,最终 sammy
属性被设置为 123
。为了使这能够正常工作,在 bean 构造完成后,something
的 fred
属性和 fred
的 bob
属性不能为 null
,否则会抛出 NullPointerException
。