Spring 为什么不推荐使用字段注入?

本文探究了在spring应用中通过字段注入的弊端,以及代替方式。

1, 概览

当我们在IDE中运行代码分析工具时,它可能会对带有 @Autowired 注解的字段发出 “Field injection is not recommended(不推荐字段注入)” 的警告。

在本教程中,我们将探讨为什么不推荐字段注入,以及我们可以使用哪些替代方法。

2,依赖注入(DI)

对象使用其依赖对象而不需要定义或创建它们的过程被称为依赖注入。它是Spring框架的核心功能之一。

我们可以通过三种方式注入依赖对象,使用:

  • 构造器注入
  • Setter 方法注入
  • 字段注入

这里的第三种方法是使用 @Autowired 注解将依赖直接注入类中。虽然这可能是最简单的方法,但我们必须明白,它可能会引起一些潜在的问题。

更重要的是,即使是 官方的Spring文档 也没有提供字段注入作为DI选项之一了。

3, Null-Safety

如果依赖没有被正确初始化,字段注入会产生 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);
    }
    // ...
}

只有当我们提供 EmailValidator 的依赖时,EmailService 才能正常工作。然而,使用字段注入,我们没有提供一个直接的方式来实例化具有所需依赖的 EmailService

此外,我们能够使用默认的构造函数来创建 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, 违反单一责任原则

作为SOLID原则的一部分,单一责任原则指出每个类应该只有一个责任。换句话说,一个类应该只对一个行动负责,因此,只有一个理由可以改变。

当我们使用字段注入时,我们可能最终违反了单一责任原则。我们可以很容易地添加更多的依赖,并创建一个做着不止一项工作的类。

另一方面,如果我们使用构造函数注入,我们会注意到,如果一个构造函数有几个以上的依赖,我们可能在设计上有问题。此外,如果构造函数中有七个以上的参数,甚至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() 方法是否正常工作。

首先,我们想模拟一下 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 注解来注入mock。然而,如果字段注入策略失败了, Mockito 就不会报告这个失败。

另一方面,使用构造函数注入,我们可以在没有反射的情况下提供所需的依赖:

private EmailValidator emailValidator;

private EmailService emailService;

@BeforeEach
public void setup() {
   this.emailValidator = Mockito.mock(EmailValidator.class);
   this.emailService = new EmailService(emailValidator);
}

7,总结

在这篇文章中,我们了解了为什么不推荐字段注入的原因。

总而言之,我们可以用构造函数注入来代替字段注入,来处理必需的依赖,用 setter 函数注入来处理可选的依赖。


原文: Why Is Field Injection Not Recommended? | Baeldung