1.4.1. 依赖注入
依赖注入 (DI) 是一个过程,对象仅通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身通过使用直接构造类或服务定位器模式来控制其依赖项的实例化或位置的逆过程(因此称为控制反转)。
使用 DI 原则,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。
DI 存在两个主要变体:基于构造函数的依赖注入和基于 Setter 的依赖注入。
基于构造函数的依赖注入
基于构造函数的 DI 是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的static
工厂方法来构造 bean 几乎是等价的,本次讨论将类似地对待构造函数和static
工厂方法的参数。以下示例显示了一个只能通过构造函数注入进行依赖注入的类:
请注意,这个类没有什么特别之处。它是一个 POJO,不依赖于容器特定的接口、基类或注解。
构造函数参数解析
构造函数参数解析匹配通过使用参数的类型进行。如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当构造函数的顺序。考虑以下类:
假设ThingTwo
和ThingThree
类不通过继承相关,则不存在潜在的歧义。因此,以下配置工作正常,您无需在 <constructor-arg/>
元素中显式指定构造函数参数索引或类型。
当引用另一个 bean 时,类型是已知的,并且可以发生匹配(就像前面的示例一样)。当使用简单类型时,如 <value>true</value>
,Spring 无法确定值的类型,因此无法在没有帮助的情况下按类型匹配。例如以下类:
构造函数参数类型匹配
在上述场景中,如果您通过属性type
显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如以下示例所示:
构造函数参数下标
您可以使用index
属性显式指定构造函数参数的下标,如以下示例所示:
除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数的歧义。
下标从 0 开始。
构造函数参数名称
您还可以使用构造函数参数名称进行值消歧,如以下示例所示:
请记住,要使这项工作开箱即用,您的代码必须在启用调试标志的情况下编译,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用调试标志编译代码,则可以使用 JDK 注解@ConstructorProperties 显式命名构造函数参数。示例类必须如下所示:
基于 Setter 的依赖注入
基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数static
工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。
以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。这个类是传统的Java。它是一个 POJO,不依赖于容器特定的接口、基类或注解。
ApplicationContext
管理的bean 支持基于构造函数和基于 setter 的 DI。在已经通过构造方法注入了一些依赖项之后,它还支持基于 setter 的 DI。您以 BeanDefinition
的形式配置依赖项,您可以将其与PropertyEditor
实例结合使用以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即以编程方式),而是使用 XMLbean
定义、带注解的组件(即,使用@Component
、 @Controller
等注解的类)或基于 Java 的@Configuration
类中的@Bean
方法。然后这些源在内部转换为实例BeanDefinition
并用于加载整个 Spring IoC 容器实例。
基于构造函数还是基于 setter 的 DI?
由于您可以混合使用基于构造函数和基于 setter 的 DI,因此将构造函数用于强制依赖项并将 setter 方法或配置方法用于可选依赖项是一个很好的经验法则。请注意,在 setter 方法上使用@Required 注解可用于使属性成为必需的依赖项;然而,带有参数的编程验证的构造函数注入是更可取的。
Spring 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不是null
. 此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。附带说明一下,大量的构造函数参数是一种不好的代码气味,这意味着该类可能有太多的职责,应该重构以更好地解决适当的关注点分离问题。
Setter 注入应该主要只用于可以在类中分配合理默认值的可选依赖项。否则,必须在代码使用依赖项的任何地方执行非空检查。setter 注入的一个好处是 setter 方法使该类的对象可以在以后重新配置或重新注入。因此,通过JMX MBean进行管理是 setter 注入的一个引人注目的用例。
使用对特定类最有意义的 DI 样式。有时,在处理您没有源代码的第三方类时,会为您做出选择。例如,如果第三方类没有公开任何 setter 方法,那么构造函数注入可能是 DI 的唯一可用形式。
依赖解决过程
容器执行 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类的实例。如果你为类 A 和 B 配置 bean 以相互注入,Spring IoC 容器会在运行时检测到这个循环引用,并抛出一个 BeanCurrentlyInCreationException
.
一种可能的解决方案是编辑某些类的源代码以由设置器而不是构造器配置。或者,避免构造函数注入并仅使用 setter 注入。也就是说,虽然不推荐,但是可以通过setter注入来配置循环依赖。
与典型情况(没有循环依赖关系)不同,bean A 和 bean B 之间的循环依赖关系强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。
您通常可以相信 Spring 会做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 和循环依赖项的引用。在实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项之一时出现问题,则正确加载的 Spring 容器稍后可以在您请求对象时生成异常——例如,bean 由于某个丢失或无效的属性而引发异常。某些配置问题的这种潜在延迟可见性就是为什么ApplicationContext
默认情况下,实现预实例化单例 bean。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在ApplicationContext
创建这些 bean 时发现配置问题,而不是之后。您仍然可以覆盖此默认行为,以便单例 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 定义,如下所示:
以下示例显示了相应的ExampleBean
类:
在前面的示例中,setter 被声明为与 XML 文件中指定的属性相匹配。以下示例使用基于构造函数的 DI:
以下示例显示了相应的ExampleBean
类:
bean 定义中指定的构造函数参数用作ExampleBean
.
现在考虑这个例子的一个变体,其中,Spring 被告知调用static
工厂方法来返回对象的实例,而不是使用构造函数:
以下示例显示了相应的ExampleBean
类:
工厂方法的参数static
由<constructor-arg/>
元素提供,就像实际使用了构造函数一样。工厂方法返回的类的类型不必与包含static
工厂方法的类的类型相同(尽管在本例中是这样)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用factory-bean
属性而不是class
属性),因此我们不在这里讨论这些细节。
最后更新于