7.4.1 依赖注入

依赖注入(DI)是对象借以定义它们的依赖也就是一起工作的其他对象的过程,该过程只能是通过构造方法传参、或者通过工厂方法传参、或者通过给实例化或工厂方法返回的对象设置属性来完成。然后当创建bean时,容器将这些依赖注入其中。这样的过程,和由bean自己通过直接使用类的构造方法或利用Service Locator模式等机制来掌控依赖的实例化或位置,是本质上的逆转,故而命名为控制反转(IoC)。

应用DI原则可以使代码更加整洁,而且对象与它的依赖一起提供可以更有效地解耦。对象不去寻找它的依赖,也不知道依赖在哪里或依赖什么类。这样,您的类变得更容易测试,特别是当依赖于接口或抽象基类时可以在单元测试中使用stub或mock实现。

DI有两种主要变体,基于构造方法的依赖注入和基于setter方法的依赖注入。

基于构造方法的依赖注入

基于构造方法的DI由容器调用一个构造方法来完成,构造方法还有一些参数,每个参数表示一个依赖。调用一个带有指定参数的静态工厂方法来构造bean几乎是相同的,在这个讨论中,会同等对待赋给构造方法的参数和赋给静态工厂方法的参数。下面的例子展示了一个只能通过构造方法依赖注入的类。请注意,这个类没有任何特别之处,它是一个不依赖于容器专有接口、基类或注解的POJO。

  1. public class SimpleMovieLister {
  2. // the SimpleMovieLister has a dependency on a MovieFinder
  3. private MovieFinder movieFinder;
  4. // a constructor so that the Spring container can inject a MovieFinder
  5. public SimpleMovieLister(MovieFinder movieFinder) {
  6. this.movieFinder = movieFinder;
  7. }
  8. // business logic that actually uses the injected MovieFinder is omitted...
  9. }

构造方法参数解析

构造方法的参数解析使用参数的类型进行匹配。如果bean定义中构造方法的参数没有潜在歧义,bean定义中定义构造方法参数的顺序就是当bean被实例化时那些参数赋给相应的构造方法的顺序。考虑下面的类:

  1. package x.y;
  2. public class Foo {
  3. public Foo(Bar bar, Baz baz) {
  4. // ...
  5. }
  6. }

假定Bar类和Baz类不在一棵继承树上,则不存在潜在的歧义。因此,下面的配置可以正常工作,而且不需要在<constructor-arg/>元素中显式地指定构造方法参数的索引和/或类型。

  1. <beans>
  2. <bean id="foo" class="x.y.Foo">
  3. <constructor-arg ref="bar"/>
  4. <constructor-arg ref="baz"/>
  5. </bean>
  6. <bean id="bar" class="x.y.Bar"/>
  7. <bean id="baz" class="x.y.Baz"/>
  8. </beans>

当另一个bean被引用时,类型是已知的,匹配就能发生(与前面的例子情况一样)。当使用简单类型,比如<value>true</value>,Spring不能确定值的类型,所以若无帮助无法匹配。考虑下面的类:

  1. package examples;
  2. public class ExampleBean {
  3. // Number of years to calculate the Ultimate Answer
  4. private int years;
  5. // The Answer to Life, the Universe, and Everything
  6. private String ultimateAnswer;
  7. public ExampleBean(int years, String ultimateAnswer) {
  8. this.years = years;
  9. this.ultimateAnswer = ultimateAnswer;
  10. }
  11. }

在上面的场景中,如果使用type属性显式地指定构造方法参数的类型,那么容器对简单类型可以使用类型匹配。例如:

  1. <bean id="exampleBean" class="examples.ExampleBean">
  2. <constructor-arg type="int" value="7500000"/>
  3. <constructor-arg type="java.lang.String" value="42"/>
  4. </bean>

使用index属性显式指定构造方法参数的索引。例如:

  1. <bean id="exampleBean" class="examples.ExampleBean">
  2. <constructor-arg index="0" value="7500000"/>
  3. <constructor-arg index="1" value="42"/>
  4. </bean>

除了解决多个简单类型值的歧义性,指定索引还能解决构造方法具有两个相同类型参数的歧义性。需要注意,索引从0开始。

也可以使用构造方法参数的名字消除值的歧义性:

  1. <bean id="exampleBean" class="examples.ExampleBean">
  2. <constructor-arg name="years" value="7500000"/>
  3. <constructor-arg name="ultimateAnswer" value="42"/>
  4. </bean>

