依赖注入

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

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

基于构造函数的依赖注入

基于构造函数的 DI 是通过容器调用带有多个参数的构造函数来完成的,每个参数代表一个依赖项。 调用带有特定参数的 static 工厂方法来构造 bean 几乎是等效的,本讨论将类似地对待构造函数的参数和 static 工厂方法的参数。 以下示例显示了一个只能使用构造函数注入进行依赖注入的类:

  • Java

public class SimpleMovieLister {

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

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

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

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

构造函数参数解析

构造函数参数解析匹配是通过使用参数的类型进行的。 如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序就是这些参数在实例化 bean 时提供给适当构造函数的顺序。 考虑以下类:

  • Java

package x.y;

public class ThingOne {

  public 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>,Infra 无法确定值的类型,因此无法在没有帮助的情况下按类型匹配。 考虑以下类:

  • Java

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;
  }
}
构造函数参数类型匹配

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

请记住,为了使其开箱即用,您的代码必须在启用调试标志的情况下进行编译,以便 Infra 可以从构造函数中查找参数名称。 如果您不能或不想在启用调试标志的情况下编译代码,可以使用 @ConstructorProperties JDK 注解来显式命名构造函数参数。示例类必须如下所示:

  • Java

package examples;

public class ExampleBean {

  // 字段省略

  @ConstructorProperties({"years", "ultimateAnswer"})
  public ExampleBean(int years, String ultimateAnswer) {
    this.years = years;
    this.ultimateAnswer = ultimateAnswer;
  }
}

基于 Setter 的依赖注入

基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数 static 工厂方法以实例化 bean 后,调用 bean 上的 setter 方法来完成的。

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

  • Java

public class SimpleMovieLister {

  // SimpleMovieLister 依赖于 MovieFinder
  private MovieFinder movieFinder;

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

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

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

基于构造函数还是基于 Setter 的 DI?

由于您可以混合使用基于构造函数和基于 setter 的 DI,因此通过构造函数注入强制性依赖项,通过 setter 方法或配置方法注入可选依赖项是一个很好的经验法则。 请注意,在 setter 方法上使用 @Autowired 注解可用于使属性成为必需的依赖项; 但是,带有参数编程验证的构造函数注入是首选。

Infra 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为 null。 此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。 顺便说一句,大量的构造函数参数是一种糟糕的代码气味,这意味着该类可能有太多的责任,应该重构以更好地解决适当的关注点分离问题。

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

使用对特定类最有意义的 DI 风格。有时,在处理您没有源代码的第三方类时,选择是为您做出的。 例如,如果第三方类没有公开任何 setter 方法,则构造函数注入可能是唯一可用的 DI 形式。

依赖解析过程

容器执行 bean 依赖解析如下:

  • ApplicationContext 被创建并使用描述所有 bean 的配置元数据进行初始化。 配置元数据可以通过 XML、Java 代码或注解指定。

  • 对于每个 bean,其依赖项以属性、构造函数参数或静态工厂方法参数的形式表示(如果您使用它而不是普通构造函数)。 当 bean 实际创建时,这些依赖项将提供给 bean。

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

  • 每个作为值的属性或构造函数参数都会从其指定的格式转换为该属性或构造函数参数的实际类型。 默认情况下,Infra 可以将以字符串格式提供的值转换为所有内置类型,例如 intlongStringboolean 等。

Infra 容器在创建容器时验证每个 bean 的配置。 但是,在实际创建 bean 之前,不会设置 bean 属性本身。 单例作用域并设置为预实例化(默认值)的 bean 在创建容器时创建。作用域在 Bean 作用域 中定义。 否则,仅在请求时才创建 bean。 创建 bean 可能会导致创建 bean 图,因为创建并分配了 bean 的依赖项及其依赖项的依赖项(依此类推)。 请注意,这些依赖项之间的解析不匹配可能会很晚才显示出来——即在首次创建受影响的 bean 时。

循环依赖

如果您主要使用构造函数注入,则可能会创建无法解析的循环依赖场景。

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

一种可能的解决方案是编辑某些类的源代码,使其由 setter 而不是构造函数配置。 或者,避免构造函数注入,仅使用 setter 注入。 换句话说,尽管不建议这样做,但您可以使用 setter 注入配置循环依赖项。

与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖迫使其中一个 bean 在完全初始化自身之前注入到另一个 bean 中(经典的鸡和蛋场景)。

您通常可以信任 Infra 做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 的引用和循环依赖。 Infra 在实际创建 bean 时尽可能晚地设置属性并解析依赖项。 这意味着,如果创建该对象或其依赖项之一存在问题——例如,bean 因缺少或无效属性而抛出异常——已正确加载的 Infra 容器稍后在您请求对象时可能会生成异常。 某些配置问题的这种潜在延迟可见性是 ApplicationContext 实现默认预实例化单例 bean 的原因。 以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您可以在创建 ApplicationContext 时发现配置问题,而不是稍后。 您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预实例化。

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

依赖注入示例

以下示例使用基于 XML 的配置元数据进行基于 setter 的 DI。 Infra 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

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">
  <!-- 使用嵌套 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

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 构造函数的参数。

现在考虑这个示例的一个变体,其中 Infra 被告知调用 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

public class ExampleBean {

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

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

    ExampleBean eb = new ExampleBean (...);
    // 一些其他操作...
    return eb;
  }
}

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