依赖项注入
依赖注入 (DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或在对象实例构建或从工厂方法返回后设置在其上的属性来定义其依赖项(即它们一起工作的其他对象)。然后,容器在创建 Bean 时注入这些依赖项。此过程从根本上是 Bean 本身通过直接构建类或服务定位器模式控制其依赖项的实例化或位置的逆过程(因此得名,控制反转)。
遵循 DI 原则,代码会更简洁,当对象提供其依赖项时,解耦会更有效。对象不会查找其依赖项,也不会知道依赖项的位置或类。因此,你的类会变得更容易测试,尤其是在依赖项位于接口或抽象基类中时,这些接口或抽象基类允许在单元测试中使用存根或模拟实现。
DI 有两种主要变体:基于构造函数的依赖项注入 和 基于设置程序的依赖项注入。
基于构造函数的依赖项注入
基于构造函数的 DI 是通过容器调用具有多个参数的构造函数来实现的,每个参数都表示一个依赖项。使用特定参数调用static
工厂方法来构造 bean 几乎是等效的,并且本讨论以类似的方式处理构造函数和static
工厂方法的参数。以下示例显示了一个只能通过构造函数注入进行依赖项注入的类
-
Java
-
Kotlin
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private final 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...
}
// a constructor so that the Spring container can inject a MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
// business logic that actually uses the injected MovieFinder is omitted...
}
请注意,此类没有什么特别之处。它是一个 POJO,不依赖于特定于容器的接口、基类或注释。
构造函数参数解析
构造函数参数解析匹配是通过使用参数的类型来进行的。如果 bean 定义的构造函数参数中不存在潜在的歧义,则 bean 定义中构造函数参数的定义顺序就是 bean 实例化时将这些参数提供给适当构造函数的顺序。考虑以下类
-
Java
-
Kotlin
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
package x.y
class 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 无法确定该值类型,因此在没有帮助的情况下无法按类型进行匹配。考虑以下类
-
Java
-
Kotlin
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private final int years;
// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
package examples
class ExampleBean(
private val years: Int, // Number of years to calculate the Ultimate Answer
private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything
)
在前面的场景中,如果使用 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 可以从构造函数中查找参数名称。如果您无法或不想使用调试标志编译代码,则可以使用 @ConstructorProperties JDK 注释来显式命名构造函数参数。然后,示例类必须如下所示
-
Java
-
Kotlin
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
package examples
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于 Setter 的依赖注入
基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数 static
工厂方法以实例化 Bean 之后,在 Bean 上调用 Setter 方法来完成的。
以下示例显示了一个只能通过纯 Setter 注入进行依赖注入的类。此类是传统的 Java。它是一个 POJO,不依赖于特定于容器的接口、基类或注释。
-
Java
-
Kotlin
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...
}
class SimpleMovieLister {
// a late-initialized property so that the Spring container can inject a MovieFinder
lateinit var 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 容器实例。
依赖项解析过程
容器按如下方式执行 Bean 依赖项解析
-
创建
ApplicationContext
并使用描述所有 Bean 的配置元数据对其进行初始化。配置元数据可以通过 XML、Java 代码或注释指定。 -
对于每个 Bean,其依赖项以属性、构造函数参数或静态工厂方法的参数的形式表示(如果您使用它而不是普通构造函数)。在实际创建 Bean 时,会向 Bean 提供这些依赖项。
-
每个属性或构造函数参数都是要设置的值的实际定义,或对容器中另一个 Bean 的引用。
-
每个作为值的属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的数值转换为所有内置类型,例如
int
、long
、String
、boolean
等。
Spring 容器在创建容器时验证每个 Bean 的配置。但是,Bean 属性本身直到实际创建 Bean 时才设置。当创建容器时,会创建单例作用域的 Bean 并将其设置为预实例化(默认)。作用域在 Bean 作用域 中定义。否则,仅在请求 Bean 时才创建 Bean。创建 Bean 可能会导致创建 Bean 图形,因为会创建并分配 Bean 的依赖项及其依赖项的依赖项(依此类推)。请注意,这些依赖项之间的解析不匹配可能会在后期显示,即在首次创建受影响的 Bean 时。
您通常可以相信 Spring 会做正确的事情。它会在容器加载时检测配置问题,例如对不存在的 Bean 的引用和循环依赖项。Spring 尽可能晚地设置属性并解析依赖项,即在实际创建 Bean 时。这意味着,如果在创建对象或其依赖项时出现问题,则正确加载的 Spring 容器稍后在您请求对象时可能会生成异常,例如,由于缺少或无效的属性,Bean 会抛出异常。某些配置问题的这种潜在延迟可见性是 ApplicationContext
实现默认预实例化单例 Bean 的原因。在实际需要这些 Bean 之前创建这些 Bean 会花费一些前期时间和内存,但您可以在创建 ApplicationContext
时发现配置问题,而不是在以后。您仍然可以覆盖此默认行为,以便单例 Bean 延迟初始化,而不是急切地预实例化。
如果没有循环依赖,当一个或多个协作 bean 注入到一个依赖 bean 中时,每个协作 bean 在注入到依赖 bean 之前都已完全配置。这意味着,如果 bean A 依赖于 bean B,Spring IoC 容器在调用 bean A 上的 setter 方法之前将完全配置 bean B。换句话说,bean 已实例化(如果它不是预先实例化的单例),其依赖项已设置,并且相关的生命周期方法(例如 配置的 init 方法 或 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
类
-
Java
-
Kotlin
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;
}
}
class ExampleBean {
lateinit var beanOne: AnotherBean
lateinit var beanTwo: YetAnotherBean
var i: Int = 0
}
在前面的示例中,声明了 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
类
-
Java
-
Kotlin
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;
}
}
class ExampleBean(
private val beanOne: AnotherBean,
private val beanTwo: YetAnotherBean,
private val i: Int)
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"/>
以下示例显示了相应的 ExampleBean
类
-
Java
-
Kotlin
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;
}
}
class ExampleBean private constructor() {
companion object {
// 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.
@JvmStatic
fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
val eb = ExampleBean (...)
// some other operations...
return eb
}
}
}
static
工厂方法的参数由 <constructor-arg/>
元素提供,与实际使用构造函数完全相同。工厂方法返回的类的类型不必与包含 static
工厂方法的类的类型相同(尽管在此示例中,它们是相同的)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean
属性而不是 class
属性),因此我们在此不讨论这些详细信息。