|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
依赖注入
依赖注入(DI)是一种对象仅通过构造函数参数、工厂方法参数,或在对象实例被构造完成或从工厂方法返回后所设置的属性来定义其依赖关系(即它们所协作的其他对象)的过程。容器在创建 bean 时会注入这些依赖项。这一过程从根本上说是对 bean 自身通过直接构造类或使用服务定位器(Service Locator)模式来控制其依赖项的实例化或定位方式的反转(因此得名“控制反转”)。
通过依赖注入(DI)原则,代码变得更加简洁;当对象被提供其依赖项时,解耦效果也更为显著。对象无需主动查找其依赖项,也不需要知道依赖项的位置或具体类。因此,您的类变得更易于测试,尤其是在依赖项基于接口或抽象基类的情况下,这使得在单元测试中可以使用桩(stub)或模拟(mock)实现。
依赖注入(DI)主要有两种形式:基于构造函数的依赖注入 和 基于 Setter 的依赖注入。
基于构造器的依赖注入
基于构造函数的依赖注入(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...
}
请注意,这个类没有任何特殊之处。它是一个普通的 Java 对象(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)
假设 ThingTwo 和 ThingThree 类之间不存在继承关系,则不会产生潜在的歧义。因此,以下配置可以正常工作,您无需在 <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>
请注意,为了使此功能开箱即用,您的代码必须在启用调试标志(debug flag)的情况下进行编译,这样 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 对象)。
-
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 进行基于构造函数和基于设置器的依赖注入(DI)。它还支持在已通过构造函数方式注入部分依赖后,再进行基于设置器的依赖注入。您可以将依赖配置为 BeanDefinition 的形式,并结合 PropertyEditor 实例使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不会直接(即以编程方式)使用这些类,而是使用 XML bean 定义、带注解的组件(即使用 @Component、@Controller 等注解的类),或基于 Java 的 @Configuration 类中的 @Bean 方法。这些来源随后会在内部被转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例。
依赖解析过程
容器按如下方式执行 bean 的依赖解析:
-
ApplicationContext是通过配置元数据创建并初始化的,这些配置元数据描述了所有的 bean。配置元数据可以通过 XML、Java 代码或注解来指定。 -
对于每个 bean,其依赖项以属性、构造函数参数或静态工厂方法的参数(如果你使用静态工厂方法而非普通构造函数)的形式进行表达。当实际创建该 bean 时,这些依赖项会被提供给该 bean。
-
每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
-
每个属性或构造函数参数的值都会从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将字符串格式提供的值转换为所有内置类型,例如
int、long、String、boolean等。
Spring 容器在创建容器时会验证每个 bean 的配置。 然而,bean 的属性本身并不会被设置,直到该 bean 实际被创建时才进行赋值。 单例作用域(singleton-scoped)且设置为预实例化(pre-instantiated,默认行为)的 bean 会在容器创建时就被实例化。作用域的定义请参见 Bean 作用域。 否则,bean 仅在其被请求时才会被创建。创建一个 bean 可能会触发一个 bean 依赖图的创建, 因为该 bean 的依赖项、依赖项的依赖项(依此类推)都会被创建并注入。 请注意,这些依赖项之间的解析不匹配问题可能会较晚才暴露出来—— 也就是说,在首次创建受影响的 bean 时才会显现。
你通常可以信赖 Spring 做出正确的选择。它会在容器加载时检测配置问题,
例如引用不存在的 Bean 和循环依赖。Spring 在实际创建 Bean 时才会尽可能晚地设置属性并解析依赖关系。这意味着,一个已正确加载的 Spring 容器在您请求某个对象时,如果在创建该对象或其某个依赖项时出现问题(例如,由于缺少属性或属性无效,导致该 bean 抛出异常),则可能随后抛出异常。这就是为什么 ApplicationContext 的实现默认会预先实例化单例 bean,以避免某些配置问题的可见性可能被延迟。在创建这些bean之前投入一些初始时间来
分配内存,并且只有在需要它们时才实际使用它们,
你可以在创建ApplicationContext bean 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,使单例 Bean 延迟初始化,而不是被急切实例化。
如果没有循环依赖存在,当一个或多个协作 Bean 被注入到某个依赖 Bean 中时,每个协作 Bean 都会在被注入到依赖 Bean 之前完成全部配置。这意味着,如果 Bean A 依赖于 Bean B,Spring IoC 容器会在调用 Bean A 的 setter 方法之前,先完整地配置好 Bean B。换句话说,该 Bean 会被实例化(如果不是预先实例化的单例),其依赖项会被设置,并且会调用相关的生命周期方法(例如配置的初始化方法或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属性),因此我们在此不再赘述这些细节。