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

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

几个月前,我们开始在www.springframework.org上发布调查,请大家提供关于Spring、其一些特性以及他们如何使用这些特性的反馈。我发布的第一个问题是人们是否在检查必需的依赖项,如果是,他们使用了什么机制。我很快对这个问题进行了跟进,询问社区使用了什么事务管理策略。

令我高兴的是,当我第一次查看3月份的结果时,许多人通过投票参与第一个调查告诉我们,他们正在使用@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注入的关注,因为当时,我们认为构造器参数缺乏默认参数和参数名称会导致开发人员的清晰度降低。但是,我们也实现了构造器注入,以便能够为想要实例化和管理他们无法控制的对象的开发人员提供该功能。

这是您在整个Spring框架本身中看到大量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>

此版本将身份验证器作为属性名称和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 社区中所有即将举行的活动。

查看全部