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

Posted

技术标签:

【中文标题】为啥在 CDI 中使用构造函数而不是 setter 注入?【英文标题】:Why use constructor over setter injection in CDI?为什么在 CDI 中使用构造函数而不是 setter 注入? 【发布时间】:2013-10-23 07:03:42 【问题描述】:

我在 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框架之后的遗物吗?

【问题讨论】:

Dependency injection through constructors or property setters? 的可能重复项 我不这么认为(我已经读过这个)因为他们正在讨论使用构造函数或设置器是否更好,我在这里问如果可以的话,设置器或构造函数注入的目的是什么使用字段注入,那么为什么要投反对票? 【参考方案1】:

构造函数和属性注入使您可以选择在非 CDI 环境中轻松初始化对象,例如单元测试。

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

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

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

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

Setter 与构造函数注入

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

对于 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;
  

我的结论

为每个可选依赖项使用属性。 对每个强制依赖项使用constructor args

PS:我的博客The difference between pojos and java beans更详细地解释了我的结论。

编辑

Spring 还建议使用构造函数注入,正如我在 spring 文档 Setter-based Dependency Injection 部分中找到的那样。

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

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

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

【讨论】:

好点,谢谢! (尽管现在 Arquillian 框架不需要这样做) 构造函数注入还允许将类字段声明为final。这在使用属性或设置器注入时是不可能的。 还可以通过构造函数注入使 bean 不可变。 我有一个案例,我们正在为一些外部项目准备 API。我们有一个更复杂的类层次结构,其中包含许多抽象超类。添加基于构造函数的注入将强制在每个继承级别上创建构造函数。这会产生大量不安全/容易损坏的代码。如果外部项目的开发人员想要扩展我们的类,他们也必须创建这样的构造函数。这将导致完全混乱和不可靠的代码。 That creates loads of unsafe / easily broken code 是这样做还是只是使依赖项噩梦可见?我的意思是,如果一个对象具有强制依赖项,您可以使用 setter 隐藏它们,也可以编写一些 javadoc,如 this setter must be called before,或者您可以使它们显式。我还没有看到你的代码,但如果你说你有一个“具有许多抽象超类的更复杂的类层次结构”,我猜想存在设计问题。我通常更喜欢组合而不是子类化。【参考方案2】:

使用 CDI 时,没有任何理由使用构造函数或 setter 注入。如问题中所述,您添加了一个 @PostConstruct 方法,否则将在构造函数中完成。

其他人可能会说你需要在单元测试中使用反射来注入字段,但事实并非如此;模拟库和其他测试工具可以为您做到这一点。

最后,构造函数注入允许字段为final,但这并不是@Inject-annotated 字段(不能是final)的真正缺点。注释的存在,加上没有明确设置字段的任何代码,应该清楚地表明它只能由容器(或测试工具)设置。实际上,没有人会重新分配注入的字段。

在过去,构造函数和 setter 注入是有意义的,当时开发人员通常必须手动实例化并将依赖项注入到测试对象中。如今,技术已经发展,现场注入是一个更好的选择。

【讨论】:

IDE为你生成,没有缺点。即使使用“进化技术”工具,字段注入模拟也变得困难,因为除非您检查代码,否则您不知道需要为初始化提供哪些依赖项。这些 3rd 方工具也带来了性能影响,这是单元测试的祸根。当他们只需要一个构造函数时,谁会想要使用 Weld 来执行 @PostConstruct 并依赖其他 3rd 方组件? 我知道这可能不太方便,但构造函数是您与外部通信的 API 的一部分。如果您开始使用隐藏的私有字段依赖项,则会造成不必要的混乱。通过同样的努力,我可以使用 PowerMock 来模拟静态依赖项,那么为什么还要麻烦控制反转呢? 使用构造函数注入,如果您向构造函数添加新的依赖项,您的测试将无法编译。对我来说这是一件好事。使用字段或设置器注入,您的测试仍将编译,并且可能会失败,具体取决于它们如何使用新添加的依赖项。 用***说话的问题在于它很容易被证明是错误的,因为人们只需要找到一个反例。构造函数注入仍然有用,原因与任何构造函数都很有用。字段注入强制您按原样注入字段,无需修改。构造函数注入允许您在设置字段之前转换参数。这是使用构造函数的基本好处之一,与依赖注入无关。 如果您想从我以外的其他人那里听到它,请考虑 Antonio Goncalves's blog,他在其中写道“没有真正的技术答案,除非这是您的个人喜好。在托管环境中,容器“他撰写了多本关于 Java EE 的书籍,并且是许多 JSR 的专家成员。【参考方案3】:

已接受的答案很好,但是它没有将 构造函数注入 的主要优势归功于 - 类不变性,这有助于实现线程安全性、状态安全性和更好的类可读性.

假设你有一个具有依赖关系的类,并且所有这些依赖关系都作为构造函数参数提供,那么你可以知道该对象永远不会在依赖关系无效的状态下存在。这些依赖项不需要设置器(只要它们是私有的),因此对象被实例化为完整状态或根本不实例化。

不可变对象更有可能在多线程应用程序中表现良好。尽管该类仍需要在内部实现线程安全,但您不必担心外部客户端会协调对对象的访问。

当然,这仅在某些情况下才有用。 Setter 注入 非常适合部分依赖,例如我们在一个类中有 3 个属性和 3 个 arg 构造函数和 setter 方法。在这种情况下,如果您只想传递一个属性的信息,则只能通过 setter 方法。对于测试目的非常有用。

【讨论】:

我不明白它与不变性有什么关系,您可以通过阻止对象方法修改对象本身(因此在修改时返回新实例)来获得不变性,而不是通过任何 DI 方式。但是,其余的点是有道理的。 我并不是说这要归功于 DI,我是说不使用 setter 是实现不可变对象的方法。问题是为什么要使用构造函数 DI 而不是 setter。

以上是关于为啥在 CDI 中使用构造函数而不是 setter 注入?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在我的代码中调用复制构造函数而不是移动构造函数?

JSR 303 Bean 验证 - 为啥使用 getter 而不是 setter?

JSR 303 Bean 验证 - 为啥使用 getter 而不是 setter?

为啥使用 NGRX 而不是构造函数注入服务?

为啥 GSON 使用字段而不是 getter/setter?

为啥这个父类 setter 调用使用 type(self) 而不是 self?