为什么在 Spring 中不推荐使用字段注入
1、概览
当我们在 IDE 中运行代码分析工具时,它可能会对带有 @Autowired
注解的字段发出 “Field injection is not recommended” 的警告。
在本教程中,我们将探讨为什么不推荐字段注入,以及我们可以使用哪些替代方法。
2、依赖注入(DI)
依赖注入是一种设计模式,用于管理对象之间的依赖关系。它通过外部容器提供对象的依赖,使得对象之间的关系更加灵活、可配置和可测试。依赖注入提高了代码的可维护性、可测试性和可扩展性。它是 Spring 框架的核心功能之一。
我们可以通过三种方式注入依赖对象,即:
- 构造函数注入。
- Setter 方法注入。
- 字段注入。
第三种方法是使用 @Autowired
注解将依赖直接注入类中。虽然这可能是最简单的方法,但它可能会导致一些潜在的问题。
更重要的是,即使是在 Spring 的官方文档 中,也不再介绍字段注入。
3、Null 安全
让我们定义 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 = new EmailService();
emailService.process("test@baeldung.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、违反单一责任原则
“单一责任原则” 规定每个类只能有一个责任。换句话说,一个类只应对一个动作负责。
当我们使用字段注入时,最终可能会违反单一责任原则。我们很容易添加超出必要的依赖,并创建一个身兼多职的类。
使用构造函数注入,如果一个构造函数有多个依赖,我们会思考这可能在设计上存在问题。构造函数参数过多,甚至 IDE 也会给出警告。
5.2、循环依赖
简单地说,当两个或多个类相互依赖时,就会出现循环依赖。由于存在这些依赖关系,因此无法构造对象,执行过程中可能会出现运行时错误或死循环。
使用字段注入可能会导致循环依赖关系被忽视:
@Component
public class DependencyA {
@Autowired
private DependencyB dependencyB;
}
@Component
public class DependencyB {
@Autowired
private DependencyA dependencyA;
}
由于依赖是在需要时注入,而不是在加载 context 时注入,因此 Spring 不会抛出 BeanCurrentlyInCreationException
异常。
通过构造函数注入,可以在编译时检测到循环依赖关系,因为它们会产生无法解决的错误。
此外,如果我们的代码中存在循环依赖,这可能表明我们的设计出了问题。因此,如果可能的话,我们应该考虑重新设计应用程序。
不过,自 Spring Boot 2.6 版本起,默认情况下不再允许循环依赖。
6、测试
单元测试揭示了字段注入方法的一个主要缺点。
假设我们想编写一个单元测试来检查 EmailService
中定义的 process()
方法是否正常工作。
首先,我们要模拟 EmailValidation
对象。但是,由于我们是通过字段注入插入 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、总结
在本文中,我们了解了不推荐使用字段注入的原因。
总之,我们可以使用构造函数注入来代替字段注入,使用 setter 方法注入来代替可选依赖。
参考:https://www.baeldung.com/java-spring-field-injection-cons