属性注入 vs 构造函数注入以及 @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 Framework 本身的性质。

历史原因

2003 年初,当 Spring 首次作为开源项目发布时,它主要专注于 setter 注入。其他框架也开创了依赖注入的方法,其中之一是 PicoContainer,它强烈专注于构造函数注入。Spring 保持了对 setter 注入的关注,因为当时我们认为,构造函数参数缺乏默认参数和参数名称会导致开发人员不够清晰。然而,我们也实现了构造函数注入,以便能够为那些希望实例化和管理非自己控制的对象提供该特性。

这就是为什么您在 Spring Framework 本身中看到大量 setter 注入的原因之一。Spring 本身使用了 setter 注入的事实,以及我们主要倡导它,也导致许多第三方软件开始使用 setter 注入,以及博客和文章开始提及 setter 注入。

(顺便问一下,大家还记得控制反转的 1 类、2 类和 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 接口或你可以在 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 社区的所有近期活动。

查看全部