人人都会AI编程

3.4 依赖注入实现原理:构造器注入、Setter 注入、字段注入

更新时间:2026-07-10

依赖注入是 Spring IoC 容器最核心的动作,它负责将 Bean 所需的协作者自动“推送”到正确的位置。Spring 支持三种注入方式:构造器注入Setter 注入字段注入。虽然最终都是在创建 Bean 时完成依赖的赋值,但它们的实现时机、设计意图和工程效果存在明显差异。

3.4.1 构造器注入

构造器注入是指通过类的构造方法将依赖关系传入。Spring 在实例化 Bean 时,会根据构造器的参数类型和名称,在容器中找到匹配的 Bean 并依次传入。

实现原理简述
容器通过 BeanDefinition 中记录的构造器参数信息,利用反射调用目标构造器。如果只有一个构造器,Spring 会直接使用它;如果有多个构造器,则通过 @Autowired 或根据参数数量、类型进行推断(从 Spring 4.3 开始,单构造器可省略 @Autowired)。容器在调用构造器之前,必须确保所有构造器参数对应的 Bean 已经创建完毕,否则会抛出 NoSuchBeanDefinitionException

实际写法

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;

    // 构造器注入,依赖不可变
    public OrderService(OrderRepository orderRepository, 
                        PaymentGateway paymentGateway) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
    }
}

如果依赖是强制的(不可为 null),构造器注入是最佳选择,因为对象创建完毕时所有依赖就已经就绪,不可能出现“半初始化”状态。使用 final 字段可以进一步确保依赖在对象生命周期内不会被篡改。

特点和适用场景

  • 强制依赖:构造器参数通常是 Bean 正常工作的必需组件,缺失会导致容器启动失败,避免了运行时 NPE。
  • 不可变性:支持 final 字段,更符合函数式和不可变对象的设计原则。
  • 测试便利:单元测试时可以直接通过 new 创建对象并手动传入 Mock 依赖,无需启动 Spring 容器。
  • 循环依赖限制:构造器注入无法解决循环依赖(A 构造器需要 B,B 构造器需要 A),会导致 BeanCurrentlyInCreationException。这种情况通常需要重构设计,或者改用 Setter/字段注入并结合三级缓存机制。

3.4.2 Setter 注入

Setter 注入是指通过无参构造器(或静态工厂)实例化 Bean 后,再通过调用 setter 方法将依赖赋予对象。

实现原理简述
容器在完成 Bean 的实例化和属性填充阶段,会查找 BeanDefinition 中的 propertyValues(XML 配置)或处理 @Autowired 标注的 setter 方法。与构造器注入不同,Setter 注入发生在对象已经存在之后,因此存在一个短暂的“依赖尚未就绪”的窗口。

实际写法

@Service
public class OrderService {
    private OrderRepository orderRepository;
    private PaymentGateway paymentGateway;

    // 无参构造器(可省略)
    public OrderService() {}

    @Autowired
    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Autowired
    public void setPaymentGateway(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

上述代码展示了用 @Autowired 标注 setter 方法的典型写法。如果某个依赖不是必须的,可以在 Autowired 中设置 required = false,但更现代的做法是通过 java.util.Optional@Nullable 来表达可选性,不过这些通常配合构造器注入使用。

特点和适用场景

  • 可选依赖与重配置:当某个依赖不是必需时(例如缓存组件是可选增强),Setter 注入允许在运行时动态替换或重新配置依赖,这是构造器注入无法做到的。
  • 避免过长构造器列表:如果一个类的依赖非常多,构造器参数列表会臃肿,Setter 注入可分散设置。但现代实践更倾向依赖数目过多时应考虑拆分类,而非滥用 Setter。
  • 循环依赖问题:结合 Spring 的三级缓存机制,Setter 注入可以解决部分循环依赖,因为对象可以先创建出来(不完全初始化),再通过 setter 补充依赖,避免构造器阶段的死锁。
  • 侵入性稍强:setter 方法暴露了对象内部状态的可变接口,破坏了不可变性原则。在并发环境下,依赖的意外更改可能引发问题。

3.4.3 字段注入

字段注入直接通过 @Autowired 注解标注在 private 字段上,不再需要构造器或 setter 方法。

实现原理简述
这是最简洁的写法,但也是最具争议的。Spring 在创建 Bean 后,通过反射直接访问字段(无视 private 修饰符)来注入依赖。整个过程对开发者完全透明,但本质上绕过了 Java 的类型安全和封装性。容器通过 InjectionMetadata 内省得到需要注入的字段列表,逐个从容器获取依赖并通过 Field.set(obj, value) 赋值。

实际写法

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentGateway paymentGateway;
}

特点和适用场景(与告诫)

  • 代码极其简洁:无样板代码,类看起来干净,常出现在快速原型或遗留代码中。
  • 隐藏显式依赖:阅读类签名无法立刻知道它需要哪些外部依赖,必须深入字段,破坏了“最小惊奇”原则,降低可读性。
  • 测试困难:单元测试时无法通过构造器或 setter 轻易传入 Mock 对象,必须依赖反射或启动 Spring 测试容器。这导致单元测试变得笨重。
  • 强制绑定容器:该类脱离了 Spring 容器无法工作,因为没有任何公开方式可以正常提供这些依赖。这在 POJO 的重用和迁移上形成阻碍。
  • 潜在的不变性缺失:字段一般不为 final,可能被意外修改,线程安全性需额外关注。

工程建议:官方文档和社区主流观点均推荐构造器注入作为首选方式,尤其对强制依赖。字段注入应尽量避免在正式业务代码中使用,它适合的场景仅限于快速演示、测试上下文中的配置类,或者某些难以更改的遗留系统。在实际项目中,一旦养成只使用构造器注入的习惯,会发现代码的可测试性和健壮性都会明显提升。

3.4.4 三种方式的对比与选择策略

| 方式 | 强制依赖 | 可选依赖 | 不可变性 | 循环依赖 | 测试友好度 | 代码简洁度 |
|------|----------|----------|----------|----------|--------------|--------------|
| 构造器注入 | ✅ 完美 | 需特殊处理 | ✅ 支持 final | ❌ 无法解决 | ⭐⭐⭐ | 中等(有构造器代码) |
| Setter 注入 | ✅ 可以 | ✅ 天然支持 | ❌ 可变 | 🟡 可缓解 | ⭐⭐ | 较差(setter 繁多) |
| 字段注入 | ✅ 可以 | ✅ 可以 | ❌ 可变 | 🟡 可缓解 | ⭐ | ⭐⭐⭐(但隐藏了依赖) |

选择策略

  1. 强制依赖一律使用构造器注入,并尽可能将字段声明为 final,确保初始化后不可变。
  2. 可选依赖可以考虑 Setter 注入,但现代的更好做法是使用构造器注入 + Optional (Java 8+) 或 @Nullable,保持不可变性。
  3. 避免字段注入,除非在受控的测试配置类中,或是在无法修改的遗留代码中作为过渡方案。
  4. 循环依赖应首先审视设计是否合理,尝试提取公共接口或使用 @Lazy 解决,而非依赖注入方式上的差异来回避问题。

在实际的 Spring 应用开发中,遵循 “优先构造器注入” 原则,可以自然地提高代码的健壮性、可测试性和可维护性,这也是 Spring 团队在多个场合明确推荐的实践。理解这三种注入方式的原理后,你将能在不同场景下做出有根据的选择,而不仅仅是模仿示例代码。