前面章节已多次提及“控制反转”和“依赖注入”这两个概念,并用简单示例说明了它们的基本形态。但在真正吃透 Spring 框架之前,有必要专门从设计思想的高度,捋清这两个术语之间的关系,理解它们究竟解决了什么本质问题,以及在日常开发中应该如何运用才最得体。
3.1.1 谁控制谁?反转了什么?
控制反转(Inversion of Control, IoC)一词最早出现在软件架构的讨论中,并非 Spring 独创。它的核心指向一个原则:不要在自己负责的业务代码中主动创建所依赖的组件,而是把这一控制权交给一个外部的容器或框架。
在传统的“正向控制”中,一个对象对自己需要什么非常清楚,并且亲自负责获取:
public class ReportService {
// 自己创建依赖
private ReportRepository repository = new MySQLReportRepository();
public Report generate(Long id) {
return repository.findById(id);
}
}
这里,ReportService 不仅决定了自己需要一个 ReportRepository,还决定了具体使用哪一种实现(MySQLReportRepository),以及如何创建它(直接 new)。控制权握在 ReportService 自己手中。
而在“控制反转”的模式下,ReportService 只声明“我需要一个 ReportRepository”,但不再关心它是谁创建的、怎么创建的、具体是什么实现:
public class ReportService {
private final ReportRepository repository;
// 依赖从外部传入
public ReportService(ReportRepository repository) {
this.repository = repository;
}
}
反转的正是“获取依赖的控制权”。控制权从业务类内部,反转给了外部的容器。这一反转看似平淡,却从根本上改变了对象的组装方式:类与类之间不再通过硬编码的“新创实例”来耦合,而是通过与抽象接口的契约来关联。
3.1.2 依赖注入:控制反转的主流实现方式
控制反转是一种原则,它并不限定技术手段。实现 IoC 的方式有多种,比如:
- 工厂模式:用一个工厂类统一创建对象,业务类调用工厂获取依赖。
- 服务定位器(Service Locator):通过一个全局注册表来查找依赖,例如 JNDI。
- 依赖注入(Dependency Injection, DI):由容器主动将依赖“注入”到对象中,对象被动接收。
Spring 选择且深度实践的,正是依赖注入。在 Spring 的理念中,DI 相比于服务定位器有几个显著优势:
- 代码无框架侵入:业务类不需要主动查阅任何容器 API(比如
applicationContext.getBean()),只需通过构造器或 setter 接收依赖即可。 - 依赖关系显性化:一看构造器签名,就能知道这个类依赖哪些组件,便于阅读和测试。
- 管理集中化:所有 Bean 的依赖关系在容器配置中一目了然,易于统一调整。
所以,当我们谈论 Spring 的 IoC 时,绝大部分时候指的就是它的 DI 容器。
3.1.3 容器如何工作:一个简化的内部视角
使用者无需了解容器的每一个内部细节,但理解一个简化的流程,有助于正确使用和排查问题:
- 配置元数据读取:容器通过 XML、注解或 Java Config 读取 Bean 的定义,包括类名、作用域、依赖关系、初始化方法等。
- BeanDefinition 注册:将解析到的定义封装成
BeanDefinition对象,存入注册中心。 - 依赖解析与注入:当应用通过容器获取某个 Bean 时,容器检查它的依赖,递归创建依赖的 Bean,最终将完整的对象图装配完成并返回。
- 生命周期回调:在初始化前后执行自定义逻辑(如
@PostConstruct),在容器关闭时执行销毁逻辑(@PreDestroy)。
在整个过程中,开发者只负责“定义”和“使用”,容器负责“创建”和“装配”。这就自然地实现了 创建与使用分离 的原则。
3.1.4 注入方式的选择:构造器 vs Setter
Spring 支持三种依赖注入方式,但在真实的团队协作中,选择并非随意。
构造器注入(推荐)
@RequiredArgsConstructor // Lombok 可简化代码
public class OrderService {
private final OrderRepository repository;
private final PaymentService paymentService;
}
所有依赖在对象创建时就确定,字段可以声明为 final,保证不可变。任何必需的依赖缺失都会在编译或启动时暴露,而不是在运行时出现空指针。这是目前社区公认的最佳实践。
Setter 注入(谨慎使用)
@Autowired
public void setOrderRepository(OrderRepository repository) {
this.repository = repository;
}
适合那些可选的依赖,或者需要在运行时动态更换实现的场景。但过多使用 setter 会让对象状态不确定,难以推理。
字段注入(不推荐)
@Autowired
private OrderRepository repository;
看似最简洁,但它隐藏了类的依赖,不利于测试(必须依靠反射或启动容器),也让字段无法成为 final。尽管在旧项目或快速原型中常见,但在高质量的工程实践中应当避免。
3.1.5 实际收益:不只是“解耦”而已
当我们把控制反转和依赖注入的思想真正落实到项目中,得到的远不止是“类之间不耦合”这么简单:
- 可测试性飞跃:单元测试中可以随手 new 一个业务对象,向构造器传入 Mock 依赖,无需启动任何容器。测试速度从秒级降到毫秒级。
- 组件可替换性:例如要切换缓存方案,只需新建一个实现
CacheService接口的 Bean,并在配置中替换原有实现,所有使用该接口的类零更改。 - 职责清晰化:业务类只写业务,创建和配置的逻辑归属到
@Configuration类中,团队分工更清晰。 - 统一生命周期管理:容器管理单例、原型等作用域,开发者无需手写资源池和销毁逻辑,避免忘记释放数据库连接这类低级错误。
3.1.6 避免将 DI 用成“全局查找”的反模式
尽管依赖注入已经深入人心,但实践中仍常见一种退化用法:在业务代码中通过 ApplicationContext.getBean() 动态获取 Bean。这种做法本质上是把容器当成了服务定位器,重新引入了对框架 API 的依赖,丧失 DI 的核心优势。
除非是在某些特殊场景(如需要动态获取原型 Bean、与遗留代码集成),否则应当让依赖始终通过构造器或 setter 显式注入,保持业务代码的纯净。
结合本书前面章节对 IoC 容器的介绍,我们已经看到了 Spring 是如何将控制反转从理论变为基础设施的。接下来的内容将进入容器的具体配置和使用,你将手把手实践如何定义 Bean、选择作用域、管理生命周期,真正驾驭这套装配艺术。