依赖项注入

依赖注入 (DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或在对象实例构建或从工厂方法返回后设置在其上的属性来定义其依赖项(即它们一起工作的其他对象)。然后,容器在创建 Bean 时注入这些依赖项。此过程从根本上是 Bean 本身通过直接构建类或服务定位器模式控制其依赖项的实例化或位置的逆过程(因此得名,控制反转)。

遵循 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)

假设ThingTwoThingThree 类不是通过继承相关的,则不存在潜在的歧义。因此,以下配置可以正常工作,并且你无需在 <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 容器实例。

基于构造函数还是基于 Setter 的 DI?

由于您可以混合基于构造函数和基于设置程序的 DI,因此一个好的经验法则是对强制依赖项使用构造函数,对可选依赖项使用设置程序方法或配置方法。请注意,可以在设置程序方法上使用 @Autowired 注释,以使属性成为必需的依赖项;但是,首选使用构造函数注入并对参数进行编程验证。

Spring 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保必需的依赖项不为 null。此外,构造函数注入的组件始终以完全初始化的状态返回给客户端(调用)代码。顺便提一句,大量的构造函数参数是一种不好的代码气味,这意味着该类可能具有太多职责,并且应该重构以更好地解决适当的关注点分离。

设置程序注入主要应仅用于可选依赖项,这些依赖项可以在类中分配合理的默认值。否则,必须在代码使用依赖项的任何位置执行非空检查。设置程序注入的一个好处是,设置程序方法使该类的对象易于在以后重新配置或重新注入。因此,通过 JMX MBean 进行管理是设置程序注入的一个引人注目的用例。

使用对特定类最有意义的 DI 样式。有时,在处理您没有源代码的第三方类时,选择是为您做出的。例如,如果第三方类不公开任何设置程序方法,那么构造函数注入可能是唯一可用的 DI 形式。

依赖项解析过程

容器按如下方式执行 Bean 依赖项解析

  • 创建 ApplicationContext 并使用描述所有 Bean 的配置元数据对其进行初始化。配置元数据可以通过 XML、Java 代码或注释指定。

  • 对于每个 Bean,其依赖项以属性、构造函数参数或静态工厂方法的参数的形式表示(如果您使用它而不是普通构造函数)。在实际创建 Bean 时,会向 Bean 提供这些依赖项。

  • 每个属性或构造函数参数都是要设置的值的实际定义,或对容器中另一个 Bean 的引用。

  • 每个作为值的属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的数值转换为所有内置类型,例如 intlongStringboolean 等。

Spring 容器在创建容器时验证每个 Bean 的配置。但是,Bean 属性本身直到实际创建 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 的引用和循环依赖项。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 属性),因此我们在此不讨论这些详细信息。