ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 4.4 依赖 典型的企业级应用程序不是由单独对象(或 Spring 中的 bean)构成的。尽管最简单的 应用程序有一些对象协同工作来表示终端用户所看到的连贯的应用。下一节会解释如何去为 完全实现的应用程序定义一组独立的 bean,其中对象间相互协作来达到目标。 ### 4.4.1 依赖注入 依赖注入(DI)是对象定义它们依赖的过程,也就是说,要和它们协同工作其它对象, 仅仅可以通过构造方法参数,工厂方法参数,或者是在工厂方法返回的对象或被构造好后, 为对象实例设置的属性。容器当创建好 bean,随后就会注入那些依赖。这个过程从根本上 来说是反向的,因此命名为控制反转(IoC),bea n 本身直接使用构造好的类或服务定位器 模式来控制实例或它的依赖的所在位置。 应用了 DI 原则,代码就干净多了,当为对象提供它们依赖的时候,解耦是很有效率的。 对象不再去检查它的依赖,也不需要知道位置或依赖的类。因此,类就很容易被测试,特别 是当依赖是接口或抽象基类的时候,这允许在单元测试中使用 stub 或 mock 的实现类。 DI 存在两种主要的方式,基于构造方法的依赖注入(4.4.1.1 节)和基于 setter 方法的依赖注入(4.4.1.2 节)。 #### 4.4.1.1 基于构造方法的依赖注入 基于构造方法的依赖注入是容器调用构造方法和一组参数完成的,每个都表示着一个依 赖。使用特定的参数来调用 static 工厂方法构造 bean 基本也是相同的,这种说法把给构 造方法的参数和给 static 工厂方法的参数相类似。下面的示例展示了一个仅使用构造方 法进行依赖注入的饿类。注意这个类没有什么特殊之处,就是一个 POJO 而且没有对容器特 定接口,基类或注解的依赖。 ``` public class SimpleMovieLister { // SimpleMovieLister对MovieFinder有依赖 private MovieFinder movieFinder; // 这个构造方法使得Spring容器可以’注入’MovieFinder public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // 业务逻辑就可以’使用’注入的MovieFinder了,代码就省略了... } ``` 构造方法参数解析 构造方法参数解析匹配使用参数的类型。如果在 bean 的构造方法参数不存在潜在的歧义,那么当 bean 被实例化的时候,定义的构造方法参数的顺序就是被提供到适当构造方法 参数的顺序。请看下面的类: ``` package x.y; public class Foo { public Foo(Bar bar, Baz baz) { // ... } } ``` 没有潜在的歧义存在,假设 Bar 和 Baz 类没有继承关系。因此下面的配置就可以使用 了,而且并不需要明确地在`<constructor-arg/>`元素中去指定构造方法参数的索引和/ 或类型。 ``` <beans> <bean id="foo" class="x.y.Foo"> <constructor-arg ref="bar"/> <constructor-arg ref="baz"/> </bean> <bean id="bar" class="x.y.Bar"/> <bean id="baz" class="x.y.Baz"/> </beans> ``` 当另外一个 bean 被引用时,类型是明确的,那么匹配就能成功(前面示例中也是这样 的)。当使用简单类型时,比如`<value>true<value>`,Spring 不能决定值的类型,所以 没有帮助是不能匹配的。看下面的示例: ``` package examples; public class ExampleBean { //计算最佳答案的年数 private int years; // 生命,宇宙,所有问题的答案 private String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ``` 构造方法参数类型匹配 在上面的情形下,如果你使用 type 属性明确地为构造方法参数指定类型的话,容器可 以进行匹配简单的类型。比如: ``` <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg type="int" value="7500000"/> <constructor-arg type="java.lang.String" value="42"/> </bean> ``` 构造方法参数索引 使用 index 属性来指定明确的构造方法参数索引。比如: ``` <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg index="0" value="7500000"/> <constructor-arg index="1" value="42"/> </bean> ``` 此外,为了解决多个简单值的歧义,如果构造方法有两个相同类型的参数时,指定所以 可以解决歧义。注意索引是基于 0 开始的。 构造方法参数名称 在 Spring 3.0 中,你也可以使用构造方法参数名称来消除歧义: ``` <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg name="years" value="7500000"/> <constructor-arg name="ultimateanswer" value="42"/> </bean> ``` 要记住要使得这种方式可用,代码必须和内嵌的调试标识一起编译,那样 Spring 才可 以从构造方法中来查找参数。如果没有和调试标识(或不想)一起编译,那么可以使用 JDK 的注解[@ConstructorProperties](http://download.oracle.com/javase/6/docs/api/java/beans/ConstructorProperties.html) 来明确构造方法参数。那么示例代码就如下所示: ``` package examples; public class ExampleBean { // 忽略属性 @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ``` #### 4.4.1.2 基于 setter 方法的依赖注入 基于 setter 方法的依赖注入由容器在调用过无参数的构造方法或无参数的 static 工厂 方法来实例化 bean 之后,再调用 bean 的 setter 方法来完成的。 下面的示例展示了一个仅仅能使用纯 setter 方法进行依赖注入的类。这是一个常见的 Java 类,是一个没有依赖容器指定的接口,基类或注解的 POJO。 ``` public class SimpleMovieLister { // SimpleMovieLister对MovieFinder有依赖 private MovieFinder movieFinder; // setter方法可以让Spring容器来'注入'MovieFinder public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // 真正’使用’注入的MovieFinder的业务逻辑代码被省略了... } ``` ApplicationContext 对它管理的 bea n 支持基于构造方法和 setter 方法的依赖注入。 也 支 持 在 一 些 依 赖 已 经 通 过 构 造 方 法 注 入 之 后 再 进 行 setter 方 法 注 入 。 以 BeanDefinition 形式来配置依赖,使用 PropertyEditor 实例将属性从一种形式格式 化到另一种。但很多 Spring 的用户不直接(编程时)使用这些类,而是使用 XML 文件来定 义,之后会在内部转换这些类的实例,需要加载整个 Spring IoC 容器的实例。 基于构造方法还是 setter 方法进行依赖注入? 因为你可以将二者混淆,那么对于基于构造方法或是 setter 方法的依赖注入,有一个很好的规则就是为强制依赖使用构造方法参数,而对于可选参数使用 setter 方法。要注意在 setter 方法上使用注解@Required(4.9.1 节),可用于 setter 方法所需的依赖。 Spring 团队通常主张使用 setter 方法注入,因为大量的构造方法参数会使程序变得非 常笨拙,特别是当属性为可选的时候。Setter 方法会让该类的对象今后适合于重新配置或 重新注入。通过 JMX Mbean(第 23 章)来管理就是一个令人关注的用例。 一些人纯粹赞成构造方法注入。提供所有对象的依赖意味着对象在完全初始化状态 时,通常要返回客户端(调用)代码。这个缺点会使得对象变得不适合去重新配置或重新注入。 使用依赖注入的时候要特别注意一些类。当要选择处理没有源代码时的第三方类库 的时候。遗留的类可能没有暴露任何 setter 方法,而构造方法注入则是唯一可用的依赖 注入方式。 #### 4.4.1.3 解决依赖过程 容器按如下步骤来解决 bean 的依赖: 1. ApplicationContext 和描述了所有 bean 的配置元数据一起被创建并初始化。配 置元数据可以通过 XML,Java 代码或注解来指定。 2. 对于每一个 bean 来说,它的依赖被表述为属性,构造方法参数的形式,如果你使用 了静态工厂方法来代替构造方法,那么还会是静态工厂方法参数的形式。当 bean 被实际创 建时,这些依赖被提供给 bean。 3. 每个属性或构造方法参数就是一个要被设置的值或者是容器中其它 bean 的引用。 4. 每个属性或构造方法参数值会被转换特定格式的形式,去匹配属性或构造方法参数的类型。默认情况下,Spring 可以转换给定的字符串格式的值到内建的类型,比如 int,long, String,boolean 等。 当容器被创建时,Spring 容器会来验证每个 bean 的配置,包括验证 bean 的引用属性是 否是一个合法的 bean。然而,bean 属性本身直到 bean 真正被创建出来后才被设置进去。 当容器被创建时,bean 的范围是单例时,会被设置成预实例(默认情况)来创建。范围在 4.5 节,“bean 的范围”部分来解释。否则,bean 就会当被请求的时候被创建。一个 bean 的创建潜在地会引起一系列 bean 被创建,因为 bean 的依赖和它依赖的依赖(等等)要不 创建和定义出来。 循环依赖 如果你使用主要的构造方法注入,就可能会引起不可解决的循环依赖情形。 比如,类 A 需要通过构造方法注入获得类 B 的实例,而类 B 也需要通过构造方法注入获得类 A 的实例。如果把类 A 和类 B 进行相互注入,Spring 的 IoC 容器会在运行时检 测到这是循环引用的情况,并且抛出 BeanCurrentlyInCreationException 异常。 一个可行的方案是编辑一些要配置的类的源码,通过 setter 方法而不是构造方法进 行注入。而且,避免构造方法注入并仅仅使用 setter 方法注入。换句话说,尽管这不是推荐做法,你可以通过 setter 方法注入来配置循环依赖。 不像典型的用例(没有循环依赖),在 bean A 和 bean B 之间的循环依赖强制一个 bean 被注入到另一个中会先于被完全初始化自己(就是经典的鸡和蛋的问题)。 通常情况下你可以信任 Spring 做出正确的事情。它会在容器加载时来检测配置问题, 比如引用了一个不存在的 bean 和循环依赖问题。当 bean 被实际创建出来后,Spring 设置属 性和解决依赖会尽量地晚些。这就意味着 Spring 容器已经加载正确了但晚些时候可能会生 成异常,比如当你请求一个创建时发生问题的对象或者是它的依赖发送问题。例如,bean 因为丢失或非法属性而抛出异常。这种潜在的一些配置问题的可见度延迟也就是为什么 ApplicationContext 的实现类默认情况都是预实例的单例 bean。在前期的时间和内存 消耗中,在 bean 真正需要前来创建这些 bean,当 ApplicationContext 被创建的时候, 你会发现这些配置问题,这还不晚。你也可以覆盖这个默认的行为,那么单例 bean 就会延 迟加载,而不是预实例的了。 如果没有循环依赖的存在,当一个或多个协作的 bean 被注入到一个独立的 bean 时, 每个协作的 bean 就会在被注入之前完全被配置好。这就意味着如果 bean A 对 bean B 有依 赖,那么 Spring 的 IoC 容器会完全配置 bean B 而优先于调用 bean A 中的 setter 方法。换句 话说,bean 被实例化了(而不是预实例的单例 bean),它的依赖才被注入,相关的生命周 期方法(比如配置初始化方法(4.6.1.1 节)或 InitializingBea n 的回调方法(4.6.1.1 节))才 被调用。 #### 4.4.1.4 依赖注入示例 下面的示例使用了基于 XML 的配置元数据来进行基于 setter 方法的依赖注入。Spring XML 配置文件的一小部分来指定几个 bean 的定义: ``` <bean id="exampleBean" class="examples.ExampleBean"> <!-- 使用嵌入<ref/>的元素来进行setter方法注入<ref/> --> <property name="beanOne"> <ref bean="anotherExampleBean"/> </property> <!-- 使用整洁的'ref'属性来进行setter方法注入--> <property name="beanTwo" ref="yetAnotherBean"/> <property name="integerProperty" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public void setBeanOne(AnotherBean beanOne) { this.beanOne = beanOne; } public void setBeanTwo(YetAnotherBean beanTwo) { this.beanTwo = beanTwo; } public void setIntegerProperty(int i) { this.i = i; } } ``` 上面的示例中,setter 方法被声明为匹配 XML 文件中指定的属性。下面的示例使用基于 构造方法的依赖注入: ``` <bean id="exampleBean" class="examples.ExampleBean"> <!-- 使用嵌入的<ref/>元素进行构造方法注入 --> <constructor-arg> <ref bean="anotherExampleBean"/> </constructor-arg> <!-- 使用整洁的'ref'属性来进行构造方法注入 --> <constructor-arg ref="yetAnotherBean"/> <constructor-arg type="int" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public ExampleBean(AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { this.beanOne = anotherBean; this.beanTwo = yetAnotherBean; this.i = i; } } ``` 在 bean 中指定的构造方法参数会被用于 ExampleBean 的构造方法参数。 现在考虑一下这个示例的变化情况,要代替使用构造方法,Spring 被告知调用 static 工厂方法并返回一个对象的实例: ``` <bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance"> <constructor-arg ref="anotherExampleBean"/> <constructor-arg ref="yetAnotherBean"/> <constructor-arg value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> public class ExampleBean { // 私有的构造方法 private ExampleBean(...) { ... } // 静态工厂方法; 这个方法的参数可以被认为是要返回bean的依赖, // 而不管那些参数是如何被使用的。 public static ExampleBean createInstance (AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { ExampleBean eb = new ExampleBean (...); // 其它的操作... return eb; } } ``` static 工厂方法的参数通过`<constructor-arg/>`元素来提供,这和构造方法已经 被实际调用是完全一致的。由工厂方法返回的类的类型不需要和包含 static 工厂方法类 的类型相同,尽管在本例中是相同的。实例(非静态)工厂方法可以被用于本质上相同的方 式(除了 factory-bean 属性的使用,来代替 class 属性),所以这里不讨论它们的细节。 ### 4.4.2 深入依赖和配置 正如之前章节中所提到的,你可以定义 bean 的属性和构造方法参数作为其它被管理 bean(协作者)的引用,或者作为内联值的定义。出于这个目的,Spring 的基于 XML 的配 置元数据支持使用`<property/>`和`<constructor-arg/>`元素的子元素类型。 #### 4.4.2.1 直接值(原生类型,String,等) <property/>元素的 value 属性指定了属性或构造方法参数,这是人们可以阅读的 字符串的表达。正如之前提到过的(6.4.2 节),JavaBean 的 PropertyEditor 可以用来转 换这些字符串值从 String 到属性或参数的真实类型。 ``` <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <!-- setDriverClassName(String)方法调用的结果 --> <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> ``` 对于更简洁的 XML 配置,下面的示例使用了 p-命名空间(4.4.2.6 节)。 ``` <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-3.0.xs d"> <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="masterkaoli"/> </beans> ``` 上面的 XML 非常简洁;然而,错误之处会在运行时被发现而不是设计的时候,除非在 你创建 bean 的时候,使用如 [IntelliJ IDEA](http://www.jetbrains.com/idea/) [或](http://www.springsource.com/products/sts) [SpringSource Tool Suite](http://www.springsource.com/products/sts)(STS,SpringSource 组织 开发的工具套件)这样的 IDE 来支持自动地属性补全。这样的 IDE 帮助是强烈建议使用的。 你也可以这样来配置 java.util.Properties 实例: ``` <bean id="mappings" class="org.springframework.beans.factory.config.PropertyPlac eholderConfigurer"> <!-- 作为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 容器会使用 JavaBean 的 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="client" class="..."> <property name="targetName" value="theTargetBean" /> </bean> ``` 第一种形式比第二种形式可取,因为使用 idref 标签允许容器在部署时来验证被引用 的,命名的 bean 是否真的存在。第二种形式下,没有对传递给 client bea n 的 targetName 属性执行验证。错误仅仅是在 client bean 被真正实例化的时候才会被发现(和很多可能 致命的结果)。如果 client bean 是 prototype(4.5 节)类型的 bean,这个错误和结果异常 可能仅仅在容器被部署很长一段时间后才会被发现。 此外,如果被引用的 bean 在同一个 XML 单元中,bean 的名称就是 bean 的 id,那么你 还可以使用 local 属性,这允许 XML 解析器本身在 XML 文档解析的时候,及早地来验证 bean 的 id。 ``` <property name="targetName"> <!-- id为'theTargetBean'的bean必须存在;否则就会抛出异常 --> <idref local="theTargetBean"/> </property> ``` `<idref/>` 元 素 带 来 的 一 个 相 同 之 处 ( 最 少 是 Spring 2.0 版 本 以 前 的 ) 是 值 在 ProxyFactoryBean 定义的 AOP 拦截器的配置中。当你指定拦截器名称来防止拼错拦截 器的 id 时,可以使用`<idref/>`元素。 #### 4.4.2.2 引用其它 bean(协作者) ref 元素是`<constructor-arg/>`或`<property/>`定义元素中最后的一个。在这里 你可以为 bean 设置指定属性的值,来引用被容器管理的另外一个 bean(协作者)。被引用 的 bean 就是要设置属性的这个 bean 的一个依赖,并且是初始化的作为属性设置之前的点 播。(如果协作者是一个单例的 bean,它可能已经被容器初始化过了。)所有引用最终都会 被引用到一个对象中。范围和验证基于通过 bean,local 或 parent 属性指定 id/name 的其它对象。 通过`<ref/>`标签的 bean 属性来指定目标 bean 是最常用的方式,并允许创建引用到相 同容器或父容器的任意 bean 中,而不管它们是不是在同一个 XML 文件中。bean 属性的值 可能和目标 bean 的 id 属性相同,或者是目标 bean 的 name 属性值之一。 ``` <ref bean="someBean"/> ``` 通过 local 属性来指定目标 bean 是利用了 XML 解析器的能力,来验证 XML 相同文件 内的 id 引用。local 属性的值必须和目标 bean 的 id 属性一致。如果没有在相同的文件内 发现匹配的元素,那么 XML 解析器会报告问题。因此,如果目标 bean 在同一个 XML 文件 中的话,使用 local 这种形式是最佳选择(为了更早地知道错误)。 ``` <ref local="someBean"/> ``` 通过 parent 属性来指定目标 bea n 会为 bea n 创建引用,它是在当前容器的父容器中的。parent 属性的值可能和目标 bean 的 id 属性相同,或者是目标 bean 的 name 属性值 之一,而且目标 bean 必须在当前容器的父容器中。当有一个容器继承关系,可以使用这个 bean 的引用,或在你想使用和父 bean 有相同名称的代理包装一个父容器中已有 bean 时。 ``` <!-- 在父上下文中--> <bean id="accountService" class="com.foo.SimpleAccountService"> <!-- 在这里插入所需要的依赖 --> </bean> <!-- 在子(后继)上下文中 --> <bean id="accountService" <-- bean名称和父bean相同 --> class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target"> <ref parent="accountService"/> <!-- 注意我们如何引用父bean --> </property> <!-- 在这里插入其它配置和所需的依赖 --> </bean> ``` #### 4.4.2.3 内部 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 或 name;容器忽略这些值。也会忽略 scope 标识。内 部 bean 通常是匿名的,而且范围通常是 prototype(4.5.2 节)的。注入内部 bean 到协作 bean 是不可能的,只能到包围它的 bean 中。 #### 4.4.2.4 集合 在 `<list/>` , `<set/>` , `<map/>` 和 `<props/>` 元素中,你可以分别设置 Java 的 Collection 类型 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 类型的 key 或 value 的值,或 set 的值,也可以是下列元素之一: + bean + ref + idref + list + set + map + props + value + null 集合合并 在 Spring 2.0 中,容器支持集合的合并。应用开发人员可以定义父样式的`<list/>`,`<map/>`,`<set/>`或`<props/>`元素,子样式的`<list/>`,`<map/>`,`<set/>`或`<props/>`元素继承或重写来自父集合的值。也就是说,子集合的值是合并元素父与子集合中的元素,和子集合元素覆盖父集合中指定值的结果。 本节关于导论父子 bean 合并的机制。如果读者不熟悉父和子 bean 的定义,可能要先去 阅读一下相关章节(4.7 节)。 下面的示例说明了集合合并: ``` <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"> <!-- 合并在*子*集合定义中来指定 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 ``` 子 bean 的 Properties 集合的值设置继承了从父`<props/>`元素而来的所有属性,子 bean 中 support 的值覆盖了父 bean 中的值。 这种合并行为的应用和`<list/>`,`<map/>`和`<set/>`集合类型很相似。在`<list/>`元素特 定的情形下,和 List 集合类型相关的语义,也就是说,ordered 集合值的概念,是要维 护的;父值优先于所有子 list 的值。在 Map,Set 和 Properties 集合类型的情况下,没有顺序的存在。因此对于容器内部使用的,和 Map,Set 和 Properties 实现类型相关的 集合类型没有排序语义的作用。 集合合并的限制 不能合并不同类型的集合(比如 Map 和 List),如果你要尝试这么去做,那么就会抛 出 Exception。merge 属性必须在低级的,继承的,子 bean 中来指定;在父集合中指定 merge 属性是冗余的,也不会看到想要的合并结果。合并特性仅在 Spring 2.0 和更高版本中 可用。 强类型集合(Java 5 以上版本) 在 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 id="foo" 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> ``` 当 foo bean 的 accounts 属性准备好注入时,关于强类型元素 `Map<String, Float>` 类型的泛型信息就通过反射机制准备好了。因此 Spring 的类型转换工具识别到各种元素的 值作为 Float 类型,字符串值 9.99,2.75 和 3.99 被转换成实际的 Float 类型。 #### 4.4.2.5 null 和空字符串 Spring 将属性的空参数当作空 String。下面基于 XML 的配置元数据片段设置了电子邮 件属性为空 String 值("")。 ``` <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)。 #### 4.4.2.6 使用 p-命名空间的 XML 快捷方式 p-命名空间可以使你使用 bea n 的元素属性,而不是内嵌的`<property/>`元素,来描述属 性的值和/或协作的 bean。 Spring 2.0 和后续版本支持使用命名空间(附录 C)的可扩展的配置格式,这是基于 XML的 Schema 定义的。本章中讨论的 bea n 的配置格式是定义在 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 http://www.springframework.org/schema/beans/spring -beans-3.0.xsd"> <bean name="classic" class="com.example.ExampleBean"> <property name="email" value=["foo@bar.co](mailto:foo@bar.com)m"/> </bean> <bean name="p-namespace" class="com.example.ExampleBean" p:email=["foo@bar.co](mailto:foo@bar.com)m"/> </beans> ``` 示例展示了 p-命名空间下的属性,在 bean 的定义中称为 email。这就告诉 Spring 来包 含属性声明。正如前面提到的,p-命名空间没有 schema 定义,所以你可以设置属性的名称 和 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 http://www.springframework.org/schema/beans/spring -beans-3.0.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"/>`来创建 john bean 对 jane bean 的引用关系,第二个 bean 中,使用了 p:spouse-ref="jane"作为属性来做相同的事情。本例中,spouse 是属性名,而-ref 部分表明了这不是一个直接值而是一个对其它 bean 的引用。 > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > P-命名空间没有标准 XML 格式那么灵活。比如,声明属性引用的格式和以 Ref 结尾的 属性相冲突,而标准的 XML 格式则不会。我们建议谨慎选择所用的方法并和开发团队成员 充分地交流,避免产生同时使用这三种方法的 XML 文档。 #### 4.4.2.7 使用 c-命名空间的 XML 快捷方式 和 4.4.2.6 节,“使用 p-命名空间的 XML 快捷方式”相似,c-命名空间是在 Spring 3.1 中 被引入的,允许使用内联属性来配置构造方法参数而不是使用 constructor-arg 元素。 我们来看 4.4.1.1 节,“基于构造方法的依赖注入”中的示例,现在使用 c 命名空间: ``` <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema -instance" xmlns:p="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="bar" class="x.y.Bar"/> <bean id="baz" class="x.y.Baz"/> <-- '传统的'声明方式 --> <bean id="foo" class="x.y.Foo"> <constructor-arg ref="bar"/> <constructor-arg ref="baz"/> <constructor-arg value=["foo@bar.co](mailto:foo@bar.com)m"/> </bean> <-- 'c-命名空间'声明方式 --> <bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email=["foo@bar.co](mailto:foo@bar.com)m"> </beans> ``` c:命名空间使用了和 p:(以-ref 结尾的 bean 引用)相同的转换机制通过它们的名称 来设置构造方法参数。这样一来,即便没有在 XSD schema(但是存在于 Spring 核心的内部) 中定义,它还是需要声明出来。 在极少数的情况下,构造方法参数名称是不可用的(通常如果字节码在编译时没有调试 信息),我们可以使用参数索引: ``` <-- 'c-命名空间'索引声明 --> <bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"> ``` > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 因为 XML 的语法,索引符号需要在其头部使用_作为 XML 属性名称,因为 XML 中属性 是不能以数字开头的(尽管一些 IDE 允许这样做)。 在实际运用中,构造方法解析机制(4.4.1.1 节)在匹配参数时是非常有效率的,所以除 非真有需要,我们建议在配置中使用名称符号。 #### 4.4.2.8 复合属性名称 在设置 bean 的属性时,只要路径中的所有组件,除了最后一个属性名称是非 null 的, 可以使用复合或者嵌套的属性名称。参考下面的 bean 定义。 ``` <bean id="foo" class="foo.Bar"> <property name="fred.bob.sammy" value="123" /> </bean> ``` foo bean 有一个 fred 属性,它还有一个 bob 属性,而它仍有一个 sammy 属性,而最 终的 sammy 属性被设置成数值 123。为了让这样的配置可用,foo 的属性 fred,fred 的 属性 bob 在 bean 被构造好后必须不能为 null,否则会抛出 NullPointerException 异常。 ### 4.4.3 使用 depends-on 如果一个 bean 是另外一个 bean 的依赖,通常表明了它会被设置成另外一个的属性。 典型的情况是在基于 XML 的配置元数据中使用`<ref/>`(4.4.2.2 节)元素来完成。然而,有 时在两个 bean 之间的依赖并不是直接的;比如,类中的静态初始化器需要触发,就像数据 库驱动程序的注册。depends-on 属性可以明确地强制一个或多个 bean 在使用这个元素的 bean 被初始化之前被初始化。下面的示例使用了 depends-on 属性来表示一个独立 bean 的依赖: ``` <bean id="beanOne" class="ExampleBean" depends-on="manager"/> <bean id="manager" class="ManagerBean" /> ``` 为了表示对多个 bean 的依赖,提供一个 bean 名称的列表作为 depends-on 属性的值 即可,要用逗号,空白或分号分隔开,它们都是有效的分隔符: ``` <bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao"> <property name="manager" ref="manager" /> </bean> <bean id="manager" class="ManagerBean" /> <bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" /> ``` > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 在 bean 定义中的 depends-on 属性可以指定初始化时的依赖,也可以是仅仅是单例(4.5.1 节)bean,对应销毁时的依赖。和给定 bean 定义了 depends-on 关系的依赖 bean 首先被销毁,先于给定的 bean 本身。因此 depends-on 也能控制关闭顺序。 ### 4.4.4 延迟初始化 bean 默认情况下,ApplicationContext 的实现类积极地创建并配置所有单例的 bean (4.5.1 节),作为初始化过程的一部分。通常来说,这种预实例化是非常可取的,因为在配 置或周边环境中的错误可以直接被发现,而不是几小时或几天后去发现。当这种行为不可用时,你可以阻止单例 bean 的预实例化,在 bean 定义中使用延迟初始化来标记一下就可以 了。延迟初始化 bean 告诉 IoC 容器在该 bean 第一次被请求时再来实例化,而不是在启动时 实例化。 在 XML 中,这个行为可以通过`<bean/>`元素的 lazy-init 属性来控制;比如: ``` <bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/> <bean name="not.lazy" class="com.foo.AnotherBean"/> ``` 当前者配置被 ApplicationContext 处理时,命名为 lazy 的 bean 并不会在 ApplicationContext 启动时被预实例化,而 not.lazy bean 会被先预实例化。 而到延迟初始化 bean 是一个单例 bean 的依赖时,且这个单例 bean 不是延迟初始化的,那么 ApplicationContext 也会在启动时来创建延迟初始化的 bean,因为它必须满足单 例 bean 的依赖。延迟初始化的 bean 被注入到单例 bean 中的时候它就不是延迟初始化的了。 你也可以在容器级别来控制延 迟初始化,在 `<beans/>` 元素上使用default-lazy-init 属性;比如: ``` <beans default-lazy-init="true"> <!-- 那么就没有bean是预实例化的了... --> </beans> ``` ### 4.4.5 自动装配协作者 Spring 容器可以自动装配协作 bean 之间的关系。你可以允许 Spring 自动为你的 bean 来 解析协作者(其它 bean),通过检查 ApplicationContext 的内容即可。自动装配有下 列优势: 自动装配可以显著减少指定属性或构造方法参数的需要。(如 bean 模板的机制,这将在 本章的 4.7 节来讨论,在这方面是有价值的。) 自动装配可以更新配置对象的演变。比如,如果你需要给一个类添加依赖,那个依赖可 以自动被填入而不需要修改配置。因此,自动装配在开发期间是非常有用的,当代码库 变得更稳定时切换到明确的装配也没有否定的选择。 当使用基于 XML 的配置元数据(4.4.1 节)时,你可以为 bean 的定义指定自动装配模 式,在`<bean/>`元素上设置 autowire 属性即可。自动装配功能有五种模式。你可以为每个 bean 来指定自动装配,因此可以选择为哪一个来指定自动装配。 表 4.2 自动装配模式 | 模式 | 解释 | | --- | --- | | no | (默认情况)没有自动装配。Bean 的引用必须通过 ref 元素来定义。对于大型的部署,修改默认设置是不推荐的,因为明确地指定协作者会给予更多的控制和清晰。某种程度上来说,它勾勒出了系统的结构。 | | byName | 通过属性名称来自动装配。Spring 以相同名称来查找需要被自动装配的 bean。比如,如果 bean 被设置成由名称来自动装配,并含有一个 master 属性(也 就说,有 setMaster(..)方法),Spring 会查找名为 master 的 bean 定义,并且用它来设置属性。 | | byType | 如果 bean 的属性类型在容器中存在的话,就允许属性被自动装配。如果存在多于一个,就会抛出致命的异常,这就说明了对那个 bean 不能使用 byType 进行自动装配。如果没有匹配的 bean 存在,就不会有任何效果;属性不会 被设置。 | | constructor | 和 byType 类似,但是是应用于构造方法参数的。如果在容器中没有确定的构造方法参数类型的 bean 的存在,就会发生致命的错误。 | 使用 byType 或 constructor 自动装配模式,你可以装配数组和集合类型。这种情况下所有在容器内匹配期望类型的自动装配候选者会被用来满足依赖。如果期望的键类型是String,你可以自动装配强类型的 Map。自动装配的 Map 值会包括匹配期望类型的实例, 而且 Map 的键会包含对应 bean 的名称。 你可以联合自动装配行为和依赖检查,这会在自动装配完成之后执行。 #### 4.4.5.1 自动装配的限制和缺点 当在项目中一直使用时,自动装配是非常不错的。如果自动装配通常是不使用的,它就 可能会迷惑开发人员来使用它去装配仅仅一两个 bean。 考虑一下自动装配的限制和缺点: * 在 property 和 constructor-arg 中设置的明确依赖通常覆盖自动装配。不能自动 装配所谓的简单属性,比如原生类型,String 和 Class(还有这样简单属性的数字)。这 是由于设计的限制。 * 自动装配没有明确装配那么精确,尽管,在上面的表格中已经说明了,Spring 很小心地 避免在有歧义的情况下去猜测,但也可能会有意想不到的结果,在 Spring 管理的对象 中间的关系可能就不会再清晰地说明了。 * 装配信息可能对从 Spring 容器中来生成文档的工具来说不可用。 * 在容器中的多个 bean 定义可能通过 setter 方法或构造方法参数匹配指定的类型进行自 动装配。对于数组,集合或 Map,这不一定是一个问题。而对期望简单值的依赖,这 种歧义不能随意解决。如果没有唯一的 bean 可用,就会抛出异常。 在后面一种情况中,你有几种选择: * 放弃自动装配而使用明确的装配。 * 避免设置它的 autowire-candidate 属性为 false 来自动装配 bean,这会在下一 节中来解释。 * 设置`<bean/>`元素的 primary 属性为 true 来指定单独的 bean 作为主要的候选者。 * 如果你使用 Java 5 或更高版本,使用基于注解的配置实现更细粒度的控制,这会在 4.9 节,“基于注解的容器配置”中来解释。 #### 4.4.5.2 从自动装配中排除 bean 在每个 bean 的基础上,你可以从自动装配中来排除 bean。在 Spring 的 XML 格式配置 中,设置`<bean/>`元素的 autowire-candidate 属性为 false;容器会把指定的 bean 对自动装配不可用(包含注解风格的配置,比如@Autowired(4.9.2 节))。 你也可以基于 bean 名称的模式匹配来限制自动装配候选者。顶级的`<beans/>`元素中的 default-autowire-candidates 属性接受一个或多个模式。比如,为了限制自动装 配候选者到任意名称以 Repository 结尾 bean 的状态,提供*Repository 值。要提供多个模式,把它们定义在以逗号分隔的列表中。Bean 定义中 autowire-candidate 属性的 true 或 false 明确的值通常是优先的,而且对于这些 bean 来说,模式匹配规则是不适用的。 这些技术对那些永远不想通过自动装配被注入到其它 bea n 中的 bea n 来说是很有用的。 这并不意味着未包含的 bean 不能使用自动装配来配置。相反,bean 本身不是自动装配其它 bean 的候选者。 ### 4.4.6 方法注入 在很多应用场景中,很多容器中的 bean 是单例(4.5.1 节)的。当一个单例的 bean 需 要和其它单例的 bean 协作时,或者一个非单例的 bean 需要和其它非单例的 bean 协作时, 典型地做法是通过定义一个 bean 作为另外一个的属性来控制依赖。当 bean 的生命周期不 同时问题就产生了。假设单例 bean A 需要使用非单例(prototype,原型)bean B,或许在 A 的每个方法调用上。容器仅仅创建单例 bean A 一次,因此仅仅有一次机会去设置属性。容 器不能每次为 bean A 提供所需的 bean B 新的实例。 解决方法就是放弃一些控制反转。你可通过实现 ApplicationContextAware 接口 以让 bean A 去感知容器(4.6.2 节),而且在每次 bean A 需要 bean B 的实例时,可以让容器 调用 getBean(“B”)(4.2.3 节)去获取(一个新的)bean B。下面的代码就是这种方式的例子: ``` // 使用了有状态的命令风格的类来执行一些处理 package fiona.apple; // 导入Spring的API import org.springframework.beans.BeansException; import org.springframework.context.Applicationcontext; import org.springframework.context.ApplicationContextAware; public class CommandManager implements ApplicationContextAware { private ApplicationContext applicationContext; public Object process(Map commandState) { // 抓取相应命令的新实例 Command command = createCommand(); // 在命令实例上(希望标记新的)设置状态 command.setState(commandState); return command.execute(); } protected Command createCommand() { // 注意Spring API的依赖! return this.applicationContext.getBean("command", Command.class); } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } } ``` 前面的做法是不可取的,因为这些业务代码感知并耦合到了 Spring 框架中。方法注入, 这种 Spring IoC 容器中的有点先进的功能,允许以一种干净的方式来处理这个用例。 你可以在[博客文章](http://blog.springsource.com/2004/08/06/method-injection/)中来阅读更多关于方法注入的动机。 #### 4.4.6.1 查找方法注入 查找方法注入是容器在其管理的 bean 上来覆盖方法的能力,来返回容器中其它命名 bean 的查找结果。查找,典型的情况是涉及原型 bean,这是在之前章节中描述的情景。Spring Framework 从 CGLIB 类库中,通过使用字节码生成机制实现了这种方法注入,来动态地生成 覆盖方法的子类。 > ![](https://box.kancloud.cn/2016-01-25_56a58526bb420.gif) > 注意 > 要让这些动态子类起作用,在类路径下必须有 CGLIB 的 jar 文件。这些 Spring 容器中的 子类不能是 final 类型的,要被覆盖的方法也不能是 final 类型的。而且,测试有 abstract 方法的类要求你自己去编写类的子类并提供 abstract 方法的实现。最后,是 方法注入目标的对象还不能被序列化。 看一下之前代码片段中的 CommandManager 类,你会看到 Spring 容器会动态地覆盖 createCommand()方法的实现。CommandManager 类不会有任何 Spring 的依赖,在重新设计 的例子中你会看到: ``` package fiona.apple; // 没有Spring API的导入! public abstract class CommandManager { public Object process(Object commandState) { // 抓取一个心得合适的Command接口的实例 Command command = createCommand(); //在命令实例上(希望标记新的)设置状态 command.setState(commandState); return command.execute(); } // 好的... 但是这个方法的实现在哪儿? protected abstract Command createCommand(); } ``` 在客户端类中包含要被注入(本例中的 CommandManager)的方法,要被注入的方法 需要从下面的示例中编写一个签名: ``` <public|protected> [abstract] <return-type> theMethodName(no-arguments); ``` 如果方法是 abstract 的,动态生成的子类就实现这个方法。否则,动态生成的子类覆盖 定义在源类中的确定方法。比如: ``` <!-- 部署为原型的(非单例的)有状态的bean --> <bean id="command" class="fiona.apple.AsyncCommand" scope="prototype"> <!-- 注入所需要的依赖 --> </bean> <!-- commandProcessor使用statefulCommandHelper --> <bean id="commandManager" class="fiona.apple.CommandManager"> <lookup-method name="createCommand" bean="command"/> </bean> ``` 当需要 command bean 的新的实例时,标识为 commandManager 的 bean 调用它自己的 方法 createCommand()。部署 command bean 为原型的可必须要小心,那确实就是需要 的才行。如果被部署为单例的(4.5.1 节),那么每次会返回相同的 command bean 的实例。 > ![](https://box.kancloud.cn/2016-01-25_56a58526e359f.gif) > 提示 > 感 兴 趣 的 读 者 可 能 会 发 现 要 使 用 ServiceLocatorFactoryBean (在 org.springframework.beans.factory.config 包下)。在 ServiceLocatorFactoryBean 中的使用的方法和其它工具类是相似的,ObjectFactoryCreatingFactoryBean,但是它允许你去指定你自己的查找接口而不是 Spring 特定的查找接口。对这些类查询一下 JavaDoc 文档,还有[博客文章](http://blog.arendsen.net/index.php/2006/10/05/on-the-servicelocatorfactorybean-dlas-and-the-sustainability-of-code-and-design/)来获取 ServiceLocatorFactoryBean 的额外的信息。 #### 4.4.6.2 任意方法的替代 在方法注入中,用处不如查找方法注入大的一种形式,就是使用另外一个方法实现来替 换被容器管理的 bean 中的任意方法的能力。用户可以安全地跳过本节的其它部分,直到你 真正需要这些功能的时候来回过头来看。 使用基于 XML 的配置元数据,对于部署的 bean,你可以使用 replaced-method 元 素来使用另外一个方法替换已有的方法实现。看看下面这个类,有一个我们想要去覆盖的 computeValue 方法: ``` public class MyValueCalculator { public String computeValue(String input) { // 真正的代码... } // 其它方法... } ``` 实现了 org.springframework.beans.factory.support.MethodReplacer 接口的类提供新的方法定义。 ``` /** 也就是要在MyValueCalculator中实现用来覆盖已有的方法 computeValue(String) */ public class ReplacementComputeValue implements MethodReplacer { public Object reimplement(Object o, Method m, Object[] args) throws Throwable { // 获取输入值,并处理它,返回一个计算的结果 String input = (String) args[0]; ... return ...; } } ``` 部署源类的 bean 定义和指定的方法覆盖可以是这样的: ``` <bean id="myValueCalculator" class="x.y.z.MyValueCalculator"> <!-- 任意方法替换 --> <replaced-method name="computeValue" replacer="replacementComputeValue"> <arg-type>String</arg-type> </replaced-method> </bean> <bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/> ``` 你可以在`<replaced-method/>`元素中使用一个或多个包含的`<arg-type/>`元素来 指示被覆盖方法的方法签名。如果方法是被重载的并在类中有很多种形式的存在,那么参数 的签名是必须的。为了简便,字符串类型的参数可能是类型全名的一个子串。比如,下面所 有的都匹配 java.lang.String: ``` java.lang.String String Str ``` 因为用参数的数量用来区分每一个可能的选择通常是足够的,这种快捷方式可以节省大 量的输入,允许你去输入匹配参数类型的最短的字符串。