发布于 2026-01-06 0 阅读
0

你应该停止使用 Spring 的 @Autowired 注解。

你应该停止使用 Spring 的 @Autowired 注解。

或者“为什么在使用 Spring 时不应该使用字段注入”。


*简而言之 *使用@Autowired
直接将 bean 注入到字段中会使依赖项“隐藏”,并助长不良设计。请改用基于构造函数的依赖注入。


在编写 Java/Spring 应用程序代码时,您很可能已经见过`@Autowired`注解。然而,我的大多数开发者朋友并不知道,自动装配是使用 Spring IoC/DI 框架的代码库中最常见的反模式之一。

为了唤起你的记忆,这里举个简短的例子来说明我的意思。



@Service
public class UserService {

    @Autowired // Field Injection
    private UserRepository userRepository;

    // ...
}


Enter fullscreen mode Exit fullscreen mode

这看起来似乎是一种简单实用的依赖注入方法,对吧?但这样做的后果可能是让你的代码库慢慢(或很快)变成一团乱麻。

但为什么开发者总是选择这种方法呢?或许是因为它易于使用,并且在开发者需要向类中注入依赖项时,能够提供开箱即用的体验。然而,与其他不良实践一样,这种方法很容易在代码库的其他部分复制,而且通常人们不会质疑这种代码设计选择的原因。那么,使用 `@Autowired` 会导致什么后果呢?你的代码会面临耦合性过强、测试体验差依赖项隐藏等问题。

在我看来,对系统底层机制缺乏理解可能会导致代码库出现灾难性的问题。这同样适用于在使用 Spring 框架时对 `@Autowired` 注解的使用。如果开发者没有透彻掌握控制反转和依赖注入等关键概念,就更容易犯错,并陷入这种陷阱。

为了更好地理解代码设计的最佳实践,开发人员和架构师需要回顾关键概念,并探索 Spring 中不同的依赖注入方法。通过这种方式,我们可以评估哪种方法最适合我们的需求,并可能发现使用 `@Autowired` 作为依赖注入策略的缺点。

Spring的依赖注入

依赖注入是软件开发中的一种关键模式,它存在于像 Spring 这样的生产级框架中。它促进了类之间的松耦合,提高了可测试性,并有助于模块化。

图片描述

使用 Spring 框架进行依赖注入主要有三种方法:

  • 字段注入使用反射来设置私有属性/字段的值。
  • Setter注入使用公共setter来设置属性/字段的值。
  • 构造函数注入发生在创建对象本身的时候。

场注入

可以使用 Spring 通过在类字段上添加 `@Autowired` 注解来实现字段注入。那么 `@Autowired` 注解的作用是什么呢?它是 Spring 提供的一个注解,允许自动注入依赖项。当将其应用于字段、方法或构造函数时,Spring 会尝试找到所需类型的合适依赖项,并将其注入到目标类中。

@Autowired 注解本身并非万恶之源。关键在于你使用@Autowired 的位置。大多数情况下,开发者将其用于字段/属性级别。在 setter 方法中使用它虽然可以减少一些问题,但并不能完全消除。

但为什么@Autowired这么糟糕呢?基本上,@Autowired违反了一些良好的代码设计原则。

依赖倒置(D 来自 SOLID)

如果你想在应用程序容器之外使用你的类,例如用于单元测试,则必须使用 Spring 容器来实例化你的类,因为除了反射之外没有其他方法可以设置 @Autowired 字段。

当使用基于字段的依赖注入并结合 @Autowired 注解时,类本身会将这些依赖项对外部隐藏起来。换句话说,不存在注入点。

下面你可以看到一个这样的例子。



@Component
public class Calculator {

   @Autowired 
   private Multiplier multiplier;

   public int add(int a, int b) {
      return a + b;
   }

   public int multiply(int a, int b) {
      return multiplier.multiply(a, b);
   }
}

@Component
public class Multiplier {
   public int multiply(int a, int b) {
      return a * b;
   }
}



Enter fullscreen mode Exit fullscreen mode

尽管 Spring 在运行时会注入依赖项(Multiplier 的一个实例),但无法手动注入依赖项,例如在对此类进行单元测试时。

