前言
在IDEA
中使用Spring
框架时,你有没有遇见过这样的一个提示:Field injection is not recommended?
这里可能有些小伙伴遇见过,也有些没有遇见过,没有关系,我们来解释一下这个提示意思:不推荐使用字段注入
没遇见的小伙伴不要着急扣问号,我们进入正题
注入类型
我们在使用Spring框架进行开发时,不可避免的要进行依赖注入DI(Dependency Injection),也就是把实例从Spring容器中取出来进行使用。Spring的依赖注入方式主要有三种,分别为Constructor、Setter和Field, Field 注入是使用Spring框架开发中最常用的一种。
Field
@Autowired
private DependencyA dependencyA;
@Autowired
private DependencyB dependencyB;
@Autowired
private DependencyC dependencyC;
...
那么为什么Spring/IDEA官方不推荐使用Filed注入呢?
如您所见,Field注入的实现方式非常简单直接,代码的可读性也很强。代码易于阅读。 您的类可以只关注重要的内容,而不会被DI(Dependency Injection)样板文件所污染。你只需要把 @Autowired 注解放在字段上面就行了。没有特殊的构造函数或setter,只是为了DI容器提供依赖项。Java本身是非常冗长的,所以任何缩短代码的机会都是受欢迎的,对吧?
事实上,字段注入是三种注入方式中最常见,也是最容易使用的一种。但它也是三种注入方式中最应该避免使用的,就像开篇所说,我们在IDEA
中使用字段注入时会遇到:Field injection is not recommended 的提示!为什么呢?原因有三点 :
字段注入的最大问题是:对象的外部可见性 。
假设我们有文稿所示的一个 HealthRecordService接口以及它的实现类
public interface HealthRecordService {
void recordUserHealthData();
}
@Service
public class HealthRecordServiceImpl implements HealthRecordService {
@Override
public void recordUserHealthData() {
System.out.println("HealthRecordService has been called");
}
}
其次我们有定义一个 ClientTest 类,对它进行字段注入
public class ClientTest {
@Autowired
private HealthRecordService healthRecordService;
public void recordUserHealthData(){
healthRecordService.recordUserHealthData();
}
}
显然这个这个实例只能在 ClientController 类中被访问,脱离了容器环境我们无法访问这个实例,如下文稿所示
public static void main(String[] args) {
test test = new test();
test.recordUserHealthData();
}
执行这段代码的结果就是抛出一个 NullPointerException
空指针异常
原因就在于无法在 test 类的外部实例化 HealthRecordService 对象,采用字段注入,类和容器的耦合度过高,我们无法脱离容器来使用目标对象。如果编写测试用例来保障 test 类的正确性,那么想要使用 HealthRecordService 对象,就只能采用反射机制的方式。这种做法,不符合 JavaBean
开发规范的,而且可能一直无法发现 NullPointerException
空指针异常的存在
字段注入的问题二:可能导致潜在的循环依赖
即两个类之间互相进行注入,例如下面文稿所展示的代码示例:
public class ClassA{ @Autowired private ClassB b; } public class ClassB{ @Autowired private ClassA a; }
这里的 ClassA 和 ClassB 发生了循环依赖,上述代码在 Spring 中是合法的,容器启动时并不会报任何的错误,只有在使用到具体某个 ClassA 或 ClassB 时才会报错
字段注入问题三:无法设置需要注入的对象为 final ,也无法注入那些不可变的对象
因为字段必须在类实例化时进行实例化!
基于以上三点,无论是 IDEA
,还是 Spring
官方都不推荐开发人员使用字段/Field 注入这种注入模式,而是推荐构造器注入!
面对字段注入时,请记住它三个缺陷:
-
不具备外部可见性
-
会导致循环依赖
-
无法注入不可变的对象
Constructor
构造器注入是
Spring
/IDEA
官方推荐的依赖注入类型,那么它有哪些特性呢?
构造器注入相比字段注入的优势在哪里?
别急,我们这就进入正题!
构造器注入的形式也很简单,就是通过构造函数来完成对象的注入
public class ClientTest {
private HealthRecordService healthRecordService;
//@Autowired
public ClientTest(HealthRecordService healthRecordService){
this.healthRecordService = healthRecordService;
}
public void recordUserHealthData(){
healthRecordService.recordUserHealthData();
}
}
如上实例代码所示
可以看到构造器注入能解决对象外部可见性的问题,因为 HealthRecordService 是通过 ClientTest 构造函数进行注入的,所以势必可以脱离 ClientTest 类而独立存在。
关于构造器注入,Spring
官方网站的文档有这样一段话来解释,构造注入功能特性:
The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.
这段话的核心意思在于,构造器注入能够保证注入的组件不可变并且确保需要的依赖不为空。这里的组件不为空也就意味着你可以使用 final
关键词来修饰所依赖的对象,而依赖不为空,是指所传入的依赖对象肯定是一个实例对象,避免出现空指针异常
同时基于构造注入,如果存在前面介绍的 ClassA 和 ClassB 之间的循环依赖关系,如文稿中所示,那么在Spring
容器启动时,就会抛出一个循环依赖异常,从而提醒你避免循环依赖异常
如此看来,字段注入的三大问题都可以通过使用构造器注入的方式来解决!
感谢看到这里的小伙伴,讲了这么多废话还是有小伙伴看的 (๑•̀ㅂ•́)و✧)
那难道构造注入就没有弊端了吗?答案是有的。当构造函数中存在较多的依赖对象时,大量的构造器参数会让代码显得非常冗长。
假设一个类的构造器需要n个参数,那么我们想要使用这个类时,就需要事先将这n个参数准备好,并严格按照构造器指定的顺序一 一进行传入,那么无法从代码的可读性还是维护性角度而言,这都不是很符合最佳实践的,这时候,就该我们的 Setter 方法进行注入了
Setter
public class ClientTest {
private HealthRecordService healthRecordService;
//@Autowired
public void setHealthRecordService(HealthRecordService healthRecordService) {
this.healthRecordService = healthRecordService;
}
...
public void recordUserHealthData(){
healthRecordService.recordUserHealthData();
}
}
Setter 方法注入的实现代码如文稿中所示
Setter 方法注入和构造器注入看上去有点类似,但它比构造器更有可读性,因为我们可把多个依赖对象,分别通过 Setter 方法逐一进行注入,而且 Setter 方法注入对于非强制性依赖时,注入很有用。我们可以有选择的注入一部分想注入的依赖对象,换句话说,可以实现按需注入,帮助我们只在需要时注入依赖关系。
另一方面 Setter 方法可以很好解决应用程序中的循环依赖问题,如文稿所示代码是可以正确执行的:
class ClassA{
private ClassB b;
public void setB(ClassB b) {
this.b = b;
}
}
class ClassB{
private ClassA a;
public void setA(ClassA a) {
this.a = a;
}
}
请注意,上述代码能够正确执行的前提是 ClassA 和 ClassB 的作用域都是 Singleton
(单例模式),最后通过 Setter 方法注入可以对依赖对象进行多次重复的注入,这在构造器注入中是无法实现的
在Spring3.x刚推出的时候,推荐使用注入的就是这种,笔者现在也基本没看到过这种注解方式,写起来麻烦,当初推荐Spring自然也有他的道理,这里我们引用一下Spring当时的原话:
TIP
在Spring3.x刚推出的时候,官方推荐使用 Setter 方法注入。Spring官方目前推荐的是构造器注入
那么后面为什么又换成构造器注入了呢?(喂喂喂,Spring你前一版本还贬低构造器注入,后面就立刻捧人家了不好吧,不过能用于承认自己的错误,才是真正令人称赞的地方吧 (๑•̀ㅂ•́)و✧)
总结
构造器注入适用于强制对象注入
Setter 方法注入适用于可选对象注入
Field 注入应该避免使用,它无法脱离容器而独立存在
三种方式各有利弊,从靠谱程度来说,还是构造器注入更好一些,它能有效避免一些比如循环依赖、空指针等异常的发生。另外,Spring中Bean默认为单例的,有可能会出现线程安全问题,这个时候final就更有必要了,方法没有好坏,主要是看怎么运用,官方也是只是推荐使用构造器的方法,但是其他的方法没有禁止,证明也有他们强势的地方,但是一般情况下最好都是用构造器方法,等有必要或者有一定了解后在做改动。
另外,当有一个依赖有多个实现的使用,推荐使用field注入或者setter注入的方式来指定注入的类型。这是spring官方博客对setter注入方式和构造器注入的比较。
好了,看到这里,各位对Spring依赖注入有了解了吧? 感谢各位小伙伴的观看
这篇字有点多,如果你看着看着有些厌烦了,你可以选择实在无聊
的时候再来看看
技术有限,如有错误,还望指出,互相学习,共同进步!
参考连接:
Field Dependency Injection Considered Harmful | Vojtech Ruzicka's Programming Blog