依赖注入

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

采用 DI 原则,代码更清晰,当对象提供其依赖项时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖项是接口或抽象基类时,这允许在单元测试中使用存根或模拟实现。

基于构造函数的依赖注入

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

构造函数参数解析

构造函数参数解析匹配通过使用参数的类型进行。如果 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)

假设 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 无法确定值的类型,因此无法在没有帮助的情况下按类型匹配。考虑以下类:

  • 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>

请记住,为了使此功能开箱即用,您的代码必须使用 -parameters 标志进行编译,以便 Spring 可以从构造函数中查找参数名称。如果您无法或不想使用 -parameters 标志编译代码,则可以使用 @ConstructorProperties JDK 注解来明确命名您的构造函数参数。示例类 then 必须如下所示:

  • 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

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

基于构造函数还是基于 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 实际创建之前不会设置。单例作用域并设置为预实例化(默认)的 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 被实例化(如果它不是预实例化的单例),其依赖项被设置,并且调用相关的生命周期方法(例如 配置的 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 类:

  • 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 属性),因此我们在此不讨论这些细节。

© . This site is unofficial and not affiliated with VMware.