请记住,为了让这种方法立即起作用,代码必须在调试标志被启用的情况下编译,那样Spring就能从构造方法中查找参数名。如果不能(或者不想)带着调试标志编译代码,可以使用JDK注解@ConstructorProperties显式地命名构造方法的参数。示例类应该像这样:

  1. package examples;
  2. public class ExampleBean {
  3. // Fields omitted
  4. @ConstructorProperties({"years", "ultimateAnswer"})
  5. public ExampleBean(int years, String ultimateAnswer) {
  6. this.years = years;
  7. this.ultimateAnswer = ultimateAnswer;
  8. }
  9. }

基于setter的依赖注入

在容器调用无参的构造方法或无参的静态工厂方法实例化bean之后,容器会调用bean的setter方法来完成基于setter方法的DI。

下面的例子展示了一个只能使用纯setter注入完成依赖注入的类,这个类是常规的Java类,是一个不依赖于容器专有接口、基类或注解的POJO。

  1. public class SimpleMovieLister {
  2. // the SimpleMovieLister has a dependency on the MovieFinder
  3. private MovieFinder movieFinder;
  4. // a setter method so that the Spring container can inject a MovieFinder
  5. public void setMovieFinder(MovieFinder movieFinder) {
  6. this.movieFinder = movieFinder;
  7. }
  8. // business logic that actually uses the injected MovieFinder is omitted...
  9. }

ApplicationContext对它所管理的bean都提供了基于构造方法和基于setter方法的DI支持,还支持在部分依赖已经通过构造方法注入之后再使用基于setter方法的注入。

基于构造方法的DI还是基于setter方法的DI

由于您可以混合使用基于构造方法的DI和基于setter方法的DI,所以一个比较好的做法是对强制性依赖使用构造方法,对可选依赖使用setter方法或配置方法。请注意,在一个setter方法上使用@Required注解会使该属性成为一个必需的依赖。

Spring团队通常提倡构造方法注入,因为它使得人们可以将应用程序组件实现为不可变对象,并且确保所需的依赖不为null。此外,使用构造方法注入的组件总是以完全初始化的状态返回给客户端(调用)代码。提醒一下,大量的构造方法参数是一种烂代码,这意味着该类可能有过多的职责,为了更好地处理适当的关注分离应该对类进行重构。

setter注入应当仅用于类中那些能够被赋予合理默认值的可选依赖。否则,任何使用该依赖的代码都必须进行非空校验。setter注入的一个好处是setter方法使该类的对象可以在稍后重新配置或重新注入。通过JMX MBeans进行管理就是一个使用setter注入的例子。

使用对特定的类最有意义的DI方式。有时,当您处理没有源代码的第三方类时您要做出选择。举例来说,一个第三方的类没有暴露出任何setter方法,那么构造方法注入可能是DI唯一可用的形式。

依赖解析过程

容器完成bean依赖解析的过程如下:

  • 根据全部描述bean的配置元数据创建并初始化ApplicationContext,可以通过XML、Java代码或注解来指定配置元数据。
  • 对每个bean来说,它的依赖表现为属性、构造方法参数等形式,或者是当您使用静态工厂方法替代通常的构造方法时bean的依赖表现为静态工厂的参数。当bean被创建时,这些依赖会提供给bean。
  • 每个属性或构造方法参数都是一个实际的可以被设置的值或一个到容器中其它bean的引用。
  • 作为值的每个属性或构造方法参数从其指定的格式转换为属性或构造方法参数的实际类型。默认情况下,Spring可以将以字符串格式提供的值转换为所有的内置类型,如int,long,String,boolean等。

在Spring容器被创建的同时,它会校验每个bean的配置。然而,直到实际创建bean时,bean的属性才会被设置。单例作用域的bean和被设为预实例化的bean在创建容器时被创建(作用域在7.5 bean的作用域中定义),否则,bean仅在被请求时创建。一个bean的创建可能导致一系列bean的创建,因为bean的依赖以及依赖的依赖(以此类推)要被创建并赋值。需要注意的是,依赖中的解析不匹配可能会在稍后显现,比如在相关的bean首次创建时。

循环依赖

如果您在绝大多数情况下使用构造方法注入,那么有可能会创建出无法解析的循环依赖场景。

例如:A类通过构造方法注入需要一个B类的实例,而B类通过构造方法注入需要一个A类的实例。如果配置A类和B类的bean为彼此注入,Spring的IoC容器会在运行时检测到循环依赖,并抛出一个BeanCurrentlyInCreationException

一个可能的解决方法是修改某些类的源代码使之通过setter配置而不用构造方法。或者,禁用构造方法注入,仅使用setter注入。换句话说,尽管不推荐,但是可以通过setter注入配置循环依赖。

与典型情况(不存在循环依赖)不同,A类和B类bean之间的循环依赖强迫其中的一个bean在另一个bean完全初始化之前注入到该bean中(一个景点的鸡生蛋蛋生鸡问题)。

