为什么在 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