如上一章所述,您可以将 Bean 的属性和构造函数参数定义为对其他 Bean 的引用,或定义为内联值。为此,Spring 基于 XML 的配置元数据支持子元素类型 <property/> 和 <constructor-arg/> 元素。
Spring 的 <property/> 元素的 value 属性用来将 Bean 的属性或构造函数参数指定为字符串值。Spring 的转换服务用于将这些值从字符串转换为 Bean 的属性或构造函数参数的实际类型。例如:
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <!-- 调用 setDriverClassName(String) 方法设置 driverClassName 属性的值 --> <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(p 命名空间) 实现了更简洁的 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>
注意,上面的示例是不是更简洁。不过,错误配置是在运行时而不是 Bean 配置时被发现,除非您使用的集成开发环境(如 IntelliJ IDEA 或 Spring Tools for Eclipse)在创建 Bean 定义时支持自动属性补全,强烈建议使用此类集成开发环境,提高开发效率,降低出错率。
您还可以配置一个 java.util.Properties 实例,如下所示:
<bean id="mappings" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"> <!-- 类型为 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 团队推荐的方式。
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="client" class="..."> <property name="targetName" value="theTargetBean"/> </bean>
第一种形式比第二种形式更可取,因为使用 idref 属性可以让容器在部署时验证所引用的 Bean 是否确实存在。在第二种变体中,不会对传递给客户机 Bean 的 targetName 属性的值进行验证。只有在实际实例化客户机 Bean 时,才会发现拼写错误(结果很可能是致命的)。如果客户机 Bean 是一个 prototype Bean,那么只有在容器部署后很久才会发现配置错误和由此产生异常。
提示:4.0 beans XSD 不再支持 idref 元素上的 local 属性,因为它不再提供比普通 bean 引用更多的值。在升级到 4.0 模式时,请将现有的 idref local 引用更改为 idref bean。
<idref/> 元素带来价值的一个常见地方(至少在 Spring 2.0 之前的版本中)是 ProxyFactoryBean Bean 定义中 AOP 拦截器的配置。在指定拦截器名称时使用 <idref/> 元素可以防止拼错拦截器 ID。
ref 元素是 <constructor-arg/> 或 <property/> 定义元素中的最后一个元素。在这里,您将 Bean 指定属性的值设置为对容器管理的另一个 Bean 的引用。被引用的 Bean 是要设置其属性 Bean 的依赖项,并且在设置属性之前根据需要对其进行初始化。如果依赖 Bean 是一个单例 Bean,它可能已经被容器初始化了。所有引用最终都是对另一个对象的引用。作用域和验证取决于您是通过 Bean 还是通过 parent 属性指定另一个对象的 ID 或 name。
通过 <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。
下面展示了如何使用父属性:
<!-- 在父容器中 --> <bean id="accountService" class="com.something.SimpleAccountService"> <!-- 在此处插入所需的依赖项 --> </bean> <!-- 在子(子孙)容器中 --> <!-- bean 名称与父 bean 相同 --> <bean id="accountService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target"> <!-- 注意我们是如何引用父 bean 的 --> <ref parent="accountService"/> </property> <!-- 根据需要在此处插入其他配置和依赖项 --> </bean>
如下面的示例所示,<property/> 或 <constructor-arg/> 元素内的 <bean/> 元素定义了一个内部 Bean:
<bean id="outer" class="..."> <!-- 而不是使用对目标 Bean 的引用,只需内联定义目标 Bean --> <property name="target"> <bean class="com.example.Person"> <!-- 这是内部 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>
Map 键、值或集合值的值也可以是以下任何元素:
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"> <!-- 合并是在子集合定义上指定的 --> <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 时,生成的实例将具有一个 adminEmails 属性集合,其中包含将子 Bean 的 adminEmails 集合与父 Bean 的 adminEmails 集合合并的结果。下面的列表显示了合并结果:
administrator=administrator@example.com sales=sales@example.com support=support@example.co.uk
子属性集合的值集继承了父属性<props/>的所有属性元素,并且子属性的支持值覆盖了父属性集合中的值。
这种合并行为同样适用于 <list/>、<map/> 和 <set/> 集合类型。在 <list/> 元素的特定情况下,与列表集合类型相关的语义(即值的有序集合概念)将保持不变。父列表的值优先于子列表的所有值。而 Map、Set 和 Properties 集合类型则不存在排序。因此,对于作为容器内部使用的关联 Map、Set 和 Properties 实现类型的基础的集合类型,不存在排序语义。
注意,不能合并不同的集合类型(如 Map 和 List),如果尝试合并,将抛出相应的 "异常"(Exception)。
合并属性必须在较低的、继承的子定义上指定。在父级集合定义中指定合并属性是多余的,不会产生预期的合并效果。
得益于 Java 对泛型的支持,您可以使用强类型集合。也就是说,可以声明一个 Collection 类型,使其只能包含(例如)String 元素。如果您使用 Spring 将强类型集合依赖注入到 Bean 中,您就可以利用 Spring 的类型转换支持,在将强类型集合实例的元素添加到集合之前将其转换为相应的类型。例如:
public class SomeClass { // 声明了一个强类型Map private Map<String, Float> accounts; public void setAccounts(Map<String, Float> accounts) { this.accounts = accounts; } }
对应的 XML 如下:
<beans> <bean id="something" class="x.y.SomeClass"> <!-- 注入强类型 Map --> <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 将属性的空参数视为空字符串。以下基于 XML 的配置元数据片段将 email 属性设置为空字符串值("")。例如:
<bean class="ExampleBean"> <property name="email" value=""/> </bean>
上面示例的配置相当于下面的 Java 代码:
exampleBean.setEmail("");
<null/> 元素用于处理空值。例如:
<bean class="ExampleBean"> <property name="email"> <null/> </property> </bean>
上面示例的配置相当于下面的 Java 代码:
exampleBean.setEmail(null);
Spring 中,p 名称空间(p-namespace)允许您使用 Bean 元素的属性(而不是嵌套的 <property/> 元素)来描述属性值、协作 Bean 或两者。
Spring 支持基于 XML Schema 定义的带有命名空间的可扩展配置格式。本章讨论的 Bean 配置格式就是在 XML Schema 文档中定义的。然而,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 https://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 使用标准格式,即嵌入 property 元素 --> <bean name="classic" class="com.example.ExampleBean"> <property name="email" value="someone@somewhere.com"/> </bean> <!-- 使用 p 名称空间模式 --> <bean name="p-namespace" class="com.example.ExampleBean" p:email="someone@somewhere.com"/> </beans>
该示例显示了 bean 定义中 p 命名空间中名为 email 的属性。这告诉 Spring 包含一个属性声明。如前所述,p 命名空间没有名称定义,因此您可以将 p 命名空间的属性的名称设置为 Bean 属性的名称,如:p:email 中的 email 属性即使 p 空间也是 Bean 的属性名称。
下一个示例包括另外两个 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> <!-- p 命名空间使用 --> <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"/> 来创建从 Bean john 到 Bean jane 的引用,而第二个 Bean 定义使用 p:spouse-ref="jane" 作为属性来做完全相同的事情。在这种情况下,spouse 是属性名称,而 -ref 部分表示这不是一个直接的值,而是对另一个 Bean 的引用。
与使用 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 https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="beanTwo" class="x.y.ThingTwo"/> <bean id="beanThree" class="x.y.ThingThree"/> <!-- 带可选参数名的传统声明 --> <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 命名空间声明 --> <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 引用)来通过名称设置构造函数参数。同样,即使它没有在 XSD 模式中定义(它存在于 Spring 核心中),也需要在 XML 文件中声明。
对于构造函数参数名称不可用的极少数情况(通常是在编译字节码时没有调试信息),可以使用参数索引,如下所示:
<!-- c 命名空间索引声明 --> <bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree" c:_2="something@somewhere.com"/>
在实践中,构造函数解析机制在匹配参数方面相当有效,因此除非确实需要,我们建议在整个配置中使用名称符号。
您可以在设置 Bean 属性时使用复合或嵌套属性名称,只要路径的所有组件 (最终属性名称除外) 都不为空。考虑以下 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 属性不得为空。否则,将产生 NullPointerException 异常。