下面的单元测试类可以编译,但在运行时很可能会抛出 NullPointerException 异常,为什么呢?因为 Calculator 和 Multiplier 之间的依赖关系没有得到满足。



public class CalculatorTest {

   @Test
   public void testAdd() {
      Calculator calculator = new Calculator();
      int result = calculator.add(2, 3);
      assertEquals(5, result);
   }

   @Test
   public void testMultiply() {
      Calculator calculator = new Calculator();
      int result = calculator.multiply(2, 3);
      assertEquals(6, result);
   }
}



Enter fullscreen mode Exit fullscreen mode

整洁代码 / 尖叫架构 / 单一职责原则(SOLID 中的 S)

代码必须清晰地体现其设计理念。单一职责原则(SRP)指出,一个类应该只有一个修改的理由。这意味着一个类应该只有一个职责或关注点。`@Autowired` 注解本身并不违反这一原则。

然而,如果一个类承担了多个职责,并且通过 `@Autowired` 注解注入了多个依赖项,这很可能违反了单一职责原则 (SRP)。在这种情况下,最好重构该类,并将这些职责提取到单独的类中。

否则,如果使用基于构造函数的依赖注入,随着类中依赖项的增加,构造函数会变得越来越大,代码开始出现问题,发出明显的错误信号。罗伯特·C·马丁(笔名“鲍勃大叔”)在其著作《代码整洁之道/尖叫架构》中对此进行了描述。

复杂

@Autowired 注解会使代码更加复杂,尤其是在处理循环依赖时。当两个或多个类相互依赖时,很难确定它们的实例化顺序。这可能会导致难以调试的运行时错误。


你的项目很可能根本不需要 @Autowired 注解。


应该用什么代替呢?

简而言之,就是构造函数注入

在面向对象编程中,我们通过调用构造函数来创建对象。如果构造函数需要所有必需的依赖项作为参数,那么我们可以百分之百确定,该类永远不会在未注入其依赖项的情况下被实例化。

下面是一个使用基于构造函数的依赖注入而不是 @Autowired 的示例。



@Component
public class Calculator {

   private Multiplier multiplier;

   // Default constructor. It will be used by Spring to 
   // inject the dependencies declared here.
   public Calculator(Multiplier multiplier) {
      this.multiplier = multiplier;
   }

   public int add(int a, int b) {
      return a + b;
   }

   public int multiply(int a, int b) {
      return multiplier.multiply(a, b);
   }
}

@Component
public class Multiplier {
   public int multiply(int a, int b) {
      return a * b;
   }
}

public class CalculatorTest {

   @Test
   public void testAdd() {
      Calculator calculator = new Calculator(mock(Multiplier.class));
      int result = calculator.add(2, 3);
      assertEquals(5, result);
   }

   @Test
   public void testMultiply() {
      Multiplier mockMultiplier = mock(Multiplier.class);
      when(mockMultiplier.multiply(2, 3)).thenReturn(6);
      Calculator calculator = new Calculator(mockMultiplier);
      int result = calculator.multiply(2, 3);
      assertEquals(6, result);
   }
}



Enter fullscreen mode Exit fullscreen mode

现在,两个组件之间的依赖关系已明确声明,并且代码设计允许在运行时注入 Multiplier 的模拟实例。

此外,构造函数注入有助于创建不可变对象,因为构造函数是创建对象的唯一途径。一旦创建了 bean,我们就无法再更改其依赖项。另一方面,使用 setter 注入可以在创建后注入依赖项或更改依赖项,从而导致对象可变。可变对象在多线程环境中可能不具备线程安全性,并且由于其可变性,调试起来也更加困难。

通过显式地定义类对构造函数的依赖关系,可以使代码更易于维护测试灵活

  • 易于维护,因为您只需依靠面向对象编程概念来注入类依赖项,并且对类依赖项契约的任何更改都将毫不费力地传达给其他类。
  • 之所以可测试,是因为你提供了一个注入点,例如,它允许单元测试通过一个模拟对象而不是具体的实现。此外,我们通过仅允许在构造时进行注入,使该类成为不可变的。
  • 灵活之处在于,您仍然可以通过向默认构造函数添加更多参数,以最小的努力向类添加更多依赖项。

