做后端这些年,Spring 大概是所有人最早接触也最自信的框架。但你敢说自己完全清楚一个 Bean 是怎么从@Component走到容器销毁的吗?我之前也是似懂非懂,直到某次排查项目启动时某个组件被重复创建,被逼着把AbstractBeanFactory#doCreateBean这条源码路径从头到尾过了一遍,才把 Bean 的生命周期真正串起来。标题里说的“15 件事”,其实就是这段路径上 Spring 依次安排的 15 个动作,覆盖从 BeanDefinition 注册、实例化、属性填充、初始化、代理增强,到单例池注册、最终销毁的完整闭环。
这篇文章适合三类人:一直在用 Spring 但没系统看过生命周期的人;准备面试想把这套流程讲清楚的人;想在团队内部做框架封装、需要从生命周期扩展点入手的人。我会先把 15 件事的总清单列出来,再按四个阶段展开讲每个动作背后的设计理由,最后给一张可直接“抄作业”的扩展点速查表和几个真实的踩坑记录。
1. 15 件事全览:Bean 生命周期的四个阶段框架
1.1 拆解清单:从 BeanDefinition 到单例池的时间线
先把结论放在这里。下面这张表就是我根据源码路径整理的 15 个事件,顺序完全对应一个普通单例 Bean 从注册到销毁的过程。建议先花两分钟把这张表过一遍,后面每一节都能对上号。
| 事件 | 所属阶段 | 触发点 | 这一动作在干什么 |
|---|---|---|---|
| 1 | BeanDefinition 阶段 | 读取配置/扫描包 | 把@Component、@Bean、XML 配置解析成 BeanDefinition 并注册 |
| 2 | BeanDefinition 阶段 | 容器启动早期 | 执行 BeanFactoryPostProcessor,允许修改 BeanDefinition |
| 3 | BeanDefinition 阶段 | 真正创建前 | 合并父子 BeanDefinition,准备完整属性列表 |
| 4 | 实例化阶段 | createBean 入口 | InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation尝试短路创建 |
| 5 | 实例化阶段 | createBeanInstance | 推断构造器或工厂方法,反射创建实例 |
| 6 | 实例化阶段 | 实例化之后 | postProcessAfterInstantiation决定是否继续属性填充 |
| 7 | 实例化阶段 | populateBean | 执行@Autowired、@Resource、XML 属性等依赖注入 |
| 8 | 实例化阶段 | 属性填充过程中 | 三级缓存提前暴露早期引用,解决单例循环依赖 |
| 9 | 初始化阶段 | 属性填充完成后 | 回调 Aware 接口(BeanName、BeanClassLoader、BeanFactory) |
| 10 | 初始化阶段 | 初始化前 | BeanPostProcessor.postProcessBeforeInitialization,@PostConstruct在这里执行 |
| 11 | 初始化阶段 | 初始化中 | InitializingBean.afterPropertiesSet() |
| 12 | 初始化阶段 | 初始化中 | 执行自定义 init-method |
| 13 | 初始化阶段 | 初始化后 | BeanPostProcessor.postProcessAfterInitialization,AOP 代理通常在这里生成 |
| 14 | 就绪阶段 | 初始化完成后 | 放入单例池 singletonObjects,Bean 正式可用 |
| 15 | 销毁阶段 | 容器关闭时 | 执行@PreDestroy、DisposableBean、destroy-method,移出单例池 |
这 15 个事件不是均匀分布在生命周期里的,它明显分成四段:第 1-3 件事发生在容器启动、还没真正创建 Bean 之前;第 4-8 件事聚焦“实例化”和“依赖注入”;第 9-13 件事聚焦“初始化”和“代理增强”;第 14-15 件事管“就绪”和“销毁”。
1.2 贯穿全程的钩子机制:为什么 Spring 需要后处理器
在往下拆之前,必须先建立一个认知:上面 15 件事里有好几件不是 Spring“自己干完就完了”,它故意留出了钩子,让框架和你自己的代码能插进来。最典型的就是BeanPostProcessor和InstantiationAwareBeanPostProcessor,它们是整个生命周期里最核心的扩展机制。
为什么 Spring 要这么设计?你可以把 Bean 的创建过程想象成饭店出菜:从采购食材(扫描 BeanDefinition)到洗切配菜(实例化)、调味摆盘(初始化)、上桌(放入单例池)——每个环节都安排了一位“质检员”。质检员可以在某一环节强行替换掉菜品,也可以在摆盘前加一道工序,这就是后处理器存在的意义。
Spring 里几乎所有高级功能都是通过这种钩子实现的:AOP 代理靠postProcessAfterInitialization生成,@PostConstruct靠CommonAnnotationBeanPostProcessor触发,@Autowired靠AutowiredAnnotationBeanPostProcessor执行。所以理解生命周期,本质上就是理解这些钩子分别在哪个事件节点上被调用。
2. 阶段一:BeanDefinition 时期的静默编排
2.1 事件 1-3:从扫描注册到合并定义,修改配方的最后机会
事件 1 是注册 BeanDefinition。你写的@Component、@Service,或者@Configuration类里的@Bean方法,最终都会被某个BeanDefinitionReader或ConfigurationClassPostProcessor解析成一个个BeanDefinition对象,注册进BeanDefinitionRegistry。这里有一个关键认知:此时 Spring 只是记录了“菜名”和“配方”,并没有真正去创建对象。所以从外部看,@PostConstruct和构造器都还没执行,一切都很安静。
事件 2 是执行BeanFactoryPostProcessor。注意它和BeanPostProcessor一字之差,作用却完全不同:BeanFactoryPostProcessor修改的是“配方”(BeanDefinition),BeanPostProcessor修改的是“成品”(Bean 实例)。在事件 2 这个时间点,所有 Bean 都还没有实例化,因此修改 BeanDefinition 的成本最低、影响最彻底。经典场景是PropertySourcesPlaceholderConfigurer,它把${jdbc.url}这种占位符在真正创建 Bean 之前替换成配置值。还有一个细节:BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor的子接口,它比普通后处理器更早执行,可以在这个时机动态注册新的 BeanDefinition,很多框架的“自动注册组件”就是这么实现的。
事件 3 是合并 BeanDefinition。初学时常忽略 Spring 支持<bean parent="...">这种父定义继承,子定义可以覆盖父定义的属性。如果父子定义都存在,Spring 在真正创建 Bean 之前会把两者合成一份完整的RootBeanDefinition,子类属性覆盖父类属性,这就是“合并”的含义。这一步极其重要,因为后续所有操作依赖的都是这份合并后的完整定义,而不是原始的碎片化定义。
2.2 事件 4:实例化前的“短路”机会,AOP 的入口
事件 4 是很多人忽略、但理解后会对生命周期豁然开朗的一个点:InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation。
这个方法在 Spring 准备创建 Bean 实例之前被调用。如果它返回一个非空对象,Spring 会直接把这个对象当作 Bean 的最终结果,跳过后续的实例化、属性填充、初始化回调,直接进入事件 13 之后的登记流程。换句话说,你在这一步返回了什么,容器最终就拿到什么。
@Component public class CustomInstantiationProcessor implements InstantiationAwareBeanPostProcessor { @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { if ("someService".equals(beanName)) { // 返回自定义代理对象,后续 populateBean / initializeBean 全部跳过 return new SomeServiceProxy(); } return null; } }这里最容易踩的坑:因为后续生命周期被跳过,所以你在@PostConstruct、afterPropertiesSet、init-method里写的逻辑全都不会执行,依赖注入也不会发生。我见过有人为了做一个“快速初始化代理”,在这里返回了一个对象,结果代理内部依赖的其他 Bean 全是 null,排查了半天才发现是这个短路机制在作怪。AOP 的早期入口AbstractAutoProxyCreator也继承了这个能力,但它通常不随便短路,而是判断是否需要提前代理。
3. 阶段二:实例化与属性填充,Spring 最精密的编排
3.1 事件 5-7:构造器推断、依赖注入与实例化后回调
事件 5 是实例化。这里最核心的是构造器推断机制。如果 Bean 只有一个默认构造器,Spring 直接反射创建;如果存在多个构造器,Spring 会根据你声明的@Autowired注解、参数数量和匹配度来“猜”该用哪个。整体规则复杂,很多文章会专门展开讲,这里你只需要记住:构造器选择会触发依赖解析,因此构造器注入的 Bean 会先于属性注入被创建。
事件 6 是postProcessAfterInstantiation。它有一个很少人用但很实用的返回值:如果返回 false,Spring 会直接跳过整个属性填充阶段。这意味着依赖注入、@Autowired全部不会执行。这个扩展点适合那些“实例化后想完全自己控制字段赋值,不想让 Spring 再动依赖”的场景,但日常业务代码几乎用不到,知道有它存在即可。
事件 7 才是真正意义上的依赖注入阶段,对应源码里的populateBean。这里会依次处理:@Autowired/@Value(由AutowiredAnnotationBeanPostProcessor处理)、@Resource(由CommonAnnotationBeanPostProcessor处理)、XML 中配置的 property 值。注意一个隐藏细节:populateBean在执行过程中如果需要注入其他 Bean,会递归触发那个 Bean 的完整生命周期。也就是说,生命周期不是“所有 Bean 都完成第 3 步再统一进入第 4 步”,而是“谁用到谁就先被创建”。
3.2 事件 8:循环依赖的三级缓存,到底发生在哪个时间点
循环依赖是生命周期里绕不开的话题,但很多人把“三级缓存”和生命周期时间点搞混了。这里我把它放进事件 8,因为它是发生在属性填充过程中、而不是独立于生命周期之外的一步。
假设 A 依赖 B,B 又依赖 A。Spring 解决这个问题的过程是:
- A 开始创建,实例化完成后,Spring 在
doCreateBean里调用addSingletonFactory,把 A 的ObjectFactory放进第三级缓存singletonFactories; - A 进入
populateBean,发现需要 B,开始创建 B; - B 实例化完成后,同样把
ObjectFactory放进三级缓存,然后进入属性填充,发现需要 A; - B 从三级缓存中拿到 A 的
ObjectFactory,执行getEarlyBeanReference得到 A 的早期引用,放入二级缓存earlySingletonObjects; - B 完成创建并进入单例池,A 拿到 B 完成注入,随后 A 进入初始化阶段,完成后也放入单例池。
关键问题来了:为什么 Spring 不直接把刚实例化完的对象放到二级缓存?因为如果 A 需要被 AOP 代理,最终暴露给外部的是代理对象,而不是原始对象。三级缓存里存的是ObjectFactory,只有真正发生循环依赖时才调用它生成早期引用;如果没有循环依赖,Spring 就不会调用getEarlyBeanReference,也就不会提前创建代理,A 会在事件 13 才统一生成代理。这样做的好处是:绝大多数 Bean 都走正常路径,不会因为循环依赖机制被迫提前代理,避免产生多个不一致的代理实例。
所以,循环依赖和三级缓存的联系不是“为了循环依赖而设计三级缓存”,而是“Spring 把实例化和初始化拆开之后,需要在中间留一个可延后处理的早期引用通道”。构造器循环依赖解决不了也是这个原因:构造器里就要拿到对方实例,可此时连三级缓存都没来得及放,自然拿不到。
4. 阶段三:Aware 回调与初始化,Bean 正式“活过来”
4.1 事件 9-12:Aware、@PostConstruct、InitializingBean 与 init-method 的先后
属性填充完成后,Bean 已经拥有了完整的依赖,接下来进入初始化阶段。事件 9 是 Aware 接口回调,在invokeAwareMethods里,Spring 会依次检查:
BeanNameAware:把 beanName 告诉对象;BeanClassLoaderAware:把类加载器传给对象;BeanFactoryAware:把当前 BeanFactory 传给对象。
还有一个容易误解的点:ApplicationContextAware不在这个invokeAwareMethods里处理,它是通过ApplicationContextAwareProcessor这个BeanPostProcessor在事件 10 的postProcessBeforeInitialization中被回调的。由于ApplicationContextAwareProcessor的优先级排在CommonAnnotationBeanPostProcessor之前,所以实际顺序是:ApplicationContextAware先于@PostConstruct。
事件 10 到事件 12 是初始化阶段最容易被面试官连环追问的部分,也是实际验证优先级的最佳位置。真实的调用顺序是:
@PostConstruct:由事件 10 的CommonAnnotationBeanPostProcessor触发;InitializingBean.afterPropertiesSet():事件 11;- 自定义
init-method:事件 12,对应@Bean(initMethod = "init")或 XML 里的init-method配置。
很多人以为这三个是互斥的三选一,其实是三连发,顺序固定。验证方法很简单:写一个 Bean,同时定义三种初始化方式,各打一行日志,启动后看到日志顺序就是上面这个顺序。我还见过有人把init-method配置成和afterPropertiesSet同名,导致同一个方法被调用两次,这是最典型的误配置。
4.2 事件 13:初始化后的偷梁换柱,AOP 代理在这里生成
如果前面的初始化方法都是让 Bean 准备好自己的状态,事件 13 则是“别人替它脱胎换骨”的时刻。postProcessAfterInitialization是所有BeanPostProcessor中最后一个关键回调,Spring 的 AOP 主要在这里通过AbstractAutoProxyCreator.wrapIfNecessary判断当前 Bean 是否需要代理。
如果判定需要代理,Spring 会基于当前 Bean 生成一个代理对象,并把原始 Bean 作为 target 塞进代理里。这个代理对象最终被放入单例池,外部通过getBean拿到的、通过@Autowired注入的,都是这个代理对象,而不是事件 5 实例化出来的原始 Bean。这种“偷梁换柱”的行为对调用方完全透明,但如果你写了自定义BeanPostProcessor,就要特别注意:在事件 13 阶段对象可能已经被替换成代理,做类型判断时别对原始类型做硬编码。
回看事件 8 的循环依赖,这里还有一个值得玩味的细节:如果 A 发生了循环依赖,早期引用已经在二级缓存中生成了,那么事件 13 生成的代理必须和早期引用是同一个代理,否则注入给 B 的 A 和最终放入单例池的 A 就不是同一个对象,会导致整个容器内出现两个不一致的 A。Spring 通过earlyProxyReferences缓存来解决这一点:如果发现当前 Bean 已经有早期引用,事件 13 就不再重复代理,直接复用。
5. 阶段四:单例池就绪与销毁回调,容器关闭的收尾逻辑
5.1 事件 14:放入单例池,Bean 从这一刻可被使用
初始化完成后,Spring 调用addSingleton,把最终的 Bean(注意可能是代理对象)放入一级缓存singletonObjects,也就是我们常说的“单例池”。到这里,一个单例 Bean 的生命周期才算正式完成,外部通过getBean或依赖注入拿到的就是它。
这里有个隐蔽的顺序问题:addSingleton发生在初始化完成之后。如果把一个单例 Bean 直接通过构造器注入另一个正在创建的单例 Bean,会触发循环依赖;但如果只是在初始化方法里通过ApplicationContext.getBean()拿自己,由于此时它还没进单例池,拿到的可能是三级缓存中的早期引用。理解了这个时间点,很多莫名其妙的“拿不到同一个 Bean”问题就能解释通了。
事件 14 之后,Bean 进入“使用中”状态,一直到容器关闭。需要注意的是,懒加载单例 Bean(@Lazy)不会在容器启动时创建,而是第一次getBean时才从事件 1 或 4 开始走完整套生命周期。
5.2 事件 15:逆序销毁与 DisposableBean/@PreDestroy/destroy-method
容器关闭时,Spring 并不会简单粗暴地清空缓存,而是会挨个执行销毁回调。事件 15 对应的销毁方式有三种:
@PreDestroy:由CommonAnnotationBeanPostProcessor在处理销毁时触发;DisposableBean.destroy():实现接口的方式;- 自定义
destroy-method:对应@Bean(destroyMethod = "close")或 XML 配置。
@Configuration public class ShutdownConfig { @Bean(destroyMethod = "close") public DataSource dataSource() { return new HikariDataSource(); } }上面这种配置很典型:数据源需要在容器关闭时释放连接池。如果类中没有close方法,Spring 会尝试从DisposableBeanAdapter里推断;如果推断失败,会抛异常。实际使用中如果某个第三方类的关闭方法名不叫close,需要显式在destroyMethod里指定名称。
销毁顺序也有讲究:Spring 会按照 Bean 注册和依赖关系的逆序来销毁,即先销毁依赖别人的 Bean,再销毁被依赖的 Bean。这一点很符合直觉——A 依赖 B,A 要先用完 B,所以 B 不能先销毁,否则 A 在销毁回调里访问 B 就会拿到已关闭的实例。另外,单例池中的 Bean 被移出缓存后,才会真正被 GC 回收。
5.3 别踩的原型 Bean 误区:Spring 不帮你销毁
关于销毁,最常被误解的是原型 Bean。原型作用域(prototype)在每次getBean时都会创建新实例,Spring 在对象创建和初始化阶段会照常走完事件 4 到事件 13,但事件 15 基本和它无关:Spring 不会为原型 Bean 注册销毁回调,容器关闭时也不会管这些散落在外的实例。这一点在 Spring 官方文档里其实写得明明白白:原型 Bean 的销毁责任在调用方。
实际项目里容易踩的坑是:一个原型 Bean 内部持有数据库连接、文件句柄或线程池,容器关闭时不释放,造成资源泄漏。解决办法并不是指望 Spring 帮你销毁,而是自己通过@PreDestroy或让调用方在业务收尾时主动清理。如果确实需要 Spring 感知,可以换一种思路:把资源释放逻辑放到一个单例 Bean 中统一管理,或者使用自定义BeanPostProcessor在销毁阶段手动跟踪原型实例列表。
6. 实战扩展点速查:把 15 件事变成自己的代码
6.1 一张表理清所有可注入时机
前面按时间线讲了原理,实战时最需要的是“我要干预生命周期,该用哪个接口”的速查表。下面这张表是我在实际做框架封装时最常用到的。
| 扩展点 | 回调时机 | 典型用途 |
|---|---|---|
BeanFactoryPostProcessor | 事件 2 | 修改 BeanDefinition,替换占位符,注册额外定义 |
InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation | 事件 4 | 创建代理、短路标准生命周期 |
InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation | 事件 6 | 控制是否执行属性填充 |
BeanNameAware/BeanFactoryAware | 事件 9 | 获取容器或 Bean 名字 |
BeanPostProcessor.postProcessBeforeInitialization | 事件 10 | 执行自定义前置逻辑,@PostConstruct的宿主 |
InitializingBean.afterPropertiesSet | 事件 11 | 依赖注入完成后做校验和初始化 |
init-method | 事件 12 | XML 配置或@Bean(initMethod)指定初始化方法 |
BeanPostProcessor.postProcessAfterInitialization | 事件 13 | AOP 代理、全局包装、只读增强 |
DisposableBean/@PreDestroy/destroy-method | 事件 15 | 释放连接、取消任务、清理临时资源 |
如果你想调整多个BeanPostProcessor之间的执行顺序,可以实现Ordered接口或者加@Order注解,数值越小越靠前。我通常在写全局后处理器时都会带上一个日志开关,因为这种处理器会作用于容器内所有 Bean,一旦效率低,启动时间会肉眼可见地变慢,而你又很难第一时间定位到具体是哪个后处理器造成的。
6.2 三个踩坑记录:初始化三连发、后处理器顺序、短路实例化的副作用
最后分享三个我在实际项目里踩过、也帮别人排查过的典型问题。这些都不是理论推演,而是真实场景里调了一个下午才定位出来的问题。
第一个坑:初始化逻辑被重复执行。某个同事在@PostConstruct里打印了初始化日志,又在 XML 里配置了init-method,某次改造后初始化逻辑被执行了两次。本质上就是前面说的事件 10、11、12 三连发的问题,你加了几个初始化手段,同一件事就会执行几次。排查方法是看日志时间点,如果同一套初始化相关日志在启动阶段出现多次,优先检查这三个配置是否重复。
第二个坑:在postProcessAfterInitialization里做类型判断时失效。代码里用if (bean instanceof UserService)判断,结果发现命中不了。原因是此时 Bean 可能已经被前面的 AOP 后处理器替换成了代理对象,原始类被藏在了代理 target 背后。这类问题不能只看instanceof,应该通过AopUtils.getTargetClass()或者AopProxyUtils.ultimateTargetClass()拿到最终目标类型再判断。这也是为什么我在写框架级后处理器时,都会把这个“对象可能是代理”的意识刻在脑子里。
第三个坑:在postProcessBeforeInstantiation里返回了非 null 对象,导致依赖注入全没了。这是我见过最隐蔽的坑,因为代码本身不报错,只是运行起来字段全是 null。如果你真的需要在这里返回代理或包装对象,一定要确认原始 Bean 的依赖是否已经被其他地方处理过,否则就把逻辑拆开,不要贪图短路带来的“省事”。记住,短路意味着放弃事件 5 到事件 13 的一整套保障。
源码读到这里你会发现,所谓 15 件事并不是 Spring 从哪本规范手册里抄来的清单,而是在设计时为了给框架级扩展留出足够多的钩子,硬生生把“创建一个对象”这个简单动作拆成了十几个环节。每个环节之间存在严格的先后依赖,一旦某个后处理器破坏了顺序,排查起来往往非常隐蔽。我个人最深的体会是:与其背面试题里的生命周期顺序,不如直接打开源码里doCreateBean走一遍,看到addSingletonFactory、populateBean、initializeBean三个关键词,整张地图就在脑子里了。这篇文章对应的就是地图的全部主干道,看完之后如果遇到启动异常或代理失效之类的问题,你会比之前有更明确的排查方向。