Spring 教程

Spring 依赖注入

典型的企业应用程序并非由单一对象(或 Spring 术语中的 bean)组成。即使是最简单的应用程序,也会由一些对象协同工作,从而呈现给用户一个连贯的、功能完善的应用程序。本章节将介绍如何从定义一些独立的 Bean 定义到完全实现对象协作以实现目标应用程序。

什么是依赖注入

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

采用 DI 原则,代码会更简洁,当对象提供了其依赖关系时,解耦会更有效。对象不会查找其依赖关系,也不知道依赖关系的位置或类。因此,你的类变得更容易测试,尤其是当依赖的是接口或抽象基类时,这就允许在单元测试中使用桩(stub,常见服务桩)或 mock 实现。

注意,依赖注入有两种主要形式: 基于构造器的依赖注入基于设置器(setter)的依赖注入

什么是服务桩?

服务桩(Service Stub)是软件开发中的一个概念,它用于模拟或替代真实的服务组件,以便在开发和测试过程中进行独立的单元测试或集成测试。

服务桩通常用于分布式系统或面向服务架构(SOA)中,其中不同的服务之间通过网络进行通信。在开发和测试过程中,某些服务可能尚未实现或不可用,或者可能会引起不稳定的行为。为了解决这些问题,可以使用服务桩来模拟这些服务的行为和响应。

服务桩可以模拟服务的接口和行为,以便在测试过程中替代真实的服务。它们通常具有与真实服务相同的接口,可以接收相同的请求并返回预定义的响应。通过使用服务桩,开发人员可以独立地测试其代码,而不需要实际的服务存在或可用。

服务桩的另一个常见用途是模拟外部依赖,例如数据库或第三方API。通过使用服务桩,可以在测试环境中模拟这些外部依赖的行为,以便更好地控制测试环境并减少对外部资源的依赖。

总结来说,服务桩是用于模拟或替代真实服务或外部依赖的组件,以便在开发和测试过程中进行独立的单元测试或集成测试。它们可以提供与真实服务相同的接口和行为,并返回预定义的响应,以便更好地控制测试环境并减少对外部资源的依赖。

基于构造函数的依赖注入

基于构造函数的依赖注入是通过容器调用带有大量参数的构造函数来实现的,构造函数的每个参数代表一个依赖关系。与调用带有特定参数的静态工厂方法来构造 Bean 几乎是等价的。

下面的示例展示了一个只能通过构造函数注入依赖的注入类:

public class SimpleMovieLister {

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

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

   //...
}

请注意,这个类没有什么特别之处。它是一个 POJO,不依赖于容器特定的接口、基类或注解。

构造函数参数解析

构造函数参数解析匹配是通过使用参数的类型来实现的。如果 bean 定义中的构造函数参数不存在潜在的歧义,那么 bean 定义中定义构造函数参数的顺序就是在实例化 bean 时将这些参数提供给相应构造函数的顺序。例如:

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 无法确定值的类型,因此在没有明确定义的情况下无法通过类型进行匹配。例如:

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;
   }
}

构造函数参数类型匹配

在前面的示例中,如果使用 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 可以从构造函数中查找参数名称。如果不能或不想使用调试标志编译代码,可以使用  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 的依赖注入

容器在调用无参数构造函数或无参数静态工厂方法实例化 Bean 后,会调用 Bean 上的 setter 方法,从而实现基于 setter 的依赖注入。

下面的示例展示了一个只能通过使用纯 setter 来实现依赖注入的类。该类是传统的 Java 类,它是一个 POJO,不依赖于容器特定的接口、基类或注解。例如:

public class SimpleMovieLister {

   // SimpleMovieLister 依赖于 MovieFinder
   private MovieFinder movieFinder;

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

   // ...
}

