为什么在CDI中使用构造函数而不是setter注入?

35

我在SO上没有找到任何合理的答案,所以希望这不是重复的问题。那么为什么我应该优先选择setter或构造函数注入而不是简单的注入?

@Inject
MyBean bean;

如果您需要在类初始化期间执行注入的bean操作,那么我理解构造函数注入的用法。

public void MyBean(@Inject OtherBean bean) {
    doSomeInit(bean);
    //I don't need to use @PostConstruct now
}

不过,它几乎与@PostConstruct方法相同,我完全不了解setter注入,难道这只是Spring和其他DI框架过时的遗物吗?


我不这么认为(我已经读过这个问题),因为他们正在讨论使用构造函数还是setter更好,而我在这里问的是如果我可以使用字段注入,那么setter或构造函数注入的目的是什么,所以为什么要踩呢? - Petr Mensik
3个回答

43

构造函数和属性注入为您提供了在非 CDI 环境中轻松初始化对象的选项,例如单元测试。

在非 CDI 环境中,您仍然可以通过传递构造函数参数来简单地使用对象。

OtherBean b = ....;
new MyBean(b);

如果你只使用字段注入,通常必须使用反射访问字段,因为字段通常是私有的。

如果你使用属性注入,你还可以在setter中编写代码。例如验证代码或者清除内部缓存,这些缓存保存了从setter修改的属性派生出来的值。你想要做什么取决于你的实现需求。

Setter vs constructor注入

在面向对象编程中,一个对象必须在构造后处于有效状态,每个方法调用都会将状态更改为另一个有效状态。

对于setter注入,这意味着您可能需要更复杂的状态处理,因为即使setter尚未被调用,对象也应该在构造后处于有效状态。因此,即使未设置属性,对象也必须处于有效状态。例如通过使用默认值或null object

如果对象的存在和属性之间存在依赖关系,则属性应该是构造函数参数。这样也会使代码更加干净,因为如果您使用构造函数参数,您就可以记录依赖关系是必需的。

所以,不要编写这样的类:

public class CustomerDaoImpl implements CustomerDao {
 
  private DataSource dataSource;
 
  public Customer findById(String id){
     checkDataSource();

     Connection con = dataSource.getConnection();
     ...
     return customer;
  }

  private void checkDataSource(){
     if(this.dataSource == null){
         throw new IllegalStateException("dataSource is not set");
     }
  }

 
  public void setDataSource(DataSource dataSource){
     this.dataSource = dataSource;
  }
 
}

您应该使用构造函数注入。

public class CustomerDaoImpl implements CustomerDao {
 
  private DataSource dataSource;
 
  public CustomerDaoImpl(DataSource dataSource){
      if(dataSource == null){
        throw new IllegalArgumentException("Parameter dataSource must not be null");
     }
     this.dataSource = dataSource;
  }
 
  public Customer findById(String id) {    
      Customer customer = null;
     // We can be sure that the dataSource is not null
     Connection con = dataSource.getConnection();
     ...
     return customer;
  }
}

我的结论:

  • 对于每个可选依赖项,请使用属性(properties)
  • 对于每个强制依赖项,请使用构造函数参数(constructor args)

PS:我的博客POJO与Java Bean的区别更详细地解释了我的结论。

编辑:

Spring文档中也建议使用构造函数注入,详见章节Setter-based Dependency Injection

Spring团队一般推荐使用构造函数注入,因为它可以让您将应用程序组件实现为不可变对象,并确保所需的依赖项不为null。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。顺便提一下,大量的构造函数参数是糟糕的代码气味,暗示着该类可能具有过多的职责,并且应进行重构以更好地解决关注点分离问题。

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

考虑到单元测试,构造函数注入也是更好的方式,因为调用构造函数比设置私有(@Autowired)字段更容易。


好的观点,谢谢!(尽管现在使用Arquillian框架已经不必要了) - Petr Mensik
9
构造函数注入还允许将类字段声明为 final,而使用属性或setter注入时则不可能。 - Pavel Horal
4
使用构造器注入,您可以使您的Bean不可变。 - Yuri
4
这会产生大量不安全/容易出错的代码,还是只是让依赖关系的噩梦变得更加明显?我的意思是,如果一个对象有必要的依赖关系,你可以使用setter隐藏它们,并可能编写一些javadoc,例如“这个setter必须在之前调用”,或者你可以让它们明确。我还没有看到你的代码,但如果你说有一个“更复杂的类层次结构和许多抽象超类”,我猜这是一个设计问题。我通常更喜欢组合而不是子类化。 - René Link
使用基于字段的注入会有性能问题吗? - Arefe
显示剩余3条评论

