Appearance
Spring IoC 容器
IoC 容器负责管理应用里的长期组件。它记录哪些类可以成为 Bean,决定这些 Bean 如何创建,负责把 Bean 之间的依赖连接起来,并在初始化和销毁阶段提供扩展点。
如果把业务应用看成一座工厂,IoC 容器不是生产线上的某个工人,而是设备台账和装配系统:哪些设备需要登记,设备之间怎么连接,启动前要做什么检查,关闭时要按什么顺序收尾,都由它统一管理。
详细位置
- 本页说明 IoC 的概念边界、Bean 来源、依赖注入、生命周期和排错方式。
- Spring 各模块关系见 Spring 总览。
- 事务和代理问题见 AOP。
- Web 请求中的 Controller、Service 装配见 Spring MVC。
概念边界
| 概念 | 准确含义 | 直观理解 |
|---|---|---|
| IoC | 对象创建和依赖组装的控制权从业务代码转移到容器 | 对象不再自己找零件,而是由装配系统配齐 |
| DI | 容器把一个对象需要的依赖传给它 | 组件声明插口,容器插上线 |
| Bean | 被 Spring 容器管理的对象 | 登记在容器台账里的组件 |
| ApplicationContext | 常用 Spring 容器 | 应用启动后的组件仓库和运行上下文 |
| BeanDefinition | Bean 的元信息 | 容器创建 Bean 前看的说明书 |
IoC 的重点不是让代码变短,而是让依赖关系显式、可替换、可检查。一个对象需要什么依赖,应能从构造器或配置中直接看出来,而不是藏在方法内部的 new 里。
哪些对象适合成为 Bean
适合注册为 Bean 的对象:
- 应用服务:例如订单服务、支付服务、导出服务。
- 数据访问对象:例如 Repository、Mapper、DAO。
- 外部系统客户端:例如支付网关客户端、短信客户端、对象存储客户端。
- 框架扩展点:例如拦截器、监听器、定时任务、消息消费者。
- 配置对象:例如线程池、序列化器、业务配置绑定对象。
不适合注册为 Bean 的对象:
- Entity、DTO、VO、Command、Request、Response。
- 一次循环、一次请求、一次计算中临时创建的数据对象。
- 持有用户临时状态或请求中间状态的对象。
判断标准是生命周期。Bean 通常像“设备”,在应用启动后长期存在;DTO 和 Entity 更像“单据”,在一次流程中流转,用完就结束。
Bean 的来源
Bean 主要来自两类入口。
组件扫描
应用内部自己写的组件,通常通过 @Service、@Repository、@Controller、@Component 注册。它表达的是“这个类由容器管理”。
使用时应让注解表达分层语义:
| 注解 | 更适合放在 | 说明 |
|---|---|---|
@Controller / @RestController | Web 入口 | 处理 HTTP 协议 |
@Service | 应用服务 | 编排业务流程、控制事务边界 |
@Repository | 数据访问 | 数据库、缓存、外部存储访问 |
@Component | 通用组件 | 没有明确分层语义的工具型组件 |
配置类
第三方对象、带构造参数的 SDK Client、线程池、连接池等,通常通过 @Configuration 和 @Bean 注册。它表达的是“这个对象怎样创建”。
组件扫描适合“类本身就是组件”;配置类适合“对象创建过程需要额外说明”。把复杂创建逻辑塞进业务类,会让业务代码同时承担装配职责。
依赖注入
依赖注入的目标是让对象只声明“我需要什么”,不关心“依赖从哪里来”。最推荐的是构造器注入,因为它有几个清晰特征:
- 对象创建完成时,必需依赖已经齐全。
- 依赖关系可以从构造器直接看见。
- 字段可以保持不可变。
- 单元测试可以直接传入替身对象。
- 循环依赖会更早暴露。
Setter 注入适合可选依赖或运行期可替换依赖。字段注入虽然短,但依赖不透明,测试不方便,业务代码中不建议作为默认选择。
多实现如何选择
一个接口有多个实现时,容器会遇到“同一个插口有多根线”的问题。常见处理方式:
| 场景 | 方式 | 含义 |
|---|---|---|
| 调用方明确需要某个实现 | @Qualifier | 指定 Bean 名称或限定符 |
| 某个实现是默认选择 | @Primary | 多实现中优先使用它 |
| 调用方需要所有实现 | 注入 List<T> 或 Map<String, T> | 适合插件、策略、责任链 |
不要靠类名巧合或加载顺序解决多实现问题。多实现本身是设计信息,应该用注解或集合注入表达清楚。
生命周期
单例 Bean 的生命周期可以按这条线理解:
- 容器读取 Bean 定义。
- 调用构造器创建对象。
- 注入依赖和配置属性。
- 执行初始化前扩展。
- 执行初始化回调。
- 执行初始化后扩展。
- Bean 进入可用状态。
- 容器关闭时执行销毁回调。
初始化回调适合做必须在启动前完成的校验,例如配置完整性检查、本地缓存结构初始化。不适合放置耗时且非关键的远程调用,否则应用启动会被外部系统拖慢。
作用域
多数业务 Bean 使用默认的 singleton。它表示容器中只有一个实例,并不表示它必须持有全局状态。
单例 Bean 应尽量无状态。用户 ID、请求参数、导入进度、临时计算结果等不应放进单例字段,否则并发请求之间可能互相污染。
常见作用域:
| 作用域 | 生命周期 | 适用性 |
|---|---|---|
singleton | 容器内单例 | Service、Repository、Client、配置类 |
prototype | 每次获取创建新实例 | 有状态任务对象,使用较少 |
request | 每个 HTTP 请求一个实例 | 请求上下文 |
session | 每个 HTTP Session 一个实例 | 会话状态,谨慎使用 |
配置绑定
零散的 @Value 适合少量简单值。只要配置形成一组,例如支付网关地址、Token、超时时间、重试次数,就更适合使用 @ConfigurationProperties。
结构化配置的好处:
- 配置项属于同一个对象,含义集中。
- 可以设置默认值和校验规则。
- 测试时可以直接构造配置对象。
- 排查时更容易确认前缀、字段和最终值。
配置对象像“设备参数表”,业务组件只读取参数表,不应该到处散落读取配置文件。
常见问题
找不到 Bean
优先检查:
- 类是否在启动类所在包或子包下。
- 类是否通过组件注解或配置类注册。
- 条件装配是否没有命中。
- 测试环境是否缺少配置类。
- 依赖是否进入运行时 classpath。
Bean 超过一个
说明容器找到了多个候选对象,但注入点没有说明要哪个。用 @Qualifier 指定、用 @Primary 设置默认,或者改成注入集合。
循环依赖
循环依赖通常不是容器问题,而是服务边界问题。A 调 B,B 又调 A,往往说明两个服务职责互相缠绕。
优先处理方式:
- 抽出共同依赖。
- 调整服务边界。
- 用事件解耦后续动作。
- 把事务入口放到更清晰的应用服务层。
配置没有注入
检查顺序:
- 配置文件是否进入 classpath。
- 当前 profile 是否正确。
- 环境变量或命令行参数是否覆盖文件配置。
- 配置前缀和字段名是否匹配。
- 配置属性类是否被启用或扫描。
测试判断
业务单元测试不一定需要启动 Spring。构造器注入设计良好的类,可以直接传入替身依赖测试业务规则。只有需要验证组件扫描、条件装配、配置绑定、事务代理等框架行为时,才需要启动 Spring 测试上下文。
总结
IoC 的核心是明确对象管理边界。长期组件交给容器,临时数据留在业务流程里;必需依赖用构造器表达,多实现用明确规则选择;单例 Bean 保持无状态;排错时按扫描范围、注册来源、条件装配、多实现冲突、配置绑定逐层检查。
