在依赖注入框架中,字段注入是一种非常流行的做法,例如Spring。然而,它有几个严重的权衡因素,一般来说应该避免。
有三种主要方式可以将你的依赖注入到你的类中。构造器、设置器(方法)和字段注入。让我们快速比较一下用所有方法注入的相同依赖的代码。
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;
@Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
this.dependencyA = dependencyA;
this.dependencyB = dependencyB;
this.dependencyC = dependencyC;
}
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;
@Autowired
public void setDependencyA(DependencyA dependencyA) {
this.dependencyA = dependencyA;
}
@Autowired
public void setDependencyB(DependencyB dependencyB) {
this.dependencyB = dependencyB;
}
@Autowired
public void setDependencyC(DependencyC dependencyC) {
this.dependencyC = dependencyC;
}
@Autowired
private DependencyA dependencyA;
@Autowired
private DependencyB dependencyB;
@Autowired
private DependencyC dependencyC;
正如你所看到的,Field
变量看起来非常漂亮。它很短,很简洁,没有模板代码。这段代码很容易阅读和浏览。
你的类可以只关注重要的东西,而不被DI的模板所污染。你只需在字段上方加上@Autowired
注解,就可以了。没有特殊的构造函数或设置函数,只是为了让DI容器提供你的依赖性。Java是非常冗长的,所以每一个能让你的代码变短的机会都是值得欢迎的,对吗?
添加新的依赖关系是非常容易的。也许太容易了。增加六个、十个甚至十几个依赖关系都没有问题。当你使用构造函数进行DI时,到了一定程度后,构造函数参数的数量变得太多,就会立刻发现有问题。
有太多的依赖关系通常意味着这个类有太多的责任。这可能是对单一责任原则和关注点分离的违反,是一个很好的指标,说明该类需要进一步检查并可能进行重构。当直接注入字段时,没有这样的红旗,因为这种方法可以无限地扩展。
使用DI容器意味着类不再负责管理它自己的依赖关系。获取依赖关系的责任从类中提取。其他的人现在负责提供依赖--DI 容器或在测试中手动分配它们。
当类不再负责获取它的依赖关系时,它应该使用公共接口--方法或构造函数来清楚地传达它们。这样就可以清楚地知道该类需要什么,以及它是可选的(设置器)还是必须的(构造器)。
DI框架的核心思想之一是管理类不应该依赖所使用的DI容器。换句话说,它应该只是一个普通的POJO,可以独立地被实例化,只要你把所有需要的依赖传递给它。
这样你就可以在单元测试中实例化它,而不启动DI容器,并单独测试它(用一个容器,这将是更多的集成测试)。
如果没有容器耦合,你可以将该类作为托管或非托管类使用,甚至可以切换到一个新的DI框架。
然而,当直接注入字段时,你没有提供直接的方式来实例化该类及其所有需要的依赖性。这意味着。
NullPointerException
。与构造函数不同,字段注入不能用于将依赖关系分配给最终字段,从而有效地使你的对象变得易变。
所以字段注入可能不是办法。剩下的是什么?Setters设置器和构造器。哪一个应该被使用?
设置器应该被用来注入可选的依赖关系。当它们没有被提供时,该类应该能够发挥作用。在对象被实例化后,可以随时改变依赖关系。这可能是也可能不是一个优势,取决于具体情况。
有时,拥有一个不可变的对象是可取的。有时,在运行时改变对象的合作者是很好的--比如JMX管理的MBeans
。
Spring 3.x文档的官方建议是鼓励使用设置器而不是构造器。
Spring团队通常提倡设置器注入,因为大量的构造器参数会变得不方便,特别是当属性是可选的时候。设置器方法也使该类的对象可以在以后进行重新配置或重新注入。通过JMX MBeans进行管理是一个引人注目的用例。
一些纯粹主义者赞成基于构造器的注入。提供所有对象的依赖性意味着对象总是以完全初始化的状态返回给客户端(调用)代码。其缺点是,对象变得不容易被重新配置和重新注入。
构造函数注入适用于强制性的依赖关系。这些是对象正常运行所需要的。通过在构造函数中提供这些字段,你可以确保对象在被构造的那一刻就可以被使用。在构造函数中分配的字段也可以是最终的,允许对象是完全不可变的,或者至少是保护它所需的字段。
使用构造函数来提供依赖关系的一个结果是,以这种方式构造的两个对象之间的循环依赖关系不再可能(与setter注入不同)。这实际上是一件好事,而不是限制,因为循环依赖应该被避免,而且通常是一个糟糕设计的标志。这种方式可以防止这种做法。
另一个好处是,如果使用spring 4.3+,你可以将你的类与DI框架完全解耦。原因是Spring现在支持隐式构造函数注入一个构造函数的场景。这意味着你不再需要在你的类中进行DI注释。当然,你也可以通过在你的Spring配置中为给定的类显式配置DI来实现同样的效果,这只是让这一切变得更容易。
从Spring 4.x开始,Spring文档的官方建议发生了变化,setter 注入的官方建议不再鼓励构造函数:
Spring团队通常提倡构造函数注入,因为它使人们能够将应用组件实现为不可变的对象,并确保所需的依赖关系不为空。此外,注入构造函数的组件总是以完全初始化的状态返回给客户端(调用)代码。 顺便提一下,大量的构造函数参数是一种不好的代码气味,意味着该类可能有太多的责任,应该重构以更好地解决适当的分离问题。
设置器注入主要应该只用于在类中可以分配合理默认值的可选依赖。否则,必须在代码使用该依赖关系的所有地方进行非空值检查。 设置器注入的一个好处是,设置器方法使得该类的对象可以在以后进行重新配置或重新注入。
自从这篇文章发表后,IDEA引入了一些贴心的支持,用于检测和轻松修复字段注入。
它可以自动从字段中移除@Autowired注解,而创建一个具有@Autowired依赖性的构造函数,有效地用构造函数注入取代了字段注入。
大部分情况下应该避免字段注入。作为替代,你应该使用构造函数或方法来注入你的依赖关系。
两者都有其优点和缺点,使用方法取决于情况。然而,由于这些方法可以混合使用,所以这不是一个非此即彼的选择,你可以在一个类中结合使用setter和constructor注入。
构造函数更适合于强制性的依赖关系和追求不变性的情况。 设置器则更适合于可选的依赖关系。