6.4 依赖
典型的企业应用程序不包含单个对象(或Spring用法中的bean)。 即使是最简单的应用程序也有一些对象可以协同工作,以呈现连贯的应用程序给最终用户看。下一节将介绍如何从定义多个独立的bean,到对象协作取得统一目标的完全实现的应用程序。
6.4.1 依赖注入
它是通过对象定义它们的依赖关系的过程,也就是说,它们使用的其他对象,在被构造或者从工厂方法返回之后,只能通过构造参数、工厂方法参数或者属性设置到对象实例上,然后容器在创建Bean的时候注入这些依赖。这相对于Bean本身通过使用类的直接构造或者诸如服务定位器模式之类的机制来控制其依赖关系的实例化和定位来说,这个过程基本上是反向的,因此把它叫做控制反转(IoC)。
使用DI原理的代码更清晰,当对象提供给依赖项时,解耦更有效。该对象不查找其依赖项,也不知道依赖项的位置或类。因此,你的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。
DI存在两个主要变体,基于构造函数的依赖注入和基于Setter的依赖注入。
基于构造函数的依赖注入
基于构造函数的依赖注入由容器调用具有多个参数的构造函数来完成,每个参数表示一个依赖项,这等同于调用具有指定参数的静态工厂方法来构造bean。 以下示例显示了一个只能通过构造函数进行依赖注入的类。请注意,此类没有什么特别之处,它是一个POJO,它不依赖于容器特定的接口,基类或注解。
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... }
构造函数参数解析
构造函数参数解析匹配发生在使用参数类型。 如果在bean定义的构造函数参数中不存在潜在的歧义,那么在bean定义中定义构造函数参数的顺序是在实例化bean时将这些参数提供给适当的构造函数的顺序。思考下面的类:
package x.y; public class Foo { public Foo(Bar bar, Baz baz) { // ... } }
假设Bar和Baz类在继承上没有关联,则不存在潜在的歧义。因此,以下配置工作正常,你无需在<constructor-arg />元素中显式指定构造函数参数索引和/或类型。
<beans> <bean id="foo" class="x.y.Foo"> <constructor-arg ref="bar"/> <constructor-arg ref="baz"/> </bean> <bean id="bar" class="x.y.Bar"/> <bean id="baz" class="x.y.Baz"/> </beans>
当引用另一个bean时,类型是已知的,并且可以发生匹配(与前面的示例一样)。当使用简单类型时,例如<value> true </ value>,Spring无法确定值的类型,因此在没有帮助的情况下无法按类型进行匹配。思考下面的类:
package examples; public class ExampleBean { // Number of years to calculate the Ultimate Answer private int years; // The Answer to Life, the Universe, and Everything private String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } }
在前面的场景中,如果使用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可以从构造函数中查找参数名称。 如果无法使用debug标志(或不想)编译代码,则可以使用JDK注解@ConstructorProperties JDK显式给构造函数参数命名。示例类如下所示:
package examples; public class ExampleBean { // Fields omitted @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } }
基于Setter的依赖注入
基于setter的依赖注入是在调用无参数构造函数或无参数静态工厂方法来实例化bean之后,通过容器调用bean上的setter方法来完成的。
以下示例显示了一个只能使用纯setter注入进行依赖注入的类。这个类是传统的Java。 它是一个POJO,它不依赖于容器指定的接口,基类或注解。
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... }
ApplicationContext支持管理基于构造函数和基于setter的依赖注入的Bean。它也支持在通过构造函数方法注入了一些依赖项之后,再使用基于setter的依赖注入。你可以以BeanDefinition的形式配置依赖项,并将其与PropertyEditor实例结合使用,以将属性从一种格式转换为另一种格式。然而,大多数Spring用户不是直接使用这些类(即以编程方式),而是以XML bean的定义,带注解的组件(即使用@ Component,@ Controller等注释的类)或是在@Configuration类中,基于Java的@Bean方法。然后,这些源头在内部会转换为BeanDefinition的实例,并用于加载整个Spring IoC容器实例。
基于构造函数还是基于setter的DI?
由于你可以混合使用基于构造函数和基于setter的依赖注入,因此将构造函数用于强制依赖项和setter方法用于可选依赖项的配置方法会是一个不错的经验法则。请注意,在setter方法上使用@Required注释可用于使属性成为必需的依赖项。
Spring团队通常提倡构造函数注入,因为它使应用程序组件可以成为不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件始终以完全初始化的状态返回到客户端(调用)代码。作为旁注,大量的构造函数参数弥散着糟糕的代码气味,暗示该类可能有太多的职责,应该进行重构以便更好地处理关注点的正确分离。
Setter注入应该主要用于在类中指定合理默认值的可选依赖项。否则,必须在代码使用依赖项的任何位置执行非空检查。 setter注入的一个好处是setter方法使该类的对象可以在以后进行重新配置或重新注入。因此,通过JMX MBean进行管理是setter注入的一个引人注目的范例。
对特定类使用依赖注入样式是最有意义的。有时,在处理没有源的第三方类时,对你来说是一种选择。例如,如果第三方类没有暴露任何setter方法,那么构造函数注入可能是唯一可用的依赖注入形式。
依赖解析过程
容器执行bean依赖性解析,如下所示:
- ApplicationContext是由描述所有bean的配置元数据创建和初始化的。可以通过XML,Java代码或注解指定配置元数据。
- 对于每个bean,如果以属性、构造函数参数或static-factory方法参数的形式表示它们的依赖关系,而不是以普通的构造函数形式。那么,在bean真正被创建的时候,会将这些依赖项提供给bean。
- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。
- 每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型的值。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,例如int,long,String,boolean等。
Spring容器在创建的时候会校验每个bean的配置。但是,在bean实际创建之前,不会设置bean属性本身。在容器创建的时候,单例作用域并设置为预先实例化(默认值)的Bean会被创建。作用域在第6.5节“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和循环依赖关系的引用。当实际创建bean时,Spring会尽可能晚地设置属性并解析依赖关系。这意味着,当你请求一个对象的时候,如果在创建该对象或其中某个依赖项出现问题,正确加载的Spring容器可以在这之后生成异常。例如,bean因缺少属性或无效属性而抛出异常。这可能会延迟一些配置问题的可见性,这就是在默认情况下ApplicationContext实现预先实例化单例bean的原因。为了在创建ApplicationContext时及时发现配置问题,你可以以牺牲一些前期时间和内存为代价,在实际需要这些Bean之前就创建这些Bean。您仍然可以覆盖此默认行为,以便延迟初始化单例bean,而不是预先实例化。
如果不存在循环依赖关系,当一个或多个协作bean被注入到依赖bean时,每个协作bean会优先配置。这意味着如果bean A依赖于bean B,Spring IoC容器在调用bean A上的setter方法之前配置好bean B。换句话说,bean被实例化(如果不是预先实例化的单例),依赖项的设置,相关的生命周期方法(如,配置的init方法或InitializingBean回调方法)就会被调用。
依赖注入的例子
以下示例使用了基于XML的配置元数据的基于setter的依赖注入。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"/>
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文件中指定的属性匹配。以下示例使用基于构造函数的依赖注入:
<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"/>
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"/>
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; } }
静态工厂方法的参数通过<constructor-arg />元素提供,与实际使用的构造函数完全相同。 工厂方法返回的类的类型不必与包含静态工厂方法的类具有相同的类型,尽管在此示例中它是。实例(非静态)工厂方法将以基本相同的方式使用(除了使用factory-bean属性而不是class属性),因此这里不再讨论细节。