1. 概述
当我们在IDE中运行代码分析工具时,可能会看到带有@Autowired
注解的字段出现“字段注入不推荐”的警告。
本文将探讨为什么不推荐字段注入,以及我们可以采用哪些替代方案。
2. 依赖注入
对象直接使用其依赖对象而无需显式定义或创建它们的过程称为依赖注入。这是Spring框架的核心功能之一。
我们可以通过三种方式注入依赖对象:
- 构造器注入
- Setter注入
- 字段注入
第三种方式通过[@Autowired](/spring-autowire)
注解直接将依赖注入到类中。虽然这种方式最简单,但必须了解它可能带来的潜在问题。
更重要的是,Spring官方文档已不再将字段注入列为推荐的DI选项。
3. 空安全风险
字段注入在依赖未正确初始化时存在NullPointerException
风险。
定义EmailService
类并使用字段注入添加EmailValidator
依赖:
@Service
public class EmailService {
@Autowired
private EmailValidator emailValidator;
}
添加process()
方法:
public void process(String email) {
if(!emailValidator.isValid(email)){
throw new IllegalArgumentException(INVALID_EMAIL);
}
// ...
}
EmailService
只有在提供EmailValidator
依赖时才能正常工作。但使用字段注入时,我们没有提供直接实例化EmailService
并传入必需依赖的方式。
更糟糕的是,我们可以通过默认构造器创建EmailService
实例:
EmailService emailService = new EmailService();
emailService.process("test@example.com");
执行上述代码会抛出NullPointerException
,因为我们没有提供其必需的依赖EmailValidator
。
改用构造器注入可以降低NullPointerException
风险:
private final EmailValidator emailValidator;
public EmailService(final EmailValidator emailValidator) {
this.emailValidator = emailValidator;
}
这种方式公开了必需的依赖,并强制客户端提供这些依赖。换句话说,没有EmailValidator
实例就无法创建EmailService
对象。
4. 不可变性
使用字段注入时,我们无法创建不可变类。
final
字段必须在声明时或通过构造器初始化。而Spring是在构造器调用之后才执行自动装配的。因此,通过字段注入无法自动装配final
字段。
由于依赖是可变的,我们无法确保它们在初始化后保持不变。此外,重新赋值非final
字段可能在应用运行时导致意外的副作用。
替代方案:对必需依赖使用构造器注入,对可选依赖使用Setter注入。这样可以确保必需依赖保持不变。
5. 设计问题
5.1. 违反单一职责原则
作为SOLID原则的一部分,单一职责原则要求每个类只承担一个职责。换句话说,一个类应该只负责一项功能,因此只有一个变更理由。
使用字段注入时,我们可能无意中违反单一职责原则。我们很容易添加过多依赖,创建出承担多项职责的类。
相反,如果使用构造器注入,当构造器参数过多时(比如超过7个),IDE会发出警告,提醒我们可能存在设计问题。
5.2. 循环依赖
简单来说,循环依赖指两个或多个类相互依赖。由于这种依赖关系,对象无法被正确构造,可能导致运行时错误或无限循环。
使用字段注入可能导致循环依赖被忽略:
@Component
public class DependencyA {
@Autowired
private DependencyB dependencyB;
}
@Component
public class DependencyB {
@Autowired
private DependencyA dependencyA;
}
因为依赖是在需要时才注入(而非上下文加载时),Spring不会抛出BeanCurrentlyInCreationException
异常。
而使用构造器注入时,循环依赖会在编译时暴露,因为它们会导致无法解析的错误。
此外,代码中出现循环依赖通常是设计有问题的信号。如果可能,应该考虑重构应用。
注意:从Spring Boot 2.6开始,默认禁止循环依赖。
6. 测试困难
单元测试揭示了字段注入的主要缺陷之一。
假设我们要为EmailService
的process()
方法编写单元测试。
首先需要模拟EmailValidator
对象。但由于使用字段注入,我们无法直接替换为模拟对象:
EmailValidator validator = Mockito.mock(EmailValidator.class);
EmailService emailService = new EmailService();
如果在EmailService
中添加setter方法,会引入新的风险:其他类(不仅仅是测试类)也能调用该方法。
不过我们可以通过反射来实例化类,例如使用Mockito:
@Mock
private EmailValidator emailValidator;
@InjectMocks
private EmailService emailService;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
}
Mockito会尝试通过@InjectMocks
注入模拟对象。但如果字段注入失败,Mockito不会报告错误。
而使用构造器注入时,我们可以直接提供依赖,无需反射:
private EmailValidator emailValidator;
private EmailService emailService;
@BeforeEach
public void setup() {
this.emailValidator = Mockito.mock(EmailValidator.class);
this.emailService = new EmailService(emailValidator);
}
7. 结论
本文探讨了不推荐字段注入的几个关键原因:
✅ 空安全风险:可能忽略必需依赖导致NPE
❌ 不可变性缺失:无法声明final字段
⚠️ 设计问题:容易违反单一职责原则,隐藏循环依赖
🧪 测试困难:需要反射或特殊工具才能模拟依赖
总结:对必需依赖使用构造器注入,对可选依赖使用Setter注入。
本文源码可在GitHub获取。