Spring Setter注入(Setter injection)和构造注入(Constructor injection)
平时使用Spring少不了各种注入,大部分时候都是直接在属性上@Autowired就完了,偶尔看到在构造方法上注入的也知道怎么回事。今天忽然想到这俩有什么区别,了解了下使用场景。
References:
- https://spring.io/blog/2007/07/11/setter-injection-versus-constructor-injection-and-the-use-of-required/ 官方在07年的一篇文章,观点至今都非常好,本文大概翻译一下
- https://stackoverflow.com/questions/40620000/spring-autowire-on-properties-vs-constructor
首先从构造注入说起吧,任何一个有参数的构造器在不传递参数时是不能被创建的。在Java中,有默认的无参构造方法,所以Spring可以在不传递任何参数的情况下实例化class。换句话说,添加带参的构造方法后,可以强制用户在实例化class时传递参数。
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 Framework本身中看到大量的setter注入的原因之一。在Spring本身中使用了setter注入,以及我们提倡使用它的事实,也导致许多第三方软件开始使用setter注入,博客和文章也开始提到setter注入。
(顺便说一句,还有人记得类型1、2和M控制反转吗?;-))
- 框架需要更多的可配置性
第二个原因是,一般而言,像Spring这样的框架更适合通过setter注入进行配置,而不是通过构造函数注入进行配置。这主要是因为需要配置的框架通常包含许多可选值。使用构造函数注入可选配置将导致不必要的混乱和激增的构造函数,尤其是在与类继承结合使用时。
出于这两个原因,我认为构造函数注入对于应用程序代码比对框架代码更有用。在应用程序代码中,本质上您对需要配置的可选值的需求减少了(应用程序代码在许多情况下不太可能使用可配置的属性)。其次,与框架代码相比,应用程序代码使用类继承的频率要少得多。在应用程序代码中,应用程序的专业化并不像在框架代码中那样频繁,应用程序代码中的数量要少得多。
你应该用什么呢?
我们通常建议人们对所有必需的参数使用构造函数注入,对其他属性使用setter注入。同样,构造函数注入可确保强制的参数都能被注入,并且不可能以无效状态实例化对象。换句话说,在使用构造函数注入时,不必使用专门的机制确保设置了必需的依赖项。
不使用构造函数注入的另一个原因是构造函数中缺少参数名称,并且这些名称未出现在XML中。我认为,在大多数应用中,这无关紧要。首先考虑使用setter注入的变量:
<bean id="authenticator" class="com.mycompany.service.AuthenticatorImpl"/>
<bean id="accountService" class="com.mycompany.service.AccountService">
<property name="authenticator" ref="authenticator"/>
</bean>
此版本将authenticator作为属性名称以及Bean名称提及。这是我经常遇到的模式。我会争辩说,在使用构造函数注入时,缺少构造函数参数名称(以及那些未出现在XML中的参数)并没有真正使我们感到困惑。
<bean id="authenticator" class="com.mycompany.service.AuthenticatorImpl"/>
<bean id="accountService" class="com.mycompany.service.AccountService">
<constructor-arg ref="authenticator"/>
</bean>
(博主注:这段是针对早期只有XML方式注入而言的,现在大多是注解注入,影响基本可以忽略了)
为什么不检查所需的依赖?
实际上,有些人根本不检查依赖项是否已正确设置。人们不这样做的最大原因是因为他们很快就发现了他们启动了ApplicationContext并自动注入了依赖。这当然是真的。例如,如果您使用的是Spring的集成测试,可以让Spring加载应用上下文。如果确信在集成测试中对某些实际代码进行了测试,则可以保证必须的依赖都被注入了。这种方法虽然让我有些困扰。不过,您必须对覆盖代码的测试有信心,因为如果测试没有覆盖到所有依赖,就会将这个问题遗漏!当然,在部署应用程序时进行冒烟测试可能会发现这个问题,但是我不想成为只在运行时发现缺少依赖项的人!
结论
关于构造函数注入与setter注入有很多话要说,我知道很多人仍然更喜欢setter注入。我认为,尽管构造函数注入可以检查依赖关系是更好的方式(对于没有很多可选参数的代码)。将其与final字段结合起来可以立即为您带来在多线程环境中提高安全性的其他好处,并且由于通常也不需要太多处理,因此在本博客文章中不再赘述。
在某些情况下,我不会使用构造函数注入。例如,其中之一是具有很多依赖项或其他可配置值的值。我个人不认为带有20个参数的构造函数是一个不错的代码示例。当然,问题是,如果一个具有20个依赖项的类没有太多职责……
可以肯定的是,我不会在业务方法中检查必须的依赖。
现在,属性注入已不被推荐