依赖注入是 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 繁多) |
| 字段注入 | ✅ 可以 | ✅ 可以 | ❌ 可变 | 🟡 可缓解 | ⭐ | ⭐⭐⭐(但隐藏了依赖) |
选择策略:
- 强制依赖一律使用构造器注入,并尽可能将字段声明为
final,确保初始化后不可变。 - 可选依赖可以考虑 Setter 注入,但现代的更好做法是使用构造器注入 +
Optional(Java 8+) 或@Nullable,保持不可变性。 - 避免字段注入,除非在受控的测试配置类中,或是在无法修改的遗留代码中作为过渡方案。
- 循环依赖应首先审视设计是否合理,尝试提取公共接口或使用
@Lazy解决,而非依赖注入方式上的差异来回避问题。
在实际的 Spring 应用开发中,遵循 “优先构造器注入” 原则,可以自然地提高代码的健壮性、可测试性和可维护性,这也是 Spring 团队在多个场合明确推荐的实践。理解这三种注入方式的原理后,你将能在不同场景下做出有根据的选择,而不仅仅是模仿示例代码。