在 Spring 开发中,依赖注入(DI)是我们每天都在使用的核心功能。很多开发者习惯直接在字段上加 @Autowired,因为这样写起来最快、最简洁。但你可能不知道,Spring 官方和社区早已将构造器注入(Constructor Injection)列为首选方式

那么,@Autowired 字段注入和构造器注入到底有什么区别?为什么说后者更优秀?本文将从强制性、不变性、测试友好性、循环依赖、代码耦合等多个维度进行深度对比,并给出明确的最佳实践建议。


一、两种注入方式快速回顾

1. 字段注入(Field Injection)

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;   // 直接写在字段上

    public void doSomething() {
        userRepository.save();
    }
}

2. 构造器注入(Constructor Injection)

@Service
public class UserService {
    private final UserRepository userRepository;

    // Spring 4.3+ 如果只有一个构造器,可以省略 @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 业务方法...
}

配合 Lombok 可以更简洁:

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
}

二、核心区别一览表

维度 字段注入 (@Autowired on field) 构造器注入
依赖是否可为 null 可以(除非设置 required=true 不可以,对象创建时必须提供所有依赖
依赖是否可变 可变(字段可被后续修改) 不可变(可声明为 final,线程安全)
测试友好性 差,必须启动 Spring 容器或使用反射 极好,直接 new 对象并传入 Mock 依赖
循环依赖检测 支持(三级缓存解决,但可能掩盖设计问题) 不支持,Spring 启动即失败,强制重构
与 Spring 耦合度 高,类明确依赖 Spring 注解 低,类是纯净的 POJO
代码可读性 隐式依赖,看构造器不知道需要什么 显式依赖,所有必需项一目了然

三、深入剖析:为什么构造器注入更优秀?

1. 强制非空与不可变性

构造器注入要求对象创建时就必须提供所有依赖。你还可以把依赖字段声明为 final,确保它永远不会被修改。这带来了两个好处:

  • 安全性:依赖一旦注入,在整个对象生命周期内不变,避免了后续被意外篡改。
  • 明确性:类的使用者(包括 Spring 和测试代码)必须提供完整依赖,不可能出现“半成品”对象。

而字段注入的依赖可以是 null(如果忘记配置 Spring),而且字段通常不是 final,随时可能被修改。

2. 单元测试极其方便

构造器注入

// 单元测试中,不需要 Spring 容器
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);   // 直接 new

字段注入

// 必须启动 Spring 容器,或者用反射去设置私有字段
// 代码丑陋且脆弱
UserService service = new UserService();
Field field = UserService.class.getDeclaredField("userRepository");
field.setAccessible(true);
field.set(service, mockRepo);

显然,构造器注入让单元测试变得简单、快速、可靠。

3. 循环依赖检测更严格

  • 字段注入:Spring 通过三级缓存可以解决循环依赖(A 依赖 B,B 依赖 A)。但这会创建“半初始化”的代理对象,可能隐藏设计缺陷,甚至导致某些方法调用时依赖尚未完全准备好。
  • 构造器注入:如果存在循环依赖,Spring 直接抛出 BeanCurrentlyInCreationException 并启动失败。这是一个强力的设计校验,逼迫开发者思考并打破循环依赖(例如提取第三类、使用回调接口、改用 Setter 注入等)。

4. 符合“依赖倒置”与“单一职责”

  • 构造器参数列表直接反映了类的必需依赖。如果参数过多(超过 5-7 个),说明这个类承担了过多职责,应当拆分。而字段注入可以轻易地“偷偷”添加依赖,导致类越来越臃肿而不自知。
  • 构造器注入让类变成不可变对象(Immutable Object),更容易推理和并发安全。

5. 低耦合,不依赖 Spring 注解

使用构造器注入的类,除了业务注解(如 @Service)外,完全没有 @Autowired 这样的 Spring 专有注解。这意味着你可以轻松地将这个类用在非 Spring 环境中,或者替换 DI 框架。


四、字段注入真的毫无用处吗?

并非绝对。在以下少数场景中字段注入可能更合适:

  • 快速原型或演示代码:追求极致简洁,不考虑可测试性。
  • 可选依赖:一个依赖不是必需的,可以为 null 并提供降级逻辑。此时应使用 Setter 注入@Autowired 放在 setter 方法上,并设置 required = false)。
@Autowired(required = false)
public void setNotifier(Notifier notifier) {
    this.notifier = notifier;
}

但注意:即使是可选依赖,使用构造器注入 + Optional 也是更好的选择


五、最佳实践总结

  1. 新代码一律使用构造器注入,配合 final 关键字,让依赖不可变。
  2. 如果使用 Lombok,用 @RequiredArgsConstructor 自动生成构造器,保持代码简洁。
  3. 当构造器参数过多(超过 5 个)时,重构类,而不是改用字段注入。
  4. 可选依赖使用 Setter 注入,或者构造器注入 + Optional 参数。
  5. 绝对不要在字段上直接写 @Autowired —— 除非你正在写一个永远不需要单元测试的一次性脚本。

六、一句话记住核心

构造器注入保证了依赖的强制性、不可变性和无 Spring 耦合,它是依赖注入的正确实践;而字段注入只是语法糖,隐藏了设计问题。

从现在开始,在你的 Spring 项目中全面拥抱构造器注入吧!你的代码会更健壮、更易测试、更干净。


如果你喜欢这篇文章,欢迎点赞、收藏、转发。后续我们会深入探讨 Spring 循环依赖的三级缓存原理,以及如何重构糟糕的字段注入代码。