ApplicationContext 支持为其管理的 Bean 提供基于构造函数和基于 setter 的依赖注入。在通过构造器方法注入某些依赖关系后,它还支持基于 setter 的依赖注入。您可以以 BeanDefinition 的形式配置依赖关系,并将其与 PropertyEditor 实例结合使用,将属性从一种格式转换为另一种格式。不过,大多数 Spring 用户并不直接使用这些类(即编程方式),而是使用 XML Bean 定义、注解组件(即使用 @Component、@Controller 等注解的类)或基于 Java 的 @Configuration 类中的 @Bean 方法。然后,这些源会在内部转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例。

如何选择“基于构造器”、“基于 setter”的依赖注入?

由于您可以混合使用基于构造函数和基于setter 的依赖注入,因此一个很好的经验法则是将构造函数用于强制性依赖关系,而将 setter 方法或配置方法用于可选依赖关系。需要注意的是,在设置器方法上使用 @Required 注解可以使属性成为必备依赖项;不过,最好使用构造器注入并对参数进行编程验证。

Spring 团队通常提倡构造器注入,因为它可以将应用程序组件作为不可变对象来实现,并确保所需的依赖关系不会为空。此外,注入构造函数的组件总是以完全初始化的状态返回给客户端(调用)代码。顺便提一句,大量的构造函数参数是一种不好的代码习惯,这意味着类的职责可能过多,应进行重构,适当的分离以更好地解决问题。

setter 注入应主要用于可在类中分配合理默认值的可选依赖项。否则,必须在代码使用依赖关系的任何地方执行非空检查。setter 注入的一个好处是,setter 方法可使该类的对象在以后进行重新配置或重新注入。因此,通过 JMX MBeans 进行管理是 setter  注入的一个重要用例。

使用对特定类最合理的依赖注入方式。有时,在处理没有源代码的第三方类时,我们会为你做出选择。例如,如果第三方类没有公开任何设置器方法,那么构造器注入可能是唯一可用的依赖注入方式。

依赖关系解析流程

容器执行 Bean 依赖关系解析的过程如下:

(1)创建 ApplicationContext 并用描述所有 Bean 的配置元数据对其进行初始化。配置元数据可以通过 XML、Java 代码或注解来指定。

(2)对于每个 Bean,其依赖关系以属性、构造函数参数或静态工厂方法参数(如果你用它来代替普通的构造函数)的形式表示。这些依赖关系会在实际创建 Bean 时提供给 Bean。

(3)每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 Bean 的引用。

(4)每个属性或构造函数参数如果是值,都会从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,如 int、long、String、布尔等。

Spring 容器会在创建容器时验证每个 Bean 的配置。但是,在实际创建 Bean 之前,Bean 属性本身不会被设置。单例作用域(singleton-scoped)且被设置为预实例化(pre-instantiated,默认)的 Bean 会在容器创建时创建。作用域在 Bean Scopes 中定义。否则,Bean 只有在被请求时才会被创建。创建一个 Bean 可能会导致创建一个 Bean 图,因为 Bean 的依赖关系及其依赖关系的依赖关系(等等)都会被创建和分配。请注意,这些依赖关系之间的解析不匹配可能会在后期出现,即在首次创建受影响的 Bean 时。

循环依赖

如果你主要使用构造函数注入,就有可能产生无法解决的循环依赖情况。

例如:类 A 通过构造器注入需要类 B 的实例,而类 B 通过构造器注入需要类 A 的实例。如果您将类 A 和类 B 的 Bean 配置为相互注入,Spring IoC 容器会在运行时检测到这种循环引用,并抛出 BeanCurrentlyInCreationException 异常。

一种可行的解决方案是编辑某些类的源代码,使其通过设置器而不是构造器进行配置。或者,避免构造器注入,只使用设置器注入。换句话说,虽然不推荐使用设置器注入,但可以配置循环依赖关系。

与典型情况(无循环依赖关系)不同的是,Bean A 和 Bean B 之间的循环依赖关系会迫使其中一个 Bean 在完全初始化之前注入到另一个 Bean 中(典型的先有鸡还是先有蛋)。

