什么是字段注入,如何避免它?

296

我在一些关于Spring MVC和Portlets的帖子中读到,不建议使用字段注入。据我理解,字段注入是指像这样使用@Autowired注入Bean:

@Component
public class MyComponent {
    @Autowired
    private Cart cart;
}

在我的研究中,我也了解了构造函数注入

@Component
public class MyComponent {
    private final Cart cart;

    @Autowired
    public MyComponent(Cart cart){
       this.cart = cart;
    }
}
这两种注入方式各有优点和缺点是什么?

EDIT 1: 由于这个问题被标记为重复,我进行了检查。因为问题或答案中没有代码示例,所以我无法确定我使用的是哪种注入方式。


4
如果你所描述的领域注入(field-injection)是如此糟糕,为什么Spring会允许它?领域注入有其优点,在使代码更易读和更简洁方面。如果你在编码时足够谨慎,即使使用领域注入,也可以确保事情不会出错。 - ashes
2
@ashes 因为当时它是一个很好的功能,而且影响并没有完全被思考过。这也是Date(int,int,int)存在的原因。 - chrylis -cautiouslyoptimistic-
4个回答

469

注入类型

有三种选项来将依赖项注入到bean中:

  1. 通过构造函数
  2. 通过setter或其他方法
  3. 通过反射,直接注入字段

您正在使用第3个选项。当您将@Autowired 直接用于字段上时,就是这种情况。


注入指南

编辑:这里提到的3个链接是针对Spring 4.2的,如需查看2023年(6.09)的更新版本文档,请参见下面的列表。

Spring推荐的一般准则(请参阅基于构造函数的DI基于Setter的DI部分)如下:

  • 对于必需的依赖项或追求不变性的情况,请使用构造函数注入
  • 对于可选或可更改的依赖项,请使用Setter注入
  • 在大多数情况下避免使用字段注入

字段注入的缺点

字段注入被人们所不赞成的原因如下:

  • 无法创建不可变对象,就像使用构造函数注入一样
  • 您的类与DI容器紧密耦合,不能在其外部使用
  • 您的类无法实例化(例如在单元测试中)而不使用反射。您需要DI容器来实例化它们,这使得您的测试更像是集成测试
  • 您的真正依赖关系对外部隐藏,并且在接口中(无论是构造函数还是方法)都没有反映出来
  • 很容易有十个依赖项。如果您使用构造函数注入,您将拥有一个带有十个参数的构造函数,这将表明某些事情不对劲。但是,您可以无限制地使用字段注入添加注入的字段。具有太多依赖项是一个红旗,表示该类通常做了不止一件事,并且可能违反单一责任原则。

结论

根据您的需求,您应该主要使用构造函数注入或构造函数和setter注入的混合。字段注入有许多缺点,应该避免使用。字段注入的唯一优点是更方便编写,但这并不能抵消所有的缺点。


进一步阅读

我写了一篇关于为什么不建议使用字段注入的博客文章:字段依赖注入被认为是有害的


Spring 文档

Spring 4.2(来自原始帖子)

Spring 6.0.9(2023 年当前稳定版本)


46
告诉全世界“应避免现场注射”这个想法通常不是一个好主意,也不太好。展示利弊得失,让别人自己决定吧 ;) 许多人有其他经验和看待事物的方式。 - dieter
15
或许在这里情况是这样的,但也有其他情况,社区已经达成了普遍共识来反对某些事情。例如,就拿匈牙利命名法来说。 - Jannik
4
你提到了一些好的观点,如可测试性和依赖可见性,但我并不完全同意。构造函数注入没有缺点吗?在一个类中注入5或6个字段来执行实际调用的组合可能是可取的。我也不同意你对不变性的看法。拥有final字段并不是使一个类成为不可变的必要条件,虽然这是更好的选择。这两者是非常不同的。 - davidxxx
@Utku 已添加语句来源。 - Vojtech Ruzicka
2
我指的是答案开头的链接,它链接到Spring文档。 - Vojtech Ruzicka
显示剩余11条评论

94

这是软件开发中永无止境的讨论之一,但业界的主要影响者越来越有自己的观点,并开始建议使用构造函数注入作为更好的选择。

构造函数注入

优点:

  • 更好的可测试性。您不需要在单元测试中使用任何模拟库或Spring上下文,可以使用new关键字创建要测试的对象。此类测试始终更快,因为它们不依赖于反射机制。(30分钟后,问到了这个问题。如果作者使用构造函数注入,则不会出现这种情况)。
  • 不变性。一旦设置了依赖项,它们就无法更改。
  • 更安全的代码。执行构造函数后,您的对象已准备好使用,因为您可以验证传递的任何参数。对象可以准备好使用,也可能没有状态之间的中间步骤。使用字段注入时,您会引入一个脆弱的中间步骤。
  • 更清晰地表达必需的依赖关系。字段注入在这方面是模糊的。
  • 使开发人员思考设计。dit写了一个有8个参数的构造函数,实际上这是糟糕设计的标志和God对象反模式。无论一个类在构造函数中还是在字段中拥有8个依赖项,都是错误的。人们更不愿意通过构造函数添加更多的依赖关系而不是通过字段。它是向您的大脑发送信号的工具,表明您应该停下来并思考代码结构。

