Dependency Injection
依赖注入 (DI) 是一种对象仅通过构造器参数、工厂方法的参数或设置在从工厂方法构造或返回后对象实例上的属性来定义其依赖关系(即它们协同工作的其他对象)的过程。然后,容器在创建 bean 时注入这些依赖关系。此过程从根本上说是 bean 本身控制其依赖关系的实例化或位置的逆过程(因此称为控制反转),而 bean 是通过直接构建类或服务定位器模式来实现此过程的。 DI 原则使得代码更加简洁,当对象得到它的依赖项时,解耦变得更加有效。该对象无需查找其依赖项,且不知道依赖项的位置或类。因此,您的类变得更易于测试,特别是在依赖项位于接口或抽象基类上时,允许在单元测试中使用存根或模拟实现。 DI 存在两种主要变体:"@72" 和 "@73"。
Constructor-based Dependency Injection
基于构造器的 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,与特定于容器的接口、基类或注释无关。
Constructor Argument Resolution
构造函数参数解析匹配是通过使用参数的类型发生的。如果 Bean 定义的构造函数参数中不存在潜在的歧义,则Bean 定义中定义的构造函数参数的顺序是将这些参数提供给适当的构造函数的顺序,当 Bean 被实例化时。考虑以下类:
-
Java
-
Kotlin
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
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
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;
}
}
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
)
在前述场景中,如果明确指定构造函数参数类型,容器可以使用类型匹配和简单类型,如以下示例所示:
<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>
请记住,为了使这项工作正常,你的代码必须在启用调试标志的情况下进行编译,以便 Spring 能够从构造函数中查找参数名称。如果你不能或不想在启用调试标志的情况下编译你的代码,可以使用 @ConstructorProperties JDK 注释来明确命名构造函数参数。然后,示例类的外观必须如下所示:
-
Java
-
Kotlin
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
Setter-based Dependency Injection
基于 Setter 的 DI 是通过在你的 bean 上调用 no-argument 构造器或 no-argument 静态工厂方法实例化你的 bean 后由容器调用 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
通过构造器和基于 setter 的 DI 来支持它管理的 bean。它还支持在已经通过构造器方式注入部分依赖项后基于 setter 的 DI。您可以通过 BeanDefinition
的形式来配置依赖项,这种方式需要结合使用 PropertyEditor
实例,以实现不同格式之间的属性转换。但是,大多数 Spring 用户不会直接(即通过编程方式)使用这些类,而是使用 XML bean
定义、带注释的组件(即用 @Component
、@Controller
等进行注释的类)或者在基于 Java 的 @Configuration
类中使用的 @Bean
方法。这些来源会被随后内部转换为 BeanDefinition
实例,用以加载整个 Spring IoC 容器实例。
由于你可以混合基于构造器的 DI 和基于调用的 DI,一个好的经验法则是使用构造器来处理强制性依赖关系,使用调用方法或配置方法来处理可选依赖关系。请注意,在调用方法上使用 @Autowired注释可用于将属性作为必需的依赖关系;但是,使用带有对参数进行编程验证的构造器注入是更可取的。
Spring 团队通常提倡构造器注入,因为它允许你将应用程序组件实现为不可变对象并确保所需依赖项不为 null
。此外,构造器注入的组件总以完全初始化的状态返回给客户端(调用)代码。附带说明一下,大量的构造器参数是一种糟糕的代码味道,这意味着该类可能具有过多的责任,并且应进行重构以更好地解决适当的关注点分离。
调用注入主要只能用于可选的依赖关系,这些依赖关系可以在类中指定合理默认值。否则,该代码使用依赖关系的任何地方都必须执行非空检查。调用注入的一个好处是,调用方法可以使该类的对象能够将来重新配置或重新注入。因此,通过 JMX MBeans进行管理是调用注入的一个有力的用例。
根据特定类最有意义的方式使用 DI 样式。有时,在处理没有源代码的第三方类时,这个选择已经为你做好了。例如,如果第三方类不暴露任何 setter 方法,则构造器注入可能是 DI 唯一可用的形式。
Dependency Resolution Process
容器如下执行 Bean 依赖项解析:
-
ApplicationContext
使用描述所有 Bean 的配置元数据创建并初始化。配置元数据可以用 XML、Java 代码或标注指定。 -
每个 Bean 的依赖关系都以属性的形式表示,或构造函数自变量的形式表示,或作为静态工厂函数的自变量的形式表示(如果你将其用于正常构造函数替代的话)。这些依赖关系在创建 Bean 时提供给 Bean。
-
每个属性或构造函数自变量都是设置值的实际定义,或对容器中其它 Bean 的引用。
-
每个作为值的属性或构造函数自变量从其指定格式转换为该属性或构造函数自变量的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,如
int
long
String
boolean
等。
Spring 容器在创建容器时会验证每个 bean 的配置。但是,只有在实际创建 bean 时才会设置 bean 属性。按默认进行预实例化(默认)并设置了单例作用域的 bean 会在创建容器时进行创建。作用域在 Bean Scopes中进行定义。否则,只有在需要时才会创建 bean。创建 bean 可能导致创建 bean 图,这是因为会创建并指定 bean 的依赖项及其依赖项的依赖项(依此类推)。请注意,这些依赖项之间的解析不匹配可能会在后期显示,也就是说,在首次创建受影响的 bean 时。
如果你主要使用构造器注入,则有可能创建一个无法解析的循环依赖项场景。
例如:类 A 通过构造器注入需要类 B 的实例,而类 B 通过构造器注入需要类 A 的实例。如果你配置 Bean 使类 A 和 B 可以相互注入,则 Spring IoC 容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException
。
一种可能的解决方案是编辑某些类利用 Setter 而非构造器配置的源代码。或者,避免构造器注入并仅使用 Setter 注入。换句话说,虽然不建议,但你可以通过 Setter 注入配置循环依赖项。
与典型情况(没有循环依赖项)不同,Bean A 和 Bean B 之间的循环依赖项会强制在 Bean 尚未完全初始化之前将其中一个 Bean 注入另一个(典型的先有鸡还是先有蛋的场景)。
你通常可以相信 Spring 会采取正确的操作。在容器加载时间,它会检测到诸如对不存在的 Bean 的引用和循环依赖项之类的配置问题。Spring 会尽可能晚地设置属性并解析依赖项,即在实际创建 Bean 时。这意味着,如果 Spring 容器加载正常,则在你请求某对象时可能会生成一个异常,如果创建该对象或其依赖项之一存在问题——例如,Bean 由于缺少或无效的属性而抛出异常。某些配置问题的这种潜在延迟可见性正是 ApplicationContext
实现默认预实例化单例 Bean 的原因。这样做的代价是在实际需要这些 Bean 之前就花费了一些时间和内存来创建这些 Bean,但你可以在创建 ApplicationContext
时发现配置问题,而不是稍后。你仍然可以覆盖此默认行为,以便单例 Bean 延迟初始化,而不是热切预实例化。
如果没有循环依赖项,当一个或多个协作 Bean 被注入到一个依赖 Bean 中时,每个协作 Bean 都会在注入到依赖 Bean 之前完全配置。这意味着,如果 Bean A 依赖于 Bean B,那么 Spring IOC 容器会在调用 Bean A 上的 setter 方法之前完全配置 Bean B。换句话说,Bean 被实例化(如果不是预例化的单例),它的依赖项被设置,并且会调用相关的生命周期方法(例如 "@74" 或 "@75")。
Examples of Dependency Injection
以下示例使用基于 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
属性之外),因此我们在此不讨论这些细节。