依赖注入

依赖注入(DI)是一种过程,其中对象仅通过构造函数参数、工厂方法的参数或在构造或从工厂方法返回对象实例后设置在对象实例上的属性来定义它们的依赖关系(即,它们使用的其他对象)。然后容器在创建bean时注入这些依赖关系。这个过程基本上是bean本身控制其依赖项的实例化或位置的反向(因此称为控制反转),而不是通过直接构造类或使用服务定位器模式。

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

基于构造函数的依赖注入

通过容器调用具有代表依赖关系的参数数量的构造函数来实现基于构造函数的DI。调用具有特定参数的static工厂方法来构造bean几乎是等效的,本讨论将构造函数和static工厂方法的参数视为相似。以下示例显示了一个只能通过构造函数注入依赖项的类:

  • Java

  • Kotlin

public class SimpleMovieLister {

	// SimpleMovieLister依赖于MovieFinder
	private final MovieFinder movieFinder;

	// 一个构造函数,以便Spring容器可以注入MovieFinder
	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// 省略了实际使用注入的MovieFinder的业务逻辑...
}
// 一个构造函数,以便Spring容器可以注入MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
	// 省略了实际使用注入的MovieFinder的业务逻辑...
}

请注意,这个类没有任何特殊之处。它是一个普通的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 {

	// 计算终极答案的年数
	private final int years;

	// 生活、宇宙和一切的答案
	private final String ultimateAnswer;

	public ExampleBean(int years, String ultimateAnswer) {
		this.years = years;
		this.ultimateAnswer = ultimateAnswer;
	}
}
package examples

class ExampleBean(
	private val years: Int, // 计算终极答案的年数
	private val ultimateAnswer: String // 生活、宇宙和一切的答案
)
构造函数参数类型匹配

在上述场景中,如果您通过使用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>

请注意,为了使其立即生效,您的代码必须使用启用了调试标志的编译,以便Spring可以从构造函数中查找参数名称。如果您无法或不想使用启用了调试标志的编译代码,可以使用@ConstructorProperties JDK注解来显式命名构造函数参数。然后,示例类将如下所示:

  • Java

  • Kotlin

package examples;

public class ExampleBean {

	// 省略字段

	@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-based DI是通过容器在调用无参数构造函数或无参数static工厂方法实例化bean后,在bean上调用setter方法来实现的。

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

  • Java

  • Kotlin

public class SimpleMovieLister {

	// SimpleMovieLister依赖于MovieFinder
	private MovieFinder movieFinder;

	// 一个setter方法,以便Spring容器可以注入MovieFinder
	public void setMovieFinder(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// 实际使用注入的MovieFinder的业务逻辑被省略...
}
class SimpleMovieLister {

	// 一个延迟初始化的属性,以便Spring容器可以注入MovieFinder
	lateinit var movieFinder: MovieFinder

	// 实际使用注入的MovieFinder的业务逻辑被省略...
}

ApplicationContext支持构造函数和setter的依赖注入,用于管理其管理的bean。它还支持在通过构造函数方法注入一些依赖项后,使用setter注入。您可以将依赖项配置为BeanDefinition的形式,然后与PropertyEditor实例一起使用,将属性从一种格式转换为另一种格式。但是,大多数Spring用户不直接使用这些类(即以编程方式),而是使用XML bean定义、带注解的组件(即使用@Component@Controller等注解的类)或Java-based @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被实例化(如果不是预实例化的单例),其依赖项被设置,并且相关的生命周期方法(例如配置的初始化方法InitializingBean回调方法)被调用。

依赖注入的示例

以下示例使用基于XML的配置元数据进行基于setter的依赖注入。Spring XML配置文件的一小部分指定了一些bean定义,如下所示:

<bean id="exampleBean" class="examples.ExampleBean">
	<!-- 使用嵌套的ref元素进行setter注入 -->
	<property name="beanOne">
		<ref bean="anotherExampleBean"/>
	</property>

	<!-- 使用更简洁的ref属性进行setter注入 -->
	<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文件中指定的属性。以下示例使用基于构造函数的依赖注入:

<bean id="exampleBean" class="examples.ExampleBean">
	<!-- 使用嵌套的ref元素进行构造函数注入 -->
	<constructor-arg>
		<ref bean="anotherExampleBean"/>
	</constructor-arg>

	<!-- 使用更简洁的ref属性进行构造函数注入 -->
	<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 {

	// 一个私有构造函数
	private ExampleBean(...) {
		...
	}

	// 一个静态工厂方法;该方法的参数可以被视为返回的对象的依赖项,
	// 无论这些参数实际上如何使用。
	public static ExampleBean createInstance (
		AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

		ExampleBean eb = new ExampleBean (...);
		// 一些其他操作...
		return eb;
	}
}
class ExampleBean private constructor() {
	companion object {
		// 一个静态工厂方法;该方法的参数可以被视为返回的对象的依赖项,
		// 无论这些参数实际上如何使用。
		@JvmStatic
		fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
			val eb = ExampleBean (...)
			// 一些其他操作...
			return eb
		}
	}
}

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