Skip to content

依赖注入 (Dependency Injection)

依赖注入 (DI) 是一个过程,通过该过程,对象仅通过构造函数参数、工厂方法参数,或在对象实例构造完成后(或从工厂方法返回后)通过属性设置来定义它们的依赖关系(即与之协同工作的其他对象)。然后由容器在创建 Bean 时注入这些依赖项。这个过程从根本上来说是“反向”的(因此得名“控制反转”),因为 Bean 本身不再通过直接构造类或使用“服务定位器 (Service Locator)”模式等机制来自行控制其依赖项的实例化或定位。

应用 DI 原则后,代码会变得更加简洁,当对象获取其依赖项时,解耦会更有效。对象不需要查找其依赖项,也不需要知道依赖项的位置或具体的类。结果,你的类变得更容易测试,特别是当依赖项基于接口或抽象基类时,这允许在单元测试中使用桩(Stub)或模拟(Mock)实现。

DI 主要有两种变体:基于构造函数的依赖注入基于 Setter 的依赖注入

基于构造函数的依赖注入 (Constructor-based DI)

基于构造函数的 DI 是通过容器调用带有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的 static 工厂方法来构造 Bean 几乎是等效的,本讨论将构造函数参数和 static 工厂方法参数同等对待。

以下示例展示了一个只能通过构造函数注入进行依赖注入的类:

java
public class SimpleMovieLister {

	// SimpleMovieLister 依赖于 MovieFinder
	private final MovieFinder movieFinder;

	// 构造函数,以便 Spring 容器可以注入 MovieFinder
	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// 实际使用注入的 MovieFinder 的业务逻辑已省略...
}
kotlin
// 构造函数,以便 Spring 容器可以注入 MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
	// 实际使用注入的 MovieFinder 的业务逻辑已省略...
}

请注意,这个类并没有什么特别之处。它是一个 POJO,不依赖于容器特定的接口、基类或注解。

构造函数参数解析

构造函数参数解析匹配是利用参数类型进行的。如果 Bean 定义的构造函数参数中不存在潜在的歧义,那么在 Bean 定义中定义构造函数参数的顺序,就是实例化 Bean 时将这些参数提供给适当构造函数的顺序。

考虑以下类:

java
package x.y;

public class ThingOne {

	public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
		// ...
	}
}
kotlin
package x.y

class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)

假设 ThingTwoThingThree 类之间没有继承关系,则不存在潜在的歧义。因此,以下配置可以正常工作,你不需要在 <constructor-arg/> 元素中显式指定构造函数参数索引或类型。

xml
<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
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;
	}
}
kotlin
package examples

class ExampleBean(
	private val years: Int, // 计算最终答案的年数
	private val ultimateAnswer: String // 生命、宇宙及万物的终极答案
)

构造函数参数类型匹配

在上述场景中,如果你通过 type 属性显式指定构造函数参数的类型,容器可以使用简单类型的类型匹配,如下例所示:

xml
<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg type="int" value="7500000"/>
	<constructor-arg type="java.lang.String" value="42"/>
</bean>

构造函数参数索引

你可以使用 index 属性显式指定构造函数参数的索引,如下例所示:

xml
<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg index="0" value="7500000"/>
	<constructor-arg index="1" value="42"/>
</bean>

除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数时的歧义。请注意,索引是从 0 开始的。

构造函数参数名称

你还可以使用构造函数参数名称进行值的去歧义,如下例所示:

xml
<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg name="years" value="7500000"/>
	<constructor-arg name="ultimateAnswer" value="42"/>
</bean>

请记住,要使其开箱即用,你的代码必须在启用 -parameters 标志的情况下进行编译,以便 Spring 可以从构造函数中查找参数名称。如果你不想编译代码时使用该标志,可以使用 JDK 的 @ConstructorProperties 注解来显式命名构造函数参数。

java
package examples;

public class ExampleBean {

	// 字段已省略

	@ConstructorProperties({"years", "ultimateAnswer"})
	public ExampleBean(int years, String ultimateAnswer) {
		this.years = years;
		this.ultimateAnswer = ultimateAnswer;
	}
}
kotlin
package examples

class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)

基于 Setter 的依赖注入 (Setter-based DI)

基于 Setter 的 DI 是由容器在调用无参数构造函数或无参数 static 工厂方法实例化 Bean 后,调用 Bean 上的 setter 方法来完成的。

以下示例展示了一个只能通过纯 setter 注入进行依赖注入的类:

java
public class SimpleMovieLister {

	// SimpleMovieLister 依赖于 MovieFinder
	private MovieFinder movieFinder;