一般情况下,可以相信Spring。它在容器加载时期检测配置问题,例如引用不存在的bean和循环依赖。Spring会尽可能晚地——当bean被实际创建时——设置属性和解析依赖。这意味着,如果创建对象或对象的某一个依赖时存在问题,那么已经正确加载完毕的Spring容器会在您请求这个对象时产生一个异常。例如,bean由于一个缺失或无效的属性而抛出一个异常。像这样,可能一些配置问题会延迟暴露,这就是为什么默认情况下ApplicationContext要预实例化单例bean。在实际需要这些bean之前,以前期的时间和内存为代价创建它们,这样当ApplicationContext创建时就能发现配置问题。您也可以覆盖这种默认行为,以便单例bean延迟初始化,而不是预先实例化。

如果不存在循环依赖,当一个或多个协作的bean被注入到依赖它们的bean中时,每个协作的bean在注入前已经完全配置好了。这意味着,如果bean A依赖bean B,那么Spring的IoC容器会在调用bean A的setter方法前将bean B完全配置好。换句话说,bean会被实例化(如果该bean不是预实例化的单例),它的依赖会被设置,并且相关的生命周期方法(例如配置的初始化方法InitializingBean回调方法)被调用。

依赖注入的例子

下面的例子使用基于XML的配置元数据来设置基于setter方法的DI。Spring的XML配置文件的一小部分指定了一些bean的定义:

  1. <bean id="exampleBean" class="examples.ExampleBean">
  2. <!-- setter injection using the nested ref element -->
  3. <property name="beanOne">
  4. <ref bean="anotherExampleBean"/>
  5. </property>
  6. <!-- setter injection using the neater ref attribute -->
  7. <property name="beanTwo" ref="yetAnotherBean"/>
  8. <property name="integerProperty" value="1"/>
  9. </bean>
  10. <bean id="anotherExampleBean" class="examples.AnotherBean"/>
  11. <bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
  1. public class ExampleBean {
  2. private AnotherBean beanOne;
  3. private YetAnotherBean beanTwo;
  4. private int i;
  5. public void setBeanOne(AnotherBean beanOne) {
  6. this.beanOne = beanOne;
  7. }
  8. public void setBeanTwo(YetAnotherBean beanTwo) {
  9. this.beanTwo = beanTwo;
  10. }
  11. public void setIntegerProperty(int i) {
  12. this.i = i;
  13. }
  14. }

在上面的例子中,setter方法被声明去匹配XML文件中指定的属性。下面的例子使用基于构造方法的DI:

  1. <bean id="exampleBean" class="examples.ExampleBean">
  2. <!-- constructor injection using the nested ref element -->
  3. <constructor-arg>
  4. <ref bean="anotherExampleBean"/>
  5. </constructor-arg>
  6. <!-- constructor injection using the neater ref attribute -->
  7. <constructor-arg ref="yetAnotherBean"/>
  8. <constructor-arg type="int" value="1"/>
  9. </bean>
  10. <bean id="anotherExampleBean" class="examples.AnotherBean"/>
  11. <bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
  1. public class ExampleBean {
  2. private AnotherBean beanOne;
  3. private YetAnotherBean beanTwo;
  4. private int i;
  5. public ExampleBean(
  6. AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
  7. this.beanOne = anotherBean;
  8. this.beanTwo = yetAnotherBean;
  9. this.i = i;
  10. }
  11. }

bean定义中指定的构造方法参数将作为参数传给ExampleBean类的构造方法。

现在考虑这个例子的一个变体,在这里不使用构造方法,Spring被告之要调用一个静态工厂方法返回一个对象的实例。

  1. <bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
  2. <constructor-arg ref="anotherExampleBean"/>
  3. <constructor-arg ref="yetAnotherBean"/>
  4. <constructor-arg value="1"/>
  5. </bean>
  6. <bean id="anotherExampleBean" class="examples.AnotherBean"/>
  7. <bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
  1. public class ExampleBean {
  2. // a private constructor
  3. private ExampleBean(...) {
  4. ...
  5. }
  6. // a static factory method; the arguments to this method can be
  7. // considered the dependencies of the bean that is returned,
  8. // regardless of how those arguments are actually used.
  9. public static ExampleBean createInstance (
  10. AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
  11. ExampleBean eb = new ExampleBean (...);
  12. // some other operations...
  13. return eb;
  14. }
  15. }

静态工厂方法的参数通过<constructor-arg/>元素来提供,与实际使用构造方法完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同,尽管在本例中它是相同的。一个实例(非静态)工厂方法将以基本相同的方式使用(除了使用工厂bean属性而不是类属性),因此这里不再讨论细节。