1.4 依赖项
典型的企业应用程序不包含单个对象(或Spring术语中的bean)。即使是最简单的应用程序也有一些对象可以一起工作。下一节将解释如何从定义一些独立的bean定义到一个完全实现的应用程序,在该应用程序中对象协作以实现一个目标。
1.4.1 依赖注入
依赖项注入(DI)是一个过程,对象通过构造函数参数、工厂方法的参数或从工厂构造方法等来定义它们的依赖项(即它们使用的其他对象),然后,容器在创建bean时注入这些依赖项。这个过程基本上是bean本身的逆过程(因此称为控制反转),通过直接构造类或服务定位器模式控制bean依赖项的实例化或位置。
使用DI原理,代码更干净,当对象具有依赖关系时,去耦合更有效。对象不查找其依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖于接口或抽象基类(允许在单元测试中使用存根或模拟实现)时。
DI有两个主要的变体:基于构造函数的依赖注入和基于setter的依赖注入。
基于构造函数的依赖注入
基于构造函数的DI是通过容器调用带有多个参数的构造函数来实现的,每个参数表示一个依赖项。调用带有特定参数的静态工厂方法来构造bean几乎也是这个原理,本讨论将处理构造函数和静态工厂方法的参数。下面的示例显示了一个只能通过构造函数注入依赖项的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private 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...
}
注意这类没有什么特别的,只是一个POJO,不依赖于特定于容器的接口、基类或注释。
构造函数使用ref
用参数的类型进行构造函数参数解析匹配,考虑以下类:
package x.y;
public class ThingOne {
public 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无法确定value的类型,因此在没有帮助的情况下无法按类型匹配。考虑以下类:
package examples;
public class ExampleBean {
// 计算最终答案的年数
private int years;
// 关于生命、宇宙和一切的答案
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
构造函数用参数type匹配
在前面的场景中,如果使用type属性显式指定构造函数参数的类型,则容器可以使用与简单类型匹配的类型。如下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
构造函数使用参数编号
如下:
<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可以从构造函数中查找参数名。如果无法或不希望使用调试标志编译代码,可以使用JDK的@ConstructorProperties注释显式命名构造函数参数。如下示例类所示:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于setter的依赖注入
基于setter的DI是由容器在调用无参数构造函数或无参数静态工厂方法实例化bean之后调用bean上的setter方法来完成的。
下面的示例显示了一个类,该类只能通过使用纯setter注入来注入依赖项。这个类是传统的Java。它是一个POJO,不依赖于特定于容器的接口、基类或注释。
public class SimpleMovieLister {
// SimpleMovieLister类依赖于MovieFinder类
private MovieFinder movieFinder;
//通过setter方法注入MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 省略了实际使用注入的movieFinder的业务逻辑。..
}
ApplicationContext支持基于构造函数和基于setter的DI。在通过构造函数方法注入依赖项之后,它还支持基于setter的DI。你可以用BeanDefinition的形式配置依赖项,但大多数Spring用户使用基于XML的bean定义、基于注解的(如用@Component、@ Controller等来注释的类),或基于用@Configuration注解的JAVA类中用@Bean注解的方法。然后,这些源在内部转换为BeanDefinition实例,并用于加载整个Spring IOC容器实例。
使用基于构造函数还是基于setter的DI?
由于您可以混合使用基于构造函数和基于setter的DI,最佳实践是:所以对于强制依赖项使用构造函数方法,对于可选依赖项则使用setter方法或配置方法。注意,可以在setter方法上使用@Required注释使属性成为必需的依赖项;但是,最好使用能进行参数验证的构造函数注入。
Spring团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变的对象,并确保所需的依赖项不为空。此外,构造函数注入的组件总是以完全初始化的状态返回到客户端。另一点需要注意的是,大量的构造函数参数是一种糟糕的代码习惯,这意味着类可能有太多的责任,应该重构。
setter注入应该主要用于可自动分配合理默认值的可选依赖项。否则,在代码使用依赖项的任何地方都必须执行非空检查。setter注入的一个好处是setter方法使该类的对象能够在以后重新配置或重新注入。因此,通过JMX MBeans进行管理是setter注入的一个引人注目的用例。
对不同的类,使用最适合的DI方法。例如,如果第三方类不公开任何setter方法,那么构造函数注入可能是DI的唯一可用形式。
依赖关系解决过程
容器执行bean依赖性解析,如下所示:
- ApplicationContext根据配置元数据创建和初始化对外象,配置元数据包括XML、Java代码或注释。
- 对于每个bean,其依赖项都以属性、构造函数参数或静态工厂方法的参数的形式表示(如果使用该方法而不是普通的构造函数)。当bean创建时,这些依赖项被提供给bean。
- 每个属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,如int、long、string、boolean等。
Spring容器在创建容器时验证每个bean的配置。但是,在bean实际创建之前,不会设置bean属性本身。创建容器时,单实例(默认)Bean会被创建并设置为预实例化。否则,只有在请求bean时才创建它。创建bean可能会导致创建bean图,因为bean的依赖项及其依赖项的依赖项(等等)是被创建和分配的。请注意,这些依赖项之间的解析不匹配问题可能会很晚出现,也就是说,在第一次创建bean时。
循环依赖
如果类A通过构造函数注入需要类B的实例,类B通过构造函数注入需要类A的实例。那么Spring IOC容器在运行时检测到这个循环引用,并抛出beanCurrentlyIncreationException。
一种可能的解决方案是编辑一些类的源代码,这些类由setter而不是构造函数配置。或者,避免构造函数注入,只使用setter注入。换句话说,尽管不建议这样做,但是可以使用setter注入配置循环依赖项。
与典型情况(没有循环依赖性)不同,bean A和bean B之间的循环依赖性强制将其中一个bean注入另一个bean,然后完全初始化自己(经典的鸡和蛋场景)。
一般来说,你可以相信Spring会做正确的事。它在容器加载时检测配置问题,例如对不存在的bean和循环依赖项的引用。Spring在实际创建bean时设置属性并尽可能晚地解析依赖项。这意味着,如果在创建对象或其依赖项时出现问题,则在稍后请求对象时出现异常-例如,bean由于缺少或无效的属性而引发异常。在实际需要这些bean之前,您需要花费一些前期时间和内存来创建它们,但在创建ApplicationContext时,您会发现配置问题,而不是稍后。您仍然可以重写这个默认行为,以便单例bean可以延迟初始化,而不是预先实例化。
如果不存在循环依赖关系,当一个或多个协作bean被注入依赖bean时,每个协作bean在被注入依赖bean之前都被完全配置。这意味着,如果bean A依赖于bean B,那么在bean A上调用setter方法之前,Spring IOC容器会完全配置bean B。换句话说,bean被实例化(如果它不是预实例化的singleton),它的依赖项将被设置,以及相关的生命周期方法(例如已配置的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"/>
相应的JAVA类ExampleBean:
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;
}
}
在前面的示例中,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"/>
相应的JAVA类ExampleBean:
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;
}
}
bean定义中指定的构造函数参数用作ExampleBean的构造函数的参数。
现在考虑这个例子的一个变体,在这里,Spring不是使用构造函数,而是被告知调用一个静态工厂方法来返回对象的一个实例:
<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"/>
相应的JAVA类ExampleBean:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// 静态工厂方法;无论这些参数实际如何使用,此方法的参数都可以被视为返回的bean的依赖项。
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}