一般来说,你可以相信 Spring 会做正确的事。它会在容器加载时检测配置问题,如引用不存在的 Bean 和循环依赖关系。在实际创建 Bean 时,Spring 会尽可能晚地设置属性并解决依赖关系。这就意味着,如果在创建对象或其某个依赖关系时出现问题(例如,Bean 因属性缺失或无效而抛出异常),已正确加载的 Spring 容器随后会在您请求对象时产生异常。这可能会延迟一些配置问题的可见性,这就是ApplicationContext实现默认情况下预实例化单例bean的原因。在实际需要之前创建这些 Bean 会花费一些前期时间和内存,但在创建 ApplicationContext 时,您就会发现配置问题。您可以覆盖这一默认行为,从而使单例 Bean 以延迟的方式初始化,而不是急切地预实例化。

如果不存在循环依赖关系,当一个或多个协作 Bean 被注入到依赖 Bean 中时,每个协作 Bean 都会在注入到依赖 Bean 中之前被完全配置。这意味着,如果 Bean A 依赖于 Bean B,Spring IoC 容器会在调用 Bean A 的 setter 方法之前完全配置 Bean B。换句话说,Bean 会被实例化(如果它不是预先实例化的单例)、设置其依赖关系,并调用相关的生命周期方法(如配置的 init 方法或 InitializingBean 回调方法)。

依赖注入示例

基于 setter 依赖注入示例

下面的示例将基于 XML 的配置元数据用于基于 setter 的依赖注入。Spring XML 配置文件的一小部分指定了如下一些 Bean 定义:

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

   <!-- 使用了更简洁的 ref 属性 -->
   <property name="beanTwo" ref="yetAnotherBean"/>
   <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

下面是 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 文件中指定的属性相匹配。下面的示例使用了基于构造函数的依赖注入:

<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 类代码:

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"/>

下面是 ExampleBean 类代码:

public class ExampleBean {

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

   // 静态工厂方法;该方法的参数可以被
   // 视为返回 bean 的依赖项、
   // 无论这些参数实际如何使用。
   public static ExampleBean createInstance (AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
       ExampleBean eb = new ExampleBean (...);
       // 其他一些操作...
       return eb;
   }
}

静态工厂方法的参数由 <constructor-arg/> 元素提供,与实际使用构造函数的情况完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是相同的)。实例(非静态)工厂方法的使用方式与静态工厂方法基本相同(除了使用 factory-bean 属性而不是 class 属性),例如:

<bean id="exampleBeanFactory" class="examples.ExampleBean" />

<bean id="exampleBean" factory-bean="exampleBeanFactory" 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 类代码:

public class ExampleBean {

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

   // 实例工厂方法;该方法的参数可以被
   // 视为返回 bean 的依赖项、
   // 无论这些参数实际如何使用。
   public ExampleBean createInstance (AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
       ExampleBean eb = new ExampleBean (...);
       // 其他一些操作...
       return eb;
   }
}

如果使用 getBean(Class<T> aclass) 方法获取上面示例中 ExampleBean 的实例,则会抛出如下错误:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type '*.*.ExampleBean' available: expected single matching bean but found 2: exampleBeanFactory,exampleBean

这是告诉我们,Spring IoC 容器中存在两个类型为 ExampleBean 的实例,要解决这个问题有两种方式:

方式一:直接改用 getBean(String name),根据 Bean 的 ID 名进行获取。

方式二:将实例工厂方法放到单独的类当中,如下:

<!-- 实例工厂方法注入依赖 -->
<bean id="exampleBeanFactory" class="examples.ExampleBeanFactory"/>
<bean id="exampleBean" factory-bean="exampleBeanFactory" factory-method="createInstance">
   <!-- 使用嵌套 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"/>

下面是 ExampleBeanFactory 类代码:

public class ExampleBeanFactory {

   // 实例工厂方法;该方法的参数可以被
   // 视为返回 bean 的依赖项、无论这些参数实际如何使用。
   public ExampleBean createInstance(AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
       ExampleBean eb = new ExampleBean(anotherBean, yetAnotherBean, i);
       // 其他一些操作...
       return eb;
   }

}
说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号