这种方法可以更轻松地理解和跟踪类的依赖关系。此外,它还强制要求你在编码时显式地为所需的依赖项提供模拟对象,从而更容易对类进行隔离测试。

注射

使用 Spring 进行依赖注入的另一种方法是使用 setter 方法注入依赖项的实例。

下面你可以看到在使用 Spring 时 Setter 注入的示例。



public class UserService {
    private UserRepository userRepository;        

    ...

    @Autowired // Setter Injection
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    ...
}



Enter fullscreen mode Exit fullscreen mode

这种方法可能会导致以下问题:

  • 可选依赖项。Setter 注入允许使用可选依赖项,但如果未正确检查依赖项的空值,则可能导致空指针异常。
  • 对象状态不完整。如果对象仅部分构造,并且调用了 setter 方法,则可能导致对象处于不完整状态,这可能会再次导致意外行为或空指针异常。
  • 隐藏依赖项。由于依赖项未在构造函数中显式声明,因此难以理解代码及其依赖关系。

尽管存在一些缺点,但 Setter 注入介于 Field 注入和构造函数注入之间,因为至少我们有一个公共方法,允许开发人员注入模拟对象而不是实际实现,从而使类更容易测试。

进一步考虑

在 Spring Framework 4.3 或更高版本中,可以省略@Autowired注解,Spring 会使用默认构造函数,并在类由应用程序上下文管理时自动注入所有必要的依赖项。( https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-constructor-vs-setter-injection

既然Spring 团队决定`@Autowired` 注解对于默认构造函数应该是可选的,那么我们可以得出结论:它对 Spring 框架的决策没有任何帮助,它的存在纯粹是多余的。把它去掉吧!

Spring Framework 开发团队的以下评论又给了我们一个选择构造函数注入作为默认注入方法的理由。

此外,“构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。”以及“Setter 注入主要用于可以在类中分配合理默认值的可选依赖项。”

简而言之,他们的意思是,通过使用构造函数注入,可以保证类处于可直接使用的状态。如果使用 `@Autowired` 注解或 setter 注入,Spring 可能会在类实例处于错误状态时将其实例化,从而导致意外行为,例如 `NullPointerException`。

@Autowired 存在的意义是什么?

如果需要注入可选或可变的依赖项,可以使用 `@Autowired` 注解将 setter 方法(或非默认构造函数)标记为注入点。但这仅在依赖项是可选或可变的,并且实现能够处理该依赖项缺失(空值)的情况下才有意义。

如果存在多个构造函数且没有主构造函数/默认构造函数,则至少需要使用 `@Autowired` 注解来指示 Spring 使用哪个构造函数。在这种情况下,可以考虑使用 `@Autowired` 注解。

结论

归根结底,`@Autowired` 对于小型、简单的项目来说可能很有用,但对于大型、复杂且即将投入生产环境的项目来说,它并非最佳选择。这类项目的代码设计需要具备开放性、灵活性和易于测试的特点。因此,建议使用类构造函数进行显式依赖注入。这样做可以确保代码保持健壮性和适应性。

你和你的团队将免费获得的一项好处是,项目的其余部分将自然而然地体现出这一代码设计决策。有时,开发人员从未真正学习过正确的依赖注入、类契约,甚至如何有效地测试代码,仅仅是因为他们使用了 `@Autowired` 注解,却从未质疑其真正的必要性和适用性。

延伸阅读和参考资料

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-constructor-vs-setter-injection
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-autowired-annotation
https://betulsahinn.medium.com/why-is-autowired-annotation-not-recommended-4939c46da1f8
https://blog.marcnuri.com/field-injection-is-not-recommended
https://reflectoring.io/constructor-injection/
https://kinbiko.com/posts/2018-02-13-spring-dependency-injection-patterns/
https://blog.frankel.ch/my-case-against-autowiring/
https://www.linkedin.com/pulse/when-autowire-spring-boot-omar-ismail/?trk=articles_directory
https://eng.zemosolabs.com/when-not-to-autowire-in-spring-spring-boot-93e6a01cb793
https://stackoverflow.com/questions/41092751/spring-injects-dependencies-in-constructor-without-autowired-annotation

文章来源:https://dev.to/felixcoutinho/you-should-stop-using-spring-autowired-p8i