构造函数注入与 Setter 注入以及 @Required 的使用

工程 | Alef Arendsen | 2007 年 7 月 11 日 | ...

几个月前,我们在 www.springframework.org 上开始发布问卷调查,征求大家对 Spring、其功能以及使用方式的反馈。我提出的第一个问题是,大家是否会检查必需的依赖项,如果会,他们使用了什么机制。我很快就跟进了这个问题,询问社区他们使用何种事务管理策略。

令我高兴的是,当我第一次查看结果时(那是在三月份),在第一个问卷中,很多人通过投票告诉我们他们正在使用 @Required 注解。第二个关于事务管理的问卷调查很快就显示,很多人正在使用 @Transactional 注解。下面你可以看到关于检查必需依赖项的问卷调查结果。结合关于事务管理的问卷调查(约 30% 的受访者使用 @Transactional 注解来界定事务边界),这些结果一致表明大家正在大量使用 Spring 2.0,这对我们来说是个非常好的消息。由于将使用 Spring 1.x 的应用程序升级到 Spring 2.0 应该没有任何问题,我们真心希望大家不要固守 Spring 1.x,事实上,大家也大规模地进行了升级。

您如何检查必需的依赖项?

8% 我在业务方法中检查它们。
9% 使用 init-method 和断言机制(参见 Assert)。
9% 在 XML 中使用 dependency-check 属性。
13% 我无需检查,我使用构造函数注入。
15% 使用 InitializingBean 和断言机制。
17% 使用 Spring 2.0 的 @Required 注解。
29% 我不检查必需的依赖项。

但有趣的是,有 29% 的人根本不检查必需的依赖项。在伴随讨论的论坛帖子中,出现了一些有趣的建议,解释了为什么有些人不做这件事以及其他人是如何解决的。让我们来回顾一下其中的一些。

构造函数注入

我想先回顾一下构造函数注入。任何具有接受参数的构造函数的对象,(显而易见地)无法在不传递参数的情况下被构造。在 Java 中,只要我们不自己添加构造函数,类就会被添加一个默认或隐式的构造函数。这个默认或隐式的构造函数不接受参数,所以只要你根本不添加带参数的构造函数,或者专门添加一个不带任何参数的构造函数,Spring(或者任何使用你的类的其他用户)就能在不传递任何东西的情况下实例化你的类。

换句话说,我们可以**强制**我们的类的用户(同样,这可能是 Spring,但也可能是一个直接实例化你的类的单元测试)在传递参数的同时实例化它。


public class Service {

  public Collaborator collaborator;

  // constructor with arguments, you *have* to
  // satisfy the argument to instantiate this class
  public Service(Collaborator collaborator) {
    this.collaborator = collaborator;
  }
}

在需要检查必需依赖项时,我们可以利用这一点。如果我们修改上面的代码示例以包含断言,那么我们就能 100% 确定该类在没有注入其协作对象的情况下永远不会被实例化。


public Service(Collaborator collaborator) {
  if (collaborator == null) {
    throw new IllegalArgumentException("Collaborator cannot be null");
  }
  this.collaborator = collaborator;
}

换句话说,如果我们使用构造函数注入并结合我上面展示的断言机制,那么我们就不需要一个专门的依赖项检查机制。

为什么人们大多不使用构造函数注入?

现在的问题当然是,如果它是完成这项工作最简单的方法,为什么这么少人使用构造函数注入来强制必需的依赖项?这有两个原因——一个更具历史性,另一个是 Spring 框架本身的性质。

历史原因

早在 2003 年初,Spring 首次作为开源项目发布时,它主要关注 Setter 注入。其他框架也率先提出了依赖注入的方法,其中一个就是 PicoContainer,它非常侧重于构造函数注入。Spring 保持其对 Setter 注入的关注,因为当时我们认为缺乏默认参数和构造函数参数的参数名导致了开发者的清晰度降低。但是,我们也实现了构造函数注入,以便能够为那些想要实例化和管理他们不控制的对象(objects they didn't control)的开发者提供这项功能。

这是你在 Spring 框架本身中看到大量 Setter 注入的原因之一。Setter 注入在 Spring 本身中的使用,以及我们大力提倡它,也导致了许多第三方软件开始使用 Setter 注入,以及博客和文章开始提及 Setter 注入。

(顺便问一下,大家还记得第一代、第二代和 M 型控制反转吗? ;-))

框架需要更高的可配置性

Setter 注入比预期使用得更多的第二个原因在于,像 Spring 这样的框架通常比构造函数注入更适合通过 Setter 注入进行配置。这主要是因为需要配置的框架通常包含许多可选值。使用构造函数注入配置可选值会导致不必要的混乱和过多的构造函数,尤其是在与类继承结合使用时。

出于这两个原因,我认为构造函数注入在应用程序代码中的可用性远高于在框架代码中。在应用程序代码中,你本身对需要配置的可选值的需求就比较少(你的应用程序代码不太可能在许多需要可配置属性的情况下被使用)。其次,应用程序代码比框架代码更少使用类继承。例如,应用程序中的特化不像框架代码中那样频繁发生——同样,应用程序代码的使用场景要少得多。