4
在使用CDI时,没有任何理由使用构造函数或设置器注入。正如问题中所指出的,您可以添加一个@PostConstruct方法来执行原本应该在构造函数中完成的操作。
其他人可能会说,在单元测试中需要使用反射来注入字段,但事实并非如此;模拟库和其他测试工具会为您完成这项工作。
最后,构造函数注入允许字段被声明为final,但这并不是@Inject注释字段的缺点(它们不能是final)。注释的存在,以及任何明确设置字段的代码的缺失,应该表明该字段仅由容器(或测试工具)设置。实际上,没有人会重新分配已注入的字段。
在过去,使用构造函数和设置器注入是有道理的,因为开发人员通常必须手动实例化并注入依赖项到测试对象中。现在,随着技术的发展,字段注入是更好的选择。

4
IDE会为您生成它,这没有任何缺点。使用字段注入时,即使使用“先进技术”工具进行模拟也很困难,因为您不知道需要为初始化提供哪些依赖项,除非您检查代码。这些第三方工具还会带来性能影响,这是单元测试的祸根。谁会想使用Weld执行@PostConstruct并依赖其他第三方组件,当他们所需的只是一个构造函数呢? - highstakes
1
我知道这可能不太方便,但构造函数是你向外部通信的API的一部分。如果你开始使用隐藏的私有字段依赖关系,会造成不必要的混淆。花同样的精力,我可以使用PowerMock来模拟静态依赖项,那么为什么还要烦恼于控制反转呢? - highstakes
3
使用构造函数注入,如果您向构造函数添加新的依赖项,您的测试将无法编译。对我来说这是一件好事。使用字段或设置器注入,您的测试仍将编译,并且可能会因为它们如何使用新添加的依赖项而失败。 - Magnilex
1
在使用绝对化语言时的问题在于很容易被证明是错误的,因为只需要找到一个反例就可以了。构造函数注入之所以仍然有用,是因为与任何构造函数一样有用。字段注入强制你将字段按原样注入,没有修改。构造函数注入允许你在设置字段之前转换参数。这是使用构造函数的基本好处之一,无论是否使用依赖注入。 - DavidS
1
如果您想从其他人那里听到这个问题的答案,可以考虑阅读Antonio Goncalves的博客,他在博客中写道:“除了个人口味外,没有真正的技术答案。在托管环境中,容器是执行所有注入工作的对象,它只需要正确的注入点。但是,使用构造函数或setter注入,如果需要的话(不使用字段注入),您可以添加一些逻辑。”他已经写了多本关于Java EE的书籍,并且是许多JSR的专家成员。 - DavidS
显示剩余6条评论

2
被接受的答案很好,但它没有给予构造函数注入的主要优势以应有的赞誉——类不可变性,这有助于实现线程安全、状态安全和更好的类可读性。
考虑一个具有依赖项的类,所有这些依赖项都作为构造函数参数提供,那么您可以知道对象永远不会存在依赖项无效的状态。对于这些依赖项,不需要设置器(只要它们是私有的),因此对象被实例化为完整状态或根本没有被实例化。
在多线程应用程序中,不可变对象更容易表现良好。虽然该类仍需在内部实现线程安全,但您不必担心外部客户端协调访问该对象。
当然,这只在某些情况下才有用。 Setter注入非常适合部分依赖关系,例如我们在一个类中有3个属性和3个参数的构造函数和设置器方法。在这种情况下,如果您只想传递一个属性的信息,则只能通过设置器方法来完成。非常有用于测试目的。

1
我不明白它与不可变性有什么关系,你可以通过防止对象方法修改对象本身(因此在修改时返回新实例)来获得不可变性,而不是通过任何形式的 DI。然而,其他方面是有道理的。 - Petr Mensik
我并不是说这是依赖注入的功劳,我是在说不使用setter方法是实现不可变对象的一种方式。问题是为什么要使用构造函数依赖注入而不是setter方法。 - Piotr Niewinski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接