依赖注入

依赖注入(DI)是一个过程,其中对象仅通过构造函数参数、工厂方法参数或在对象实例构造后或从工厂方法返回后设置的属性来定义它们的依赖项(即它们与之协作的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程本质上是与 bean 自己通过直接构建类或使用服务定位器模式来控制其依赖项的实例化或位置相反的(因此得名:控制反转)。遵循 DI 原则使代码更整洁,并且在为对象提供依赖项时,解耦更有效。对象不会查找其依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,尤其当依赖项是接口或抽象基类时,这允许在单元测试中使用桩(stub)或模拟(mock)实现。

运用依赖注入(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>

请记住,为了使此功能开箱即用,您的代码必须使用启用 -parameters 标志进行编译,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用 -parameters 标志编译代码,您可以使用 @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?

由于可以混合使用基于构造函数和基于 Setter 的 DI,一个好的经验法则是对强制依赖项使用构造函数,对可选依赖项使用 setter 方法或配置方法。请注意,在 setter 方法上使用 @Autowired 注解可以使该属性成为必需的依赖项;然而,带有参数编程验证的构造函数注入更可取。

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

Setter 注入主要应仅用于可在类内分配合理默认值的可选依赖项。否则,代码中使用依赖项的任何地方都必须执行非 null 检查。Setter 注入的一个好处是 setter 方法使得该类的对象可以在稍后重新配置或重新注入。因此,通过 JMX MBeans 进行管理是使用 setter 注入的一个令人信服的用例。

为特定类选择最适合的 DI 风格。有时,在处理没有源代码的第三方类时,选择是为您做出的。例如,如果第三方类没有暴露任何 setter 方法,则构造函数注入可能是唯一可用的 DI 形式。

依赖解析过程

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

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

  • 对于每个 bean,其依赖项以属性、构造函数参数或静态工厂方法参数(如果您使用它代替普通构造函数)的形式表达。当 bean 实际创建时,将这些依赖项提供给 bean。

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

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

Spring 容器在创建时会验证每个 bean 的配置。然而,bean 属性本身直到 bean 实际创建时才设置。作用域为 singleton 并设置为预实例化(默认)的 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 实现默认预实例化 singleton bean 的原因。虽然在实际需要这些 bean 之前会花费一些前期时间和内存来创建它们,但您可以在创建 ApplicationContext 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便 singleton bean 延迟初始化,而不是被急切地预实例化。

如果不存在循环依赖,当一个或多个协作 bean 被注入到依赖 bean 中时,每个协作 bean 在被注入到依赖 bean 之前都已完全配置。这意味着,如果 bean A 依赖于 bean B,Spring IoC 容器会在调用 bean A 的 setter 方法之前完全配置 bean B。换句话说,bean 被实例化(如果它不是预实例化的 singleton),其依赖项被设置,并且相关的生命周期方法(例如 配置的 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 属性),因此我们在此不讨论这些细节。