那么你应该使用什么?

我们通常建议使用构造函数注入来处理所有必需的协作对象,并使用 Setter 注入来处理所有其他属性。再次重申,构造函数注入确保所有必需的属性都已满足,并且不可能在无效状态(未传递其协作对象)下实例化一个对象。换句话说,在使用构造函数注入时,你无需使用专门的机制来确保必需的属性已设置(除了正常的 Java 机制)。

不使用构造函数注入的其他论点之一是构造函数中缺乏参数名,以及这些参数名不会出现在 XML 中。我认为在**大多数**应用程序中,这并没有太大影响。首先考虑使用 Setter 注入的变体:


<bean id="authenticator" class="com.mycompany.service.AuthenticatorImpl"/>

<bean id="accountService" class="com.mycompany.service.AccountService">
  <property name="authenticator" ref="authenticator"/>
</bean>

这个版本提到了 authenticator 作为属性名和 bean 名。这是我经常遇到的模式。我认为,在使用构造函数注入时,缺乏构造函数参数名(以及它们不在 XML 中显示)并不会真正让我们感到困惑。


<bean id="authenticator" class="com.mycompany.service.AuthenticatorImpl"/>

<bean id="accountService" class="com.mycompany.service.AccountService">
  <constructor-arg ref="authenticator"/>
</bean>

使用替代机制

这让我们回到了这篇博文的主题,其中还提到了 @Required。这是我们在 2006 年引入的新的 Spring 2.0 注解。@Required 允许你指示 Spring 为你检查必需的依赖项。如果你不能使用构造函数注入,或者出于任何其他原因,你更喜欢 Setter 注入,那么 @Required 是一个不错的选择。只需注解一个属性的 setter,并将 RequiredAnnotationBeanFactoryPostProcessor 注册为应用程序上下文中的一个 bean 即可。

public class Service {

  private Collaborator collaborator;

  @Required
  public void setCollaborator(Collaborator c) {
    this.collaborator = c;
  }
}

<bean class="org.sfw.beans.factory.annotation.RequiredAnnotationBeanFactoryPostProcessor"/>

检查必需依赖项的其他机制

还有一些其他机制可以强制检查必需的依赖项。其中大多数依赖于 Spring 在对象构造和初始化过程中在特定时间点进行回调的能力,例如 Spring InitializingBean 接口,或者 Spring 中可以在 XML 中配置的任意 init 方法(使用 init-method 属性)。这些都与构造函数注入非常相似,不同之处在于你依赖 Spring 来调用执行断言的方法。

public class Service implements InitializingBean {

  private Collaborator collaborator;

  public void setCollaborator(Collaborator c) {
    this.collaborator = c;
  }

  // from the InitializingBean interface
  public void afterPropertiesSet() {
    if (collaborator == null) {
      throw new IllegalStateException("Collaborator must be set in order for service to work");
    }
  }
}

另一种机制,类似于 Java 中的 @Required,是在 XML 中的 dependency-check 属性,奇怪的是,它的使用率并不高。通过调整此属性(默认关闭)启用依赖项检查,将告诉 Spring 开始检查 bean 上的某些依赖项。有关此功能的更多信息,请参阅参考文档。

那么为什么**不**检查必需的依赖项?

有很多人实际上并不检查依赖项是否已正确设置。人们不这样做的最大原因是他们认为,如果他们启动了 ApplicationContext 并以某种方式使用了具有依赖项的类,他们会很快发现。这当然是非常真实的。例如,如果你使用 Spring 的集成测试支持,你可以让 Spring 加载应用程序上下文。如果你还确保在集成测试中测试了实际代码,那么你可能几乎可以保证类工作所需的所有依赖项都已设置。然而,这种方法让我有些困扰。你必须对你的测试用例能够覆盖你的代码有足够的信心,因为如果你的测试没有测试依赖于协作对象已设置的代码,那么你就麻烦了,因为你可能无法检测到问题!当然,在部署应用程序时进行一次冒烟测试可能会当场解决问题,但我可不想成为只在运行时才发现缺失依赖项的人!

结论

关于构造函数注入与 Setter 注入有很多可以讨论的,我知道很多人仍然偏爱 Setter 注入。但我(以及很多人)认为,对于(不具有大量可选和可配置值或协作对象的)代码而言,构造函数注入与在构造函数中检查依赖项相结合是强制检查必需依赖项的更好方法。将其与 final 字段结合使用,可以立即带来在多线程环境中提高安全性的另一个好处,而且由于这通常也不是什么大问题,所以我不会在这篇博文中讨论它。

在某些情况下,我不会使用构造函数注入。例如,一个具有**大量**依赖项或其他可配置值的类。我个人不认为一个带有 20 个参数的构造函数是一个好代码的例子。当然,问题在于,一个具有 20 个依赖项的类是否不承担太多的职责……

有一件事是肯定的——通过在业务方法中检查必需依赖项来强制它们,我肯定不会这样做。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有