Spring IoC 控制反转研究:从对象创建到容器生命周期
Spring IoC 控制反转研究:从对象创建到容器生命周期
Spring 最重要的能力,不是帮我们少写几个
new,而是把对象创建、依赖组装、生命周期管理和扩展逻辑从业务代码中抽离出来,交给一个可扩展的容器统一协调。理解 IoC,应该从“控制权到底转移了什么”开始,而不是停留在“自动注入依赖”这句口号上。
一、为什么要研究 IoC
在一个小型程序里,直接创建对象并不难:
public class OrderService {
private final OrderRepository repository = new JdbcOrderRepository();
}
但随着系统规模变大,这段代码会逐渐暴露出几个问题:
OrderService依赖了具体实现JdbcOrderRepository,替换为远程实现、缓存实现或测试替身时需要修改业务代码。- 对象的创建时机、构造参数和生命周期散落在各个类中,无法统一管理。
- 依赖关系隐藏在实现内部,阅读类的构造函数时看不出它需要哪些协作者。
- 测试时很难替换基础设施,容易被迫启动数据库、消息队列等外部资源。
- 日志、事务、权限等横切逻辑如果直接写进业务类,会让核心代码越来越臃肿。
IoC 的研究价值,就在于解释 Spring 如何把这些控制权收回容器,并且让业务对象保持相对纯粹。
★ Insight ─────────────────────────────────────
- IoC 解决的不是“对象如何被注入”这么一个局部问题,而是“谁负责决定对象的创建、组装和生命周期”。
- DI 是 IoC 的主要实现方式,但 IoC 不等于注解注入;构造器、工厂方法、配置文件和手工注册都可以实现控制反转。
- Spring 的关键抽象不是
@Autowired,而是BeanDefinition。注解只是把元数据转成 Bean 定义,容器再根据定义完成后续工作。
─────────────────────────────────────────────────
本文以 Spring Framework 6 / Spring Boot 3 的常见用法为背景。源码中的具体方法名可能随版本演进,但整体职责划分相对稳定。
二、IoC、DI 与容器:三个概念不要混为一谈
2.1 IoC:控制权发生了转移
传统程序由业务对象主动控制依赖的创建:
public class OrderService {
private final OrderRepository repository;
public OrderService() {
this.repository = new JdbcOrderRepository();
}
}
这里的控制关系是:OrderService 决定使用哪个实现,也决定什么时候创建它。
使用 IoC 后,类只表达自己需要什么:
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
OrderService 不再负责创建 OrderRepository,创建和组装工作由外部容器完成。控制权从业务对象转移到容器,这就是 Inversion of Control。
2.2 DI:依赖被外部注入
Dependency Injection 是实现 IoC 的一种具体方式。依赖可以通过多种入口传入:
- 构造器注入:对象创建时传入必需依赖。
- Setter 注入:对象创建后通过方法设置可选依赖。
- 字段注入:容器通过反射直接设置字段。
- 方法注入:容器调用配置方法,把依赖作为参数传入。
现代 Spring 项目更推荐构造器注入:
@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 容器。单构造器场景下,Spring 通常不要求额外写 @Autowired。
2.3 容器:IoC 的运行时执行者
Spring 中常见的容器层次如下:
BeanFactory
└── ListableBeanFactory
└── ApplicationContext
└── AnnotationConfigApplicationContext
└── ClassPathXmlApplicationContext
└── Spring Boot 的应用上下文
BeanFactory 提供最基本的 Bean 创建和获取能力。ApplicationContext 在此基础上增加了事件发布、国际化、资源加载、环境配置以及自动发现后处理器等完整应用能力。
实际应用中,我们通常不直接调用 new 创建业务对象,而是让配置类、组件扫描和自动配置把组件注册到 ApplicationContext 中:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
三、BeanDefinition:容器手里的“对象蓝图”
Spring 不会在扫描到 @Service 后立刻把所有对象都随手创建出来。更准确的过程是:先把类上的信息解析成 BeanDefinition,再根据这些定义创建 Bean。
一个 BeanDefinition 可以理解为对象的生产说明书,通常包含:
- Bean 的类型或工厂方法信息;
- Bean 名称和别名;
- 作用域,例如
singleton、prototype或 Web 作用域; - 是否延迟初始化;
- 构造参数和属性依赖;
- 初始化方法和销毁方法;
- 是否参与自动代理和其他后处理流程。
例如下面的配置:
@Configuration
public class PaymentConfig {
@Bean
public PaymentGateway paymentGateway(PaymentProperties properties) {
return new HttpPaymentGateway(properties.endpoint());
}
}
Spring 需要记录的不只是 PaymentGateway 这个返回类型,还包括工厂方法是 paymentGateway、工厂对象是 PaymentConfig、方法参数需要解析 PaymentProperties,以及这个 Bean 的默认作用域等信息。
组件扫描的本质也类似:扫描器发现带有 @Component、@Service、@Repository 等注解的类,把它们转换为 BeanDefinition,注册到 BeanDefinitionRegistry。注册完成后,容器才有了批量创建对象的依据。
四、容器启动:refresh() 做了什么
Spring 容器的启动流程很长,但可以先抓住主线。以 AbstractApplicationContext#refresh() 为观察入口,大致可以抽象成下面的步骤:
准备并刷新应用上下文
↓
创建或准备 BeanFactory
↓
加载并注册 BeanDefinition
↓
注册 BeanFactoryPostProcessor
↓
执行 BeanFactoryPostProcessor,修改 BeanDefinition
↓
注册 BeanPostProcessor
↓
初始化消息源、事件广播器等上下文组件
↓
实例化非懒加载的单例 Bean
↓
发布 ContextRefreshedEvent
4.1 先注册定义,再创建实例
这两个阶段的分离非常关键:
- 扫描和配置解析阶段只负责收集元数据。
- BeanFactoryPostProcessor 可以在实例化前调整 BeanDefinition。
- 容器根据最终定义创建 Bean,并执行依赖解析和生命周期回调。
这也是为什么 Spring 可以实现配置占位符、自动配置、条件装配等能力。容器先拥有一张完整的“对象地图”,再决定哪些对象真正需要被创建。
4.2 BeanFactoryPostProcessor 与 BeanPostProcessor
两类后处理器名字很像,但处理时机不同:
| 类型 | 处理对象 | 典型职责 |
|---|---|---|
BeanFactoryPostProcessor |
BeanDefinition / BeanFactory 元数据 | 修改属性、解析占位符、注册额外定义 |
BeanPostProcessor |
已创建的 Bean 实例 | 属性注入、初始化回调、生成代理对象 |
可以用一句话记忆:BeanFactoryPostProcessor 改“生产说明书”,BeanPostProcessor 改“生产出来的对象”。
AutowiredAnnotationBeanPostProcessor 负责处理 @Autowired、@Value 等注入注解;AbstractAutoProxyCreator 则会根据切面和匹配规则决定是否为 Bean 创建 AOP 代理。
五、一个 Bean 是如何被创建出来的
从 AbstractAutowireCapableBeanFactory 相关逻辑看,一个普通 Bean 的创建过程可以概括为:
根据 BeanName 获取 Bean
↓
检查单例缓存
↓
确定 Bean 类型与构造器
↓
实例化原始对象
↓
提前暴露单例工厂(必要时)
↓
填充属性和解析依赖
↓
执行 Aware 回调
↓
执行初始化前的 BeanPostProcessor
↓
执行初始化方法
↓
执行初始化后的 BeanPostProcessor
↓
返回最终对象或代理对象
5.1 实例化:构造器不是容器魔法,而是依赖解析结果
对于构造器注入,Spring 会先找出候选构造器,再解析构造器参数。假设:
public OrderService(OrderRepository repository) {
this.repository = repository;
}
容器并不是简单地反射调用无参构造器,而是要回答几个问题:
OrderRepository有几个候选实现?- 是否有
@Primary指定默认实现? - 是否有
@Qualifier限定 Bean 名称? - 依赖本身是否已经创建?
- 是否形成了无法解析的循环依赖?
解析成功后,Spring 才会把依赖实例作为参数传给构造器。
5.2 属性填充:注解注入发生在 Bean 创建过程中
字段注入看起来很简单:
@Service
public class ReportService {
@Autowired
private ReportRepository repository;
}
但运行时需要通过 BeanPostProcessor 找到注入点、解析依赖、反射写入字段,并处理必要的限定条件。字段注入把依赖隐藏在类内部,也让对象脱离容器后处于不完整状态,因此通常不如构造器注入清晰。
5.3 初始化:对象可用之前还有一串回调
Bean 初始化阶段可能涉及:
BeanNameAware、BeanFactoryAware、ApplicationContextAware等 Aware 接口;@PostConstruct标记的方法;InitializingBean#afterPropertiesSet();- 自定义
init-method; BeanPostProcessor#postProcessBeforeInitialization();BeanPostProcessor#postProcessAfterInitialization()。
这些回调不是越多越好。业务初始化逻辑应该尽量集中,避免一个 Bean 同时实现多个生命周期接口,再叠加注解和 XML 配置,最后连创建顺序都没人敢动。
六、三级缓存与循环依赖:最容易被讲错的部分
Spring 常见的单例循环依赖场景是属性注入:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
Spring 为单例 Bean 维护了几层缓存,常见概念包括:
- 一级缓存:完整创建并初始化后的单例对象。
- 二级缓存:提前暴露的早期单例引用。
- 三级缓存:用于生成早期引用的
ObjectFactory,可在需要时创建代理引用。
简化后的流程是:创建 A 时发现需要 B;创建 B 时又发现需要 A。此时 A 还没有完全初始化,但容器可以通过三级缓存获取 A 的早期引用,把它注入 B,然后完成 B,再回到 A 完成属性填充。
这里有三个边界必须记住:
- 这种机制主要针对单例和属性 / Setter 注入。
- 构造器循环依赖无法靠“提前暴露半成品对象”解决,因为对象连构造都没有完成。
prototypeBean 不享受同样的单例缓存机制,循环依赖通常会直接失败。
因此,循环依赖能被容器“处理”不代表设计合理。它往往说明两个类承担的职责耦合过深,拆分领域服务、引入更稳定的抽象通常比依赖容器兜底更好。
★ Insight ─────────────────────────────────────
- 三级缓存的重点不只是“缓存了三个 Map”,而是为了在 Bean 尚未完成初始化时,仍然能为 AOP 代理等扩展点提供早期引用。
- 循环依赖之所以表现得时好时坏,通常取决于注入方式、作用域和是否需要代理;不能只记“Spring 能解决循环依赖”这句半真半假的结论。
- 设计上真正应该追问的是“为什么两个对象必须互相持有”,而不是“怎样把容器调到能启动”。能启动不等于依赖关系健康。
─────────────────────────────────────────────────
七、IoC 与 AOP 为什么能自然结合
IoC 容器掌握了 Bean 的创建过程,因此可以在对象返回给业务代码之前插入代理:
@Service
public class PaymentService {
@Transactional
public void pay(Long orderId) {
// payment logic
}
}
业务代码注入的对象,可能并不是 PaymentService 原始实例,而是 JDK Dynamic Proxy 或 CGLIB 代理。代理在调用目标方法前后加入事务开启、提交和回滚逻辑,真正的目标对象则由容器继续管理。
这解释了几个常见现象:
@Transactional通常只对经过代理的外部调用生效。- 同一个类中通过
this.method()进行内部调用,可能绕过代理。 - 使用 JDK 动态代理时,注入点通常应面向接口;CGLIB 则基于子类生成代理。
- 从容器里拿到的 Bean 类型可能是代理类型,直接比较实现类或强制转换时要留意。
IoC 是 AOP 的基础设施。没有统一的 Bean 创建和返回过程,事务、缓存、权限、异步等横切能力就很难以一致方式织入。
八、Bean 的作用域与生命周期
Spring 默认作用域是 singleton,但它不是 JVM 级别的全局单例,而是“在一个 ApplicationContext 内共享一个实例”:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ExportTask {
}
常见作用域包括:
| 作用域 | 含义 | 适用场景 |
|---|---|---|
singleton |
每个容器一个实例 | 无状态服务、Repository、配置组件 |
prototype |
每次获取时创建新实例 | 有短生命周期状态的对象 |
request |
每个 HTTP 请求一个实例 | Web 请求上下文数据 |
session |
每个 HTTP Session 一个实例 | 会话级状态 |
application |
每个 Web 应用上下文一个实例 | 应用级 Web 组件 |
需要特别注意:把 prototype Bean 注入 singleton Bean 时,注入动作通常只发生一次,后续并不会因为每次调用方法就自动生成新对象。若确实需要按使用时机创建,可以考虑 ObjectProvider<T>、Provider<T> 或显式工厂。
销毁阶段也有对应机制:@PreDestroy、DisposableBean#destroy() 和自定义 destroy-method 都可以参与容器关闭流程。非容器创建的对象不会自动享受这些生命周期管理能力,这也是“把对象交给 Spring 管理”这句话的实际含义。
九、三种注册方式与适用边界
9.1 组件扫描
适合应用内部由自己控制的业务组件:
@Component
public class TaxCalculator {
}
@Service
public class InvoiceService {
private final TaxCalculator taxCalculator;
public InvoiceService(TaxCalculator taxCalculator) {
this.taxCalculator = taxCalculator;
}
}
优点是简洁,缺点是类和 Spring 注解产生了直接联系,扫描范围、命名规则和条件装配也需要清楚管理。
9.2 Java 配置与 @Bean
适合第三方类、复杂构造过程和需要明确装配关系的对象:
@Configuration
public class ClientConfiguration {
@Bean
public HttpClient httpClient(ClientProperties properties) {
return HttpClient.newBuilder()
.connectTimeout(properties.connectTimeout())
.build();
}
}
这种方式让创建逻辑显式可见,也适合把外部库对象纳入容器。
9.3 编程式注册
在框架扩展、插件系统或动态模块中,可以直接操作注册表:
registry.registerBeanDefinition("dynamicHandler", definition);
它的灵活性最高,但也最容易把容器变成隐藏的全局状态。业务代码一般不应到处直接操作 BeanDefinitionRegistry。
十、常见误区与工程判断
10.1 把 IoC 理解成“少写 new”
少写 new 只是表象。真正的收益是依赖关系外置、生命周期统一、扩展点集中。若只是把 new 换成 applicationContext.getBean(),控制权仍然在业务代码手里,只是从显式依赖变成了字符串查找,反而更难维护。
10.2 到处注入 ApplicationContext
下面这种写法会把 Service 变成容器脚本:
public class OrderService {
private final ApplicationContext context;
public void submit() {
var repository = context.getBean(OrderRepository.class);
repository.save();
}
}
更好的方式是直接声明依赖:
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
只有在插件发现、动态类型解析、基础设施桥接等确实需要容器能力的边界位置,才考虑注入 ApplicationContext 或 ObjectProvider。
10.3 为了“方便”使用字段注入
字段注入的代码短,但代价是依赖隐藏、不可变性丢失、纯单元测试变麻烦。项目中保留字段注入往往是历史兼容或框架限制,新增代码优先使用构造器注入更稳妥。
10.4 一个接口只有一个实现,却滥用 @Qualifier
@Qualifier 是解决多个候选实现的工具,不是装饰品。只有一个实现时,构造器按类型注入已经足够;当实现确实存在多个时,优先通过有意义的 Bean 名称、@Primary 或配置条件表达选择规则。
10.5 把容器当成万能服务定位器
Service Locator 会隐藏依赖,使类的行为依赖运行时字符串和全局状态。IoC 的目标是让依赖关系更透明,而不是把所有对象塞进一个“能拿到任何东西”的入口。
十一、如何调试 Bean 装配问题
遇到 NoSuchBeanDefinitionException、NoUniqueBeanDefinitionException 或循环依赖异常时,可以按下面顺序排查:
- 确认目标类型是否被注册为 Bean。
- 确认组件扫描路径是否覆盖了实现类所在包。
- 确认实现类是否是具体类,或者是否有对应的
@Bean方法。 - 如果候选对象有多个,检查
@Primary、@Qualifier和 Bean 名称。 - 检查构造器依赖图,确认是否形成构造器循环依赖。
- 检查是否因为
@Profile、@Conditional或配置属性导致 Bean 没有生效。 - 检查是否注入了代理对象,尤其是接口和实现类的类型边界。
- 打开 Spring 的相关 debug 日志,观察条件评估报告和 Bean 创建日志。
一个很实用的思路是:把异常还原成一张依赖图。节点是 Bean,边是依赖关系;缺 Bean 是节点不存在,多 Bean 是入边没有唯一目标,循环依赖则是图中出现了环。这样看,比盯着一大段异常堆栈有效得多。
十二、研究结论:IoC 的边界在哪里
IoC 不是把程序交给 Spring 后就万事大吉。它更像一种组织复杂度的方法:
- 业务对象声明职责和依赖,不负责基础设施的创建细节。
- 容器负责根据元数据组装对象,并统一协调生命周期。
- 后处理器为事务、缓存、校验和代理等扩展能力提供入口。
- 作用域和生命周期决定对象状态是否安全,不能只看注入语法。
- 容器能力应该集中在组合根和基础设施边界,业务代码不应主动到处索取容器服务。
从源码角度看,Spring IoC 的核心链路可以压缩成一句话:
组件扫描和配置解析生成
BeanDefinition,BeanFactory根据定义完成依赖解析与对象创建,BeanPostProcessor在生命周期关键节点扩展行为,ApplicationContext再把这些能力组织成完整的应用运行时。
理解这条链路之后,@Component、@Autowired、@Bean、@Transactional 就不再是互相孤立的注解,而是同一套容器机制在不同阶段的表现。Spring IoC 真正反转的,是对象创建与协作关系的控制权;真正值得掌握的,也正是这份控制权如何被容器接管、细化并在边界处交还给业务代码。
参考方向
- Spring Framework Reference Documentation:Core Technologies / The IoC Container
org.springframework.beans.factory.BeanFactoryorg.springframework.context.ApplicationContextorg.springframework.beans.factory.support.DefaultListableBeanFactoryorg.springframework.context.support.AbstractApplicationContext#refreshorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
- 点赞
- 收藏
- 关注作者
评论(0)