	// setter 方法,以便 Spring 容器可以注入 MovieFinder
	public void setMovieFinder(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// 实际使用注入的 MovieFinder 的业务逻辑已省略...
}
kotlin
class SimpleMovieLister {

	// 延迟初始化的属性,以便 Spring 容器可以注入 MovieFinder
	lateinit var movieFinder: MovieFinder

	// 实际使用注入的 MovieFinder 的业务逻辑已省略...
}

ApplicationContext 支持其管理的 Bean 使用基于构造函数和基于 setter 的 DI。它还支持在通过构造函数方法注入了某些依赖项后,再进行基于 setter 的 DI。

构造函数注入还是 Setter 注入?

既然可以混合使用,一个好的经验法则是:对 强制性依赖项 使用构造函数,对 可选依赖项 使用 setter 方法或配置方法。

Spring 团队通常提倡 构造函数注入,因为它允许你将应用组件实现为不可变对象,并确保所需的依赖项不为 null。此外,构造函数注入的组件总是以完全初始化的状态返回给调用代码。

Setter 注入 应主要仅用于可选依赖项,这些依赖项可以在类中分配合理的默认值。Setter 注入的一个优点是,setter 方法使该类的对象可以在以后重新配置或重新注入。

依赖解析过程 (Dependency Resolution Process)

容器按以下步骤执行 Bean 依赖解析:

  1. ApplicationContext 使用描述所有 Bean 的配置元数据进行创建和初始化。
  2. 对于每个 Bean,其依赖关系以属性、构造函数参数或静态工厂方法参数的形式表示。当 Bean 实际创建 时,这些依赖项被提供给 Bean。
  3. 每个属性或构造函数参数既可以是设置值的实际定义,也可以是对容器中另一个 Bean 的引用。
  4. 值类型的参数将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型。

Spring 容器在创建容器时验证每个 Bean 的配置。但是,直到 Bean 实际创建 时才会设置 Bean 属性。单例作用域且设置为预实例化(默认)的 Bean 在创建容器时创建。否则,Bean 仅在被请求时才创建。

循环依赖 (Circular dependencies)

如果你主要使用构造函数注入,可能会创建一个无法解析的循环依赖场景。

例如:类 A 通过构造函数注入需要类 B 的实例,而类 B 通过构造函数注入需要类 A 的实例。如果你将类 A 和类 B 的 Bean 配置为互相注入,Spring IoC 容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException

解决办法: 一种可能的解决方案是修改某些类的源代码,通过 setter 而不是构造函数进行配置。或者,完全避免构造函数注入,仅使用 setter 注入。虽然不推荐,但你可以通过 setter 注入来配置循环依赖。

依赖注入示例 (Examples of DI)

基于 Setter 的 DI 示例

xml
<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"/>

基于构造函数的 DI 示例

xml
<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg ref="anotherExampleBean"/>
	<constructor-arg ref="yetAnotherBean"/>
	<constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

补充教学 —— 好莱坞原则与 DI

1. “别找我们,我们会找你” (The Hollywood Principle) 这正是 DI 的核心哲学。在传统开发中,你如果要用某个工具(依赖),得自己去仓库找。在 Spring 里,你只要在构造函数或 Setter 里留个位置,Spring 这个“选角导演”就会在演出(运行)开始前,把最合适的演员(Bean)塞进你的手里。

2. 不可变性 (Immutability) 与安全感 为什么 Spring 团队极力推崇构造函数注入?

  • 不可变性:通过将字段声明为 private final(Java)或在构造函数中初始化,可以确保对象一旦创建,其核心依赖就不会再改变。
  • 非空保证:在构造函数里你可以做参数校验 (Assert),保证 Bean 在走出大门时就是“健康”的。
  • 避免半成品:Setter 注入可能导致对象在被调用时,某个 Setter 还没被执行,出现 NullPointerException。

3. 处理循环依赖的“三级缓存” 虽然文档建议用 Setter 解决,但 Spring 内部通过极其精妙的 三级缓存 (Three-level Cache) 机制,在单例 (Singleton) 模式下默认就能解决大部分循环依赖问题。

  • 核心逻辑:Spring 允许在对象还没完全初始化好时,先把它的“半成品”(早期引用)暴露出来供别人引用。
  • 注意:这种“魔法”仅限于普通单例 Bean 之间的属性/Setter 注入。构造器注入产生的循环依赖是无解的,就像两个人必须同时拥抱着对方才能出生,逻辑上无法成立。

Based on Spring Framework.