我们可以通过三种方式注入依赖对象:
- 构造函数注入
- Setter注入
- 字段注入
而这里的字段注入是使用 @Autowired 注解将依赖关系直接注入类中。虽然这可能是最简单的方法,但我们必须明白它可能会导致一些潜在的问题。而且,甚至Spring的官方文档也不再将字段注入列为依赖注入的选项之一。如下官方文档:
图片
2. 引发的问题
2.1 空安全(Null-Safety)
如果未正确初始化依赖关系,字段注入会产生 NullPointerException 风险。如下示例:
@Repository
public class PersonDAO {
}
@Service
public class PersonService {
// @Autowired
@Resource
private PersonDAO dao ;
public String toString() {
return "PersonService [dao=" + dao + "]";
}
}
在上面的示例中,只有容器中存在 PersonDAO 依赖项,PersonService才能正常工作。然而,在使用字段注入时,我们没有提供直接实例化 PersonService 所需的依赖关系的方法。
使用@Autowired时,启动容器会报错,这也还算好,如果我们如下使用:
@Autowired(required = false)
容器启动将正常,但是如果当调用方法时将出现NPE。这里如果你使用@Resource也会出现NPE问题。
此外,我们还可以使用默认构造函数创建 PersonService 实例:
PersonService ps = new PersonService() ;
ps.xxx() ;
这都将导致NPE问题。
对于这NPE问题,我们可以通过构造函数注入来降低NPE问题。
private final PersonDAO dao ;
public PersonService(PersonDAO dao) {
this.dao = dao ;
}
通过这种方法,我们公开了所需的依赖关系。此外,我们现在要求客户提供必须的依赖项。换句话说,如果不提供 PersonDAO 实例,就无法创建新的 PersonService 实例。
2.2 不变性(Immutability)
使用字段注入,我们无法创建不可变类。
我们需要在声明或通过构造函数实例化最终字段。此外,一旦构造函数被调用,Spring 就会执行自动装配。因此,不可能使用字段注入方式来装配最终字段。
由于依赖关系是可变的,我们无法确保它们在初始化后保持不变。此外,重新分配非最终字段可能会在运行应用程序时产生意想不到的副作用。或者,我们可以对强制依赖关系使用构造器注入,对可选依赖关系使用setter注入。这样,我们就能确保所需的依赖关系保持不变。
2.3 设计问题
违反单一职责
作为SOLID原则的一部分,单一职责原则规定每个类应该只有一项职责。换句话说,一个类应该只负责一个操作,因此只有一个改变的理由。
当我们使用字段注入时,最终可能会违反单一责任原则。我们很容易添加超出必要的依赖关系,并创建一个身兼多职的类。
另一方面,如果我们使用构造函数注入,我们就会发现,如果一个构造函数有超过几个依赖关系,我们可能会遇到设计问题。此外,如果构造函数中的参数超过 7 个,甚至集成开发环境也会发出警告。
循环依赖问题
简单地说,当两个或多个类相互依赖时,就会出现循环依赖。由于存在这些依赖关系,因此无法构造对象,执行过程中可能会出现运行时错误或无限循环。
使用字段注入可能会导致循环依赖被忽视,如下示例:
@Component
public class A {
@Autowired
private B b ;
}
@Component
public class B {
@Autowired
private A a ;
}
这里两个类通过字段注入,形成循环依赖。这样运行不会有问题也就是并不会抛出BeanCurrentlyInCreationException 异常;简单说,对于这种循环依赖其实是一种设计的缺陷我们应该避免,但是这里并没有表现出来。
注意:在Spring环境下默认是允许循环依赖的。
但是在Spring Boot环境并且从2.6版本开始循环依赖默认是不允许的,也就是上面的字段注入将抛出异常。
乘热打铁,接下来说说循环依赖的解决。
如果你将上面的依赖注入方式改成了构造函数,如下:
@Component
public class A {
private B b ;
public A(B b) {
this.b = b ;
}
}
@Component
public class B {
private A a ;
public B(A a) {
this.a = a ;
}
}
容器启动将抛出BeanCurrentlyInCreationException 异常;对于这种构造函数的循环依赖是可以通过@Lazy解决。如下示例:
// 构造函数上添加注解或者是在参数加都可以
// @Lazy
public A(@Lazy B b) {
this.b = b ;
}
只要在任意一端添加了@Lazy都可解决。
最后,我们可以使用构造器注入来代替字段注入,构造器注入是必需的,而setter注入则是可选的。