接上篇:Spring 核心技术(2)
version 5.1.8.RELEASE
1.4 依赖
典型的企业应用程序不会只包含单个对象(或 Spring 术语中的 bean)。即使是最简单的应用程序也是由很多对象进行协同工作,以呈现出最终用户所看到的有条理的应用程序。下一节将介绍如何从定义多个独立的 bean 到实现对象之间相互协作从而实现可达成具体目标的应用程序。
1.4.1 依赖注入
依赖注入(DI)是一钟对象处理方式,通过这个过程,对象只能通过构造函数参数、工厂方法参数或对象实例化后设置的属性来定义它们的依赖关系(即它们使用的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程从本质上逆转了 bean 靠自己本身通过直接使用类的构造函数或服务定位模式来控制实例化或定位其依赖的情况,因此称之为控制反转。
使用 DI 原则的代码更清晰,当对象和其依赖项一起提供时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类。因此,尤其是依赖允许在单元测试中使用模拟实现的接口或抽象基类时,类会变得更容易测试。
DI 存在两个主要变体:基于构造函数的依赖注入和基于 Setter 的依赖注入。
基于构造函数的依赖注入
基于构造函数的 DI 由容器调用具有多个参数的构造函数来完成,每个参数表示一个依赖项。和调用具有特定参数的静态工厂方法来构造 bean 几乎一样,本次讨论用相同的方式处理构造函数和静态工厂方法的参数。以下示例显示了一个只能通过构造函数注入进行依赖注入的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
请注意,这个类没有什么特别之处。它是一个不依赖于特定容器接口、基类或注释的POJO。
构造函数参数解析
通过使用的参数类型进行构造函数参数解析匹配。如果 bean 定义的构造函数参数中不存在潜在的歧义,那么在 bean 定义中定义构造函数参数的顺序就是在实例化 bean 时将这些参数提供给适当的构造函数的顺序。参考以下类:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
假设 ThingTwo
类和 ThingThree
类没有继承关系,则不存在潜在的歧义。那么,以下配置可以正常工作,你也不需要在 <constructor-arg/>
元素中显式指定构造函数参数索引或类型。
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>
当引用另一个 bean 时,类型是已知的,并且可以进行匹配(与前面的示例一样)。当使用简单类型时,例如 <value>true</value>
,Spring 无法确定值的类型,因此无法在没有帮助的情况下按类型进行匹配。参考以下类:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
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开始。
构造函数参数名称
也可以使用构造函数参数名称消除歧义,如以下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
请记住,为了使这项工作开箱即用,必须在启用调试标志的情况下编译代码,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用 debug 标志编译代码,则可以使用 JDK 批注 @ConstructorProperties 显式命名构造函数参数。然后,示例类必须如下所示:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于 Setter 的依赖注入
基于 setter 的 DI 由容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 之后调用 setter 方法完成。
以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。这个类是传统的 Java 类。它是一个POJO,它不依赖于特定容器接口、基类或注释。
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
ApplicationContext
支持它管理的 bean 使用基于构造函数和基于 setter 的 DI。它还支持在通过构造函数方法注入了一些依赖项之后使用基于 setter 的 DI。你可以以 BeanDefinition
的形式配置依赖项,可以将其与 PropertyEditor
实例结合使用将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户不直接使用这些类(即编码),而是用 XML bean
定义、注解组件(也就是带 @Component
, @Controller
等注解的类)或基于 Java 的 @Configuration
类中的 @Bean
方法。然后,这些源在内部转换为 BeanDefinition
实例并用于加载整个 Spring IoC 容器实例。
基于构造函数或基于 setter 的 DI?
由于可以混合使用基于构造函数和基于 setter 的 DI,因此将构造函数用于必填依赖项的同时 setter 方法或配置方法用于可选依赖项是一个很好的经验法则。请注意, 在 setter 方法上使用 @Required 注解可使属性成为必需的依赖项,然而更推荐使用编程式参数验证的构造函数注入。
Spring 团队通常提倡构造函数注入,因为它允许你将应用程序组件实现为不可变对象,并确保所需的依赖项不是
null
。此外,构造函数注入的组件始终以完全初始化的状态返回给客户端(调用)代码。旁注:大量的构造函数参数是一个糟糕的代码味道,意味着该类可能有太多的责任,应该重构以更好地进行关注点的分离。Setter 注入应仅用于可在类中指定合理默认值的可选依赖项。否则,必须在代码使用依赖项的所有位置执行非空检查。setter 注入的一个好处是 setter 方法使该类的对象可以在以后重新配置或重新注入。因此,通过 JMX MBean 进行管理是 setter 注入的一个很好的使用场景。
使用对特定类最有意义的 DI 方式。有时在处理没有源码的第三方类时需要你自己做选择。例如,如果第三方类没有暴露任何 setter 方法,那么构造函数注入可能是唯一可用的 DI 方式。
依赖处理过程
容器执行 bean 依赖性解析过程如下:
- 创建
ApplicationContext
,之后根据描述所有 Bean 的配置元数据进行初始化。配置元数据可以由 XML、Java代码或注解指定。 - 每个 bean 的依赖关系都以属性、构造函数参数或静态工厂方法参数(如果使用它而不是普通的构造函数)的形式表示。实际创建 bean 时,会将这些依赖项提供给 bean。
- 每个属性或构造函数参数都实际定义了需要设置的值或对容器中另一个 bean 的引用。
- 每个属性或构造函数参数都是一个从其指定的格式转换为该属性或构造函数参数实际类型的值。默认情况下,Spring 能够将提供的字符串格式转换成所有内置类型的值,例如
int
、long
、String
、boolean
等等。
Spring 容器在创建时验证每个 bean 的配置。但是在实际创建 bean 之前不会设置其属性。作用域为单例且被设置为预先实例化(默认值)的 Bean 会在创建容器时创建。作用域在 Bean 作用域中定义。否则 bean 仅在需要时才会创建。创建 bean 可能会导致很多 bean 被创建,因为 bean 的依赖项及其依赖项的依赖项(依此类推)被创建和分配。请注意,这些依赖项之间不匹配的问题可能会较晚才能被发现 - 也就是说,受影响的 bean 首次创建时。
循环依赖
如果您主要使用构造函数注入,有可能创建无法解析的循环依赖场景。
例如:类 A 通过构造函数注入依赖类 B 的实例,而类 B 通过构造函数注入依赖类 A 的实例。如果将 A 类和 B 类的 bean 配置为相互注入,Spring IoC 容器会在运行时检测到此循环引用,并抛出
BeanCurrentlyInCreationException
。一种可能的解决方案是编辑一些类的源代码,将注入方式修改为 setter。或者是避免使用构造函数注入并仅使用 setter 注入。换句话说,虽然不推荐使用,但你可以使用 setter 注入配置循环依赖项。
与一般情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖强制其中一个 bean 在完全初始化之前被注入另一个 bean(一个经典的鸡与鸡蛋场景)。
你通常可以相信 Spring 会做正确的事。它会在容器加载时检测配置问题,例如引用不存在的 bean 和循环依赖关系。当实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着容器正常加载后,如果在创建对象或其中一个依赖项时出现问题,Spring 容器会捕获一个异常 - 例如,bean 因属性缺失或无效而抛出异常。可能会稍后发现一些配置问题,所以 ApplicationContext
默认情况下实现预实例化单例 bean。在实际需要之前创建这些 bean 是以前期时间和内存为代价的,ApplicationContext
会在创建时发现配置问题,而不是更晚。你仍然可以覆盖此默认行为,以便单例 bean 可以延迟初始化,而不是预先实例化。
如果不存在循环依赖关系,当一个或多个协作 bean 被注入到依赖 bean 时,每个协作 bean 在注入到依赖 bean 之前会被完全配置。这意味着,如果 bean A 依赖于 bean B,那么 Spring IoC 容器在调用 bean A 上的 setter 方法之前会完全配置 bean B。换句话说,bean 已经被实例化(如果它不是预先实例化的单例),依赖项已经被设置,并调用了相关的生命周期方法(如配置初始化方法 或 InitializingBean 回调方法)。
依赖注入的示例
以下示例将基于 XML 的配置元数据用于基于 setter 的 DI。Spring XML 配置文件的一小部分指定了一些 bean 定义,如下所示:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
以下示例展示了相应的 ExampleBean 类:
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 文件中指定的属性进行匹配。以下示例使用基于构造函数的DI:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- constructor injection using the nested ref element -->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- constructor injection using the neater ref attribute -->
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
以下示例展示了相应的 ExampleBean
类:
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 调用静态工厂方法来返回对象的实例:
<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"/>
以下示例展示了相应的 ExampleBean
类:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
静态工厂方法的参数由 <constructor-arg/>
元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类相同(尽管在本例中是)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean 属性而不是 class 属性),因此我们不在此讨论这些细节。
网友评论