跳到主要内容

依赖注入

DeepSeek V3 中英对照 Dependency Injection

依赖注入(Dependency Injection,简称 DI)是一个过程,通过这个过程,对象仅通过构造函数参数、工厂方法的参数或在对象实例构造或从工厂方法返回后设置的属性来定义它们的依赖(即它们所协作的其他对象)。然后,容器在创建 bean 时注入这些依赖项。这个过程本质上是 bean 自身通过直接构造类或使用服务定位器模式来控制其依赖项的实例化或定位的反向过程(因此得名控制反转,Inversion of Control)。

通过依赖注入(DI)原则,代码变得更加清晰,当对象被提供其依赖时,解耦更加有效。对象不会查找其依赖项,也不知道依赖项的位置或类。因此,你的类变得更加容易测试,特别是当依赖项是基于接口或抽象基类时,这使得在单元测试中可以使用存根或模拟实现。

基于构造函数的依赖注入

基于构造函数的依赖注入是通过容器调用带有多个参数的构造函数来实现的,每个参数代表一个依赖项。调用带有特定参数的 static 工厂方法来构造 bean 几乎是等价的,这里的讨论将构造函数参数和 static 工厂方法参数视为类似。以下示例展示了一个只能通过构造函数注入进行依赖注入的类:

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...
}
java

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

构造函数参数解析

构造器参数解析匹配是通过使用参数的类型来进行的。如果一个 bean 定义的构造器参数中不存在潜在的歧义,那么在 bean 定义中构造器参数的顺序就是当 bean 被实例化时这些参数被提供给相应构造器的顺序。考虑以下类:

package x.y;

public class ThingOne {

public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
java

假设 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>
xml

当引用另一个 bean 时,类型是已知的,并且可以进行匹配(如前例所示)。当使用简单类型时,例如 <value>true</value>,Spring 无法确定值的类型,因此在没有帮助的情况下无法通过类型进行匹配。考虑以下类:

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;
}
}
java

构造函数参数类型匹配

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

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

构造函数参数索引

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

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

除了解决多个简单值的歧义外,指定索引还可以解决构造函数具有两个相同类型参数时的歧义。

备注

索引从 0 开始。

构造函数参数名称

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

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

请注意,为了使此功能开箱即用,您的代码必须在启用 -parameters 标志的情况下进行编译,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用 -parameters 标志编译代码,可以使用 @ConstructorProperties JDK 注解来显式命名构造函数参数。那么示例类将如下所示:

package examples;

public class ExampleBean {

// Fields omitted

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

基于 Setter 的依赖注入

基于 Setter 的依赖注入是通过容器在调用无参构造函数或无参 static 工厂方法实例化 bean 后,调用 bean 的 setter 方法来实现的。

以下示例展示了一个只能通过纯 setter 注入进行依赖注入的类。这个类是传统的 Java 类。它是一个 POJO,不依赖于容器特定的接口、基类或注解。

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...
}
java

ApplicationContext 支持基于构造函数和基于 setter 的依赖注入(DI)来管理它所管理的 bean。它还支持在通过构造函数方法注入了一些依赖项之后,再进行基于 setter 的依赖注入。你可以以 BeanDefinition 的形式配置依赖项,并将其与 PropertyEditor 实例结合使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接(即通过编程方式)使用这些类,而是使用 XML bean 定义、注解组件(即带有 @Component@Controller 等注解的类)或基于 Java 的 @Configuration 类中的 @Bean 方法。这些配置源随后会在内部转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例。

基于构造函数的还是基于 setter 的依赖注入?

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

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

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

为特定类选择最有意义的依赖注入风格。有时,当你处理没有源代码的第三方类时,选择已经为你做好了。例如,如果第三方类没有暴露任何 setter 方法,那么构造函数注入可能是唯一可用的依赖注入形式。

依赖解析过程

容器执行 bean 依赖解析的步骤如下:

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

  • 对于每个 bean,它的依赖关系以属性、构造函数参数或静态工厂方法参数的形式表达(如果你使用静态工厂方法而不是普通构造函数)。当 bean 实际创建时,这些依赖关系会提供给 bean。

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

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

Spring 容器在创建时会验证每个 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 之前花费一些前期时间和内存来创建它们,您可以在 ApplicationContext 创建时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,使单例 Bean 延迟初始化,而不是急切地预实例化。

如果不存在循环依赖,当一个或多个协作 Bean 被注入到依赖的 Bean 中时,每个协作 Bean 在注入到依赖的 Bean 之前都会被完全配置。这意味着,如果 Bean A 依赖于 Bean B,Spring IoC 容器会在调用 Bean A 的 setter 方法之前完全配置 Bean B。换句话说,Bean 会被实例化(如果它不是预实例化的单例),其依赖项会被设置,并且相关的生命周期方法(例如配置的初始化方法InitializingBean 回调方法)会被调用。

依赖注入的示例

以下示例使用基于 XML 的配置元数据进行基于 setter 的依赖注入。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"/>
xml

以下示例展示了对应的 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;
}
}
java

在前面的示例中,声明了 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"/>
xml

以下示例展示了对应的 ExampleBean 类:

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;
}
}
java

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

以下示例展示了相应的 ExampleBean 类:

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;
}
}
java

static 工厂方法的参数由 <constructor-arg/> 元素提供,这与实际使用构造函数时完全相同。工厂方法返回的类类型不必与包含 static 工厂方法的类类型相同(尽管在这个例子中是相同的)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean 属性而不是 class 属性),因此我们在这里不讨论这些细节。