依赖注入是实现控制反转的核心手段,但注入方式的选择直接影响代码的可测试性、可读性和长期可维护性。Spring 支持三种注入方式:字段注入、Setter 方法注入和构造器注入。其中,构造器注入被社区和 Spring 官方团队一致推荐为首选方式,这并非风格偏好,而是基于大量实践经验得出的工程准则。
22.1.1 三种注入方式回顾与对比
先看三种注入方式在同一场景下的写法差异:
1. 字段注入
@Service
public class OrderService {
@Autowired
private OrderRepository repository;
@Autowired
private PaymentGateway paymentGateway;
}
2. Setter 方法注入
@Service
public class OrderService {
private OrderRepository repository;
private PaymentGateway paymentGateway;
@Autowired
public void setRepository(OrderRepository repository) {
this.repository = repository;
}
@Autowired
public void setPaymentGateway(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
3. 构造器注入
@Service
public class OrderService {
private final OrderRepository repository;
private final PaymentGateway paymentGateway;
public OrderService(OrderRepository repository, PaymentGateway paymentGateway) {
this.repository = repository;
this.paymentGateway = paymentGateway;
}
}
在构造器注入中,若只有一个构造器,Spring 会自动识别并完成注入,@Autowired 注解可以省略。Lombok 的 @RequiredArgsConstructor 也能进一步减少样板代码,但从可读性考虑,显式编写构造器有时更易于理解依赖关系。
22.1.2 构造器注入的核心优势
1. 显式的强制依赖声明
构造器参数直接告诉阅读者“这个类正常工作必须依赖哪些组件”。如果依赖缺失,编译阶段就会报错,而不会等到运行时抛出空指针异常。字段注入和 Setter 注入则无法提供这种编译期安全性——使用者难以一目了然地知道一个类到底需要哪些依赖,只有在查看所有字段或 setter 签名后才能拼凑出完整图景。
2. 支持不可变性(final 字段)
构造器注入允许将依赖声明为 final,保证对象在构造完成后其依赖不会被外部或内部代码意外篡改。这符合“创建即完整”的设计原则,也避免了并发场景下可能的状态不一致。字段注入的依赖是默认可变的,对象在其生命周期中可以被任意重新赋值,埋下难以排查的隐患。
3. 纯正的 POJO,不依赖 Spring 注解
使用构造器注入的业务类,不包含任何 Spring 注解(构造器可省略 @Autowired)。这意味着该类是完全独立的 POJO,可以在不启动 Spring 容器的前提下,通过 new 关键字直接实例化,并传入 Mock 对象进行单元测试。这种低侵入性正是 Spring 一贯倡导的设计哲学。
4. 防止循环依赖的隐蔽陷阱
字段注入和 Setter 注入会掩盖循环依赖问题,因为 Spring 可以先创建对象并缓存半成品,再通过反射注入依赖,使得循环依赖在启动时不报错,但运行时可能引发难以追踪的调用异常。而构造器注入要求在对象创建时所有依赖必须就绪,一旦存在循环依赖,容器启动就会抛出 BeanCurrentlyInCreationException,强制开发者及早解决架构上的不合理设计。
5. 测试友好性极为突出
测试一个使用构造器注入的类,不需要任何 Spring Test 工具的介入,只需在测试代码中手动 new 出待测对象,并将依赖的 Mock 实例通过构造器传入:
@Test
void shouldThrowWhenAmountInvalid() {
OrderRepository mockRepo = mock(OrderRepository.class);
PaymentGateway mockGateway = mock(PaymentGateway.class);
OrderService service = new OrderService(mockRepo, mockGateway);
assertThrows(IllegalArgumentException.class,
() -> service.placeOrder(new Order(-100)));
}
而字段注入的类必须通过反射设置私有字段,或者启动 Spring 测试容器,测试代码与框架深度耦合,执行速度和可维护性大打折扣。
22.1.3 Setter 注入与字段注入的合理使用场景
尽管构造器注入是首选,但在特定情况下,其他注入方式依然有其存在价值:
- 可选依赖:如果某个依赖不是组件运行的必须条件(例如一个非必须的缓存管理器,缺失时服务降级运行),可以使用 Setter 注入(配合
@Autowired(required = false)),让依赖变为可选的。不过这种场景在实际业务中并不多见,更推荐通过构造器注入一个“空对象”实现(Null Object Pattern)或提供默认实现。 - 循环依赖的临时缓解:在遗留系统中,可能出现难以立即重构的循环依赖,此时 Setter 注入可以作为过渡方案。但必须明确这只是技术债务的临时出口,后续应通过抽取中间层或事件机制彻底解耦。
- 字段注入在代码简洁性上的考量:字段注入只需要一个注解,代码量最少,因此在一些示例、快速原型或配置类内部(
@Configuration类中使用@Autowired注入依赖 Bean)依然常见。但在生产级业务代码中,不应以牺牲长期可维护性为代价追求短暂的简洁。
22.1.4 IntelliJ IDEA 与团队的强制约束
成熟的开发团队通常会在静态代码分析中设定规则,禁止字段注入。IntelliJ IDEA 在检测到字段注入时会给出“Field injection is not recommended”的警告。团队可以进一步通过 SonarQube、Checkstyle 或 ArchUnit 等工具,将构造器注入列为强制性架构约束,从而持续保持代码质量。
22.1.5 实践小结
在实际项目中,遵循以下简单规则即可避免绝大多数依赖注入相关的坑:
- 生产级业务 Bean 一律使用构造器注入,且依赖声明为
final。 - 若构造器参数多于 3~5 个,应停下来审视是否该类承担了过多职责,考虑拆分为更小的单元。
- 单元测试完全不依赖 Spring 容器,直接 new 待测类并传入 Mock 依赖。
- 除非有明确的“可选依赖”需求,否则不使用 Setter 注入。
- 弃用字段注入,即便是
private字段加上@Autowired看起来很诱人。
构造器注入优先不仅是 Spring 生态的最佳实践,也是面向对象设计中“依赖倒置”和“显式依赖”原则的具体体现。它用极小的额外代码量,换回了编译安全、不可变性、清晰依赖关系和纯 POJO 测试能力,这些收益随着项目复杂度的增长会愈发明显。