缺点:

  • 更多的代码(但现代IDE可以减轻痛苦)。

基本上,字段注入则相反。


5
测试可行性,是的,对我来说模拟注入字段的Bean是一场噩梦。一旦我使用了构造函数注入,我就不需要进行任何不必要的模拟。 - voucher_wolves
请问voucher_wolves能否举一个构造函数注入及其测试的例子? - java dev
如果您想在测试中创建新对象,只能使用构造函数注入。我通常使用InjectMocks和Mocks。在JUnit中使用new()是一个好习惯吗? - Abe
"这种绝对的说法总是有问题" - 对于像这样的极端绝对陈述,我持怀疑态度,尤其是在经历了足够多次发现代码变得更加复杂而最终无法实现的情况后。当你过于绝对并遇到边界情况时,因为你试图竭尽全力使规则严格,往往会导致事与愿违。 - The_Sympathizer

47

口味问题。 这是你的决定。

但我可以解释为什么我从不使用构造函数注入

  1. 我不想为所有我的@Service, @Repository@Controller bean实现构造函数。我的意思是,大约有40-50个bean。每次我添加新字段时都必须扩展构造函数。不,我不想也没必要。

  2. 如果您的Bean(Service或Controller)需要注入很多其他的beans怎么办? 有4个以上参数的构造函数非常丑陋。

  3. 如果我使用CDI,构造函数就不关心我了。


EDIT #1: Vojtech Ruzicka说:

类有太多的依赖关系,可能违反单一责任原则,应该重构

是的。理论与现实。这是一个示例:DashboardController映射到单个路径*:8080 / dashboard

我的 DashboardController 从其他服务中收集很多信息,以在仪表板/系统概述页面上显示它们。 我需要这个单一控制器。 所以我只需保护此一个路径(基本身份验证或用户角色过滤器)。

EDIT #2: 由于每个人都关注构造函数中的8个参数... 这是一个现实世界的例子 - 客户遗留代码。 我已经改变了。 对于我来说,同样的论点适用于4个以上的参数。

这一切都是关于代码注入,而不是实例构造。


60
有8个依赖项的构造函数看起来很丑,但实际上这是一个警示信号,说明类有太多的依赖关系,可能违反了单一职责原则,需要进行重构。这其实是一件好事。 - Vojtech Ruzicka
10
@VojtechRuzicka 这肯定不好,但有时你无法避免。 - dieter
5
我认为,对于任何一个类来说,3个依赖关系就已经是一个经验法则了,更不用说40-50个了。如果一个类有40个依赖关系,那么它肯定没有遵循单一职责原则或开闭原则,这时候你需要进行重构。 - Amin J
7
@AminJ 这个规则很好,但实际情况与之有所不同。我工作的公司已经有20多年历史了,我们有很多遗留代码。重构是个好主意,但需要花费成本。而且我不知道你为什么会这么说,但我的意思并不是40-50个依赖项,我是指40-50个bean、组件、模块... - dieter
10
@dit,你的情况明显是技术债务导致你做出次优选择。根据你自己的话,你处于一个情况下,20年以上的旧代码严重影响你的决策能力。在开始新项目时,你是否仍然推荐使用字段注入而非构造函数注入?也许你应该在回答中加上警告,指出在哪些情况下你会选择字段注入。 - Umar Farooq Khawaja
显示剩余15条评论

3

还有一点需要补充 - Vojtech Ruzicka声称Spring使用以下三种方式进行依赖注入(答案得分最高):

  1. 通过构造函数
  2. 通过setter或其他方法
  3. 通过反射,直接注入到字段中

这个答案是错误的 - 因为对于每种类型的注入,Spring都使用了反射!请使用IDE,在setter /构造函数上设置断点并检查。

这可能是一个品味问题,也可能是一个情况问题。@dieter提供了一个很好的案例,证明了字段注入更好。如果您在集成测试中使用字段注入来设置Spring上下文,则类的可测试性参数也无效 - 除非您后续要在集成测试中编写测试;)


请问您能否澄清一下所有三种注入方法都使用了反射吗?我为构造函数注入设置了断点,但没有发现任何类似反射的东西。 - wlnirvana
<init>:13,Bleh (com.wujq.cameldemo) newInstance0:-1,NativeConstructorAccessorImpl(sun.reflect) newInstance:62,NativeConstructorAccessorImpl(sun.reflect) newInstance:45,DelegatingConstructorAccessorImpl(sun.reflect) newInstance:423,Constructor(java.lang.reflect) 这是构造函数类型注入bean的调用堆栈 - topn条目。 - wujek.oczko
请阅读以下文章。字段注入比其他两种类型更昂贵,因为它依赖于反射API。https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring - Asanka Siriwardena

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