跳到主要内容
版本:7.0.3

依赖注入

Hunyuan 7b 中英对照 Dependency Injection

依赖注入(Dependency Injection,简称DI)是一种编程方法,在这种方法中,对象仅通过构造函数参数、工厂方法的参数,或者在对象实例被创建后或从工厂方法返回后被设置的属性来定义其依赖关系(即它们所需要协同工作的其他对象)。然后,在创建该对象时,容器会负责注入这些依赖关系。从本质上讲,这一过程与对象自身使用直接构造类或服务定位器模式(Service Locator pattern)来控制其依赖关系的实例化或定位的方式正好相反(因此得名“控制反转”(Inversion of Control))。

使用依赖注入(DI)原则,代码会更加简洁,当对象附带其依赖项时,解耦效果也会更佳。对象无需自行查找其依赖项,也不需要知道这些依赖项的位置或类名。因此,你的类就更容易进行测试,尤其是当依赖项为接口或抽象基类时,因为在单元测试中可以使用存根(stub)或模拟(mock)实现来替代这些依赖项。

依赖注入(Dependency Injection,DI)主要存在两种形式:基于构造函数的依赖注入基于setter方法的依赖注入

基于构造函数的依赖注入

基于构造函数的依赖注入(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),不依赖于特定于容器的接口、基类或注解。

构造函数参数解析

构造函数参数的解析是通过使用参数的类型来完成的。如果bean定义中的构造函数参数不存在潜在的歧义,那么在实例化bean时,这些参数被提供给相应构造函数的顺序,就与在bean定义中定义这些参数的顺序相同。考虑以下类:

package x.y;

public class ThingOne {

public 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无法确定该值的类型,因此在没有帮助的情况下无法通过类型进行匹配。考虑以下类:

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

构造函数参数类型匹配

在之前的场景中,如果通过 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 注解来明确指定构造函数的参数名称。那么示例类的格式将如下所示:

package examples;

public class ExampleBean {

// Fields omitted

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

基于setter的依赖注入

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

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

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

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

基于构造函数的依赖注入(DI)还是基于setter方法的依赖注入?

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

Spring团队通常提倡使用构造函数注入,因为它可以让你将应用程序组件实现为不可变对象,并确保必需的依赖关系不会为null。此外,通过构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。顺便提一下,过多的构造函数参数是一种不良的代码习惯,这表明该类可能职责过多,应该进行重构以更好地实现职责分离。

setter注入主要应仅用于那些可以在类内部赋予合理默认值的可选依赖关系。否则,在代码使用该依赖关系的每个地方都必须进行非空检查。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 Scopes中有定义。否则,bean仅会在被请求时才被创建。bean的创建可能会引发一系列bean的连锁创建,因为bean的依赖项以及这些依赖项的依赖项(依此类推)也会被创建并赋值。需要注意的是,这些依赖项之间的解析不匹配问题可能会在后期才显现出来——也就是说,在受影响的bean首次被创建时才会出现问题。

循环依赖

如果你主要使用构造函数注入(constructor injection),就有可能出现无法解决的循环依赖情况。

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

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

与典型的情况(没有循环依赖)不同,在bean A和bean B之间存在循环依赖时,其中一个bean必须在完全初始化之前就被注入到另一个bean中(这就是典型的“先有鸡还是先有蛋”的问题)。

你通常可以信任Spring会做出正确的处理。它会在容器加载时检测配置问题,例如引用不存在的bean或存在循环依赖的情况。Spring会在bean实际被创建时才设置属性和解析依赖关系。这意味着,如果某个bean的创建或其依赖关系的处理过程中出现问题(例如,由于缺少或无效的属性导致bean抛出异常),那么一个已经正确加载的Spring容器在后续请求该对象时仍可能生成异常。正是由于某些配置问题的可见性可能会被延迟,因此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的依赖注入(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类:

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

在前面的例子中,设置器(setters)被声明为与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类:

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工厂方法来返回对象的实例:

<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 类:

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

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