依赖项注入(DI)是一个过程,对象仅通过构造函数参数、工厂方法的参数或对象实例构造或从工厂方法返回后设置的属性来定义其依赖项(即与它们一起工作的其他对象)。然后,容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此称为控制反转),通过使用类的直接构造或服务定位器模式来控制其依赖项的实例化或位置。
使用 DI 原则,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。因此,您的类变得更容易测试,尤其是当依赖项位于接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。
DI 存在两个主要变体:基于构造函数的依赖注入 和 基于 Setter 的依赖注入。
基于构造函数的依赖注入
基于构造函数的依赖注入是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个 依赖项(工厂方法中有参数时和这个是一样的);
比如下面这个示例,表示只能通过构造函数进行依赖注入的类
public class SimpleMovieLister {
// SimpleMovieLister 依赖于 MovieFinder
private final MovieFinder movieFinder;
// 一个构造函数,这样 Spring 容器就可以注入一个 MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
构造函数参数解析
构造函数参数解析匹配通过使用 参数的类型进行。如果 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 {
// 计算最终答案的年数
private final int years;
// 生命、宇宙和一切的答案
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
构造函数参数类型匹配
在上述场景中,如果您通过 type 属性显式指定构造函数参数的类型,则容器可以使用 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>
除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数的歧义。 :::tips index 从 0 开始 :::
构造函数参数名称
您还可以使用构造函数参数名称进行值消歧
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
请记住,要使这项工作开箱即用,您的代码必须在启用调试标志的情况下编译,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用调试标志编译代码,则可以使用 @ConstructorProperties JDK 注释来显式命名您的构造函数参数。示例类必须如下所示:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
@ConstructorProperties 是 JDK java.bens 包下的注解,由于参数名称通常在运行时不可用,所以获取不到参数名称(对于这个有歧义的可以参考 这篇文章,简单说,可以通过编译参数保留参数名称,或则在 jdk8 中使用反射方式获取到参数名称;对于这个参数名不可用的另一个例子就是 mybatis 框架中的 @Param(“参数名”) 注解)
基于 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;
}
// ...
}
你需要明白的是,这样写不代表 spring 会自动扫描这个 set 方法,并注入,还是需要你手动通过配置元数据才能实现(笔者有时候就会认为上面那样就可以自动注入了)
<bean id="simpleMovieLister" class="cn.mrcode.study.springdocsread.SimpleMovieLister">
<property name="movieFinder" ref="movieFinder"></property> // 这里使用属性注入
</bean>
<bean id="movieFinder" class="cn.mrcode.study.springdocsread.MovieFinder"/>
ApplicationContext 为其管理的 bean 支持基于构造函数和基于 setter 的 DI。在通过构造函数方法注入一些依赖项之后,它还支持基于 setter 的 DI。您可以以 BeanDefinition 的形式配置依赖项,将其与PropertyEditor 实例一起使用,将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即编程),而是使用基于 Java 的 @Configuration
类中的 XML Bean 定义、带注释的组件(即用 @Component
、@Controller
等注释的类)或 @bean
方法。然后,这些源在内部转换为 BeanDefinition
的实例,并用于加载整个 Spring IoC 容器实例。
:::tips 基于构造函数还是基于 setter 的 DI?
由于可以混合使用这两种方式,因此可以将基于构造函数用于强制依赖项,将 setter 用于可选依赖项。在 setter 方法上使用 @Required 注解,这表示该 setter 是必须的依赖项
Spring 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为 不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件总是以完全初始化的状态返回给客户机(调用)代码。作为旁注,大量构造函数参数是一种糟糕的代码味道,这意味着类可能有太多的责任,应该进行重构,以更好地解决问题的适当分离。
Setter 注入应该主要只用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。否则,必须在代码使用依赖项的任何地方执行非空检查。setter 注入的一个好处是 setter 方法使该类的对象能够在以后重新配置或重新注入。因此,通过 JMX MBeans 进行管理是 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 图。请注意,这些依赖项之间的分辨率不匹配可能会延迟出现 — 也就是说,在第一次创建受影响的 bean 时。
:::tips
循环依赖
如果您主要使用构造函数注入,则可能会创建无法解决的循环依赖场景。
例如:类 A 通过构造函数注入需要类 B 的实例,类 B 通过构造函数注入需要类 A 的实例。如果为类 A和类 B 配置 bean 以相互注入,Spring IoC 容器会在运行时检测到这个循环引用,并抛出 BeanCurrentlyIncremationException。
一种可能的解决方案是编辑某些类的源代码,由 setter 而不是构造函数进行配置。或者,避免构造函数注入,只使用 setter 注入。换句话说,虽然推荐使用,但是可以通过 setter 注入来配置循环依赖项。
与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖迫使一个 bean 在完全初始化之前注入另一个 bean(典型的鸡和蛋场景)。 :::
您通常可以相信 Spring 可以做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 和循环依赖项的引用。Spring 设置属性和解析依赖项的时间越晚越好,也就是实际创建 bean 的时候。这意味着,当您请求对象时,如果正确加载的 Spring 容器在创建该对象或其依赖项之一时出现问题,则可以在以后生成异常——例如,bean 由于丢失或无效属性而抛出异常。这可能延迟了一些配置问题的可见性,这就是为什么 ApplicationContext 实现默认情况下会预先实例化单例 bean。为了在实际需要之前创建这些bean,您需要花费一些前期时间和内存,在创建 ApplicationContext 时才会发现配置问题,而不是在稍后。你仍然可以覆盖这个默认行为,这样单例 bean 就可以延迟初始化,而不是提前初始化。
如果不存在循环依赖项,当一个或多个协作 bean 被注入到依赖 bean 中时,每个协作 bean 在被注入到依赖 bean 之前都已经完全配置好了。这意味着,如果 bean A 依赖于 bean B, Spring IoC 容器会在调用bean A 的 setter 方法之前完全配置 bean B。换句话说,bean 被实例化(如果它不是一个预先实例化的单例),它的依赖被设置,并调用相关的生命周期方法(例如配置的 init 方法或 InitializingBean 回调方法)。
依赖注入的例子
比如下面这个例子基于 XML 的配置元数据,使用 setter 的注入
<bean id="exampleBean" class="examples.ExampleBean">
<!-- 使用嵌套 ref 元素的 setter 注入 -->
<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"/>
对应的 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">
<!-- 使用嵌套 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 工厂方法来返回对象的实例,而不是使用构造函数:
public class ExampleBean {
// 私有有构造
private ExampleBean(...) {
...
}
// 静态工厂法;此方法的参数可以是
// 考虑到返回的 bean 的依赖关系,
// 不管这些参数实际上是如何使用的。
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
<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"/>
静态工厂方法的参数由 <constructor arg/>
元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是这样)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean 属性而不是 class 属性之外),因此我们在这里不讨论这些细节。