news 2026/10/14 6:43:51

Spring Bean生命周期15件事:从BeanDefinition到销毁全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Bean生命周期15件事:从BeanDefinition到销毁全解析

做后端这些年,Spring 大概是所有人最早接触也最自信的框架。但你敢说自己完全清楚一个 Bean 是怎么从@Component走到容器销毁的吗?我之前也是似懂非懂,直到某次排查项目启动时某个组件被重复创建,被逼着把AbstractBeanFactory#doCreateBean这条源码路径从头到尾过了一遍,才把 Bean 的生命周期真正串起来。标题里说的“15 件事”,其实就是这段路径上 Spring 依次安排的 15 个动作,覆盖从 BeanDefinition 注册、实例化、属性填充、初始化、代理增强,到单例池注册、最终销毁的完整闭环。

这篇文章适合三类人:一直在用 Spring 但没系统看过生命周期的人;准备面试想把这套流程讲清楚的人;想在团队内部做框架封装、需要从生命周期扩展点入手的人。我会先把 15 件事的总清单列出来,再按四个阶段展开讲每个动作背后的设计理由,最后给一张可直接“抄作业”的扩展点速查表和几个真实的踩坑记录。

1. 15 件事全览:Bean 生命周期的四个阶段框架

1.1 拆解清单:从 BeanDefinition 到单例池的时间线

先把结论放在这里。下面这张表就是我根据源码路径整理的 15 个事件,顺序完全对应一个普通单例 Bean 从注册到销毁的过程。建议先花两分钟把这张表过一遍,后面每一节都能对上号。

事件所属阶段触发点这一动作在干什么
1BeanDefinition 阶段读取配置/扫描包把@Component、@Bean、XML 配置解析成 BeanDefinition 并注册
2BeanDefinition 阶段容器启动早期执行 BeanFactoryPostProcessor,允许修改 BeanDefinition
3BeanDefinition 阶段真正创建前合并父子 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 解决这个问题的过程是:

  1. A 开始创建,实例化完成后,Spring 在doCreateBean里调用addSingletonFactory,把 A 的ObjectFactory放进第三级缓存singletonFactories;
  2. A 进入populateBean,发现需要 B,开始创建 B;
  3. B 实例化完成后,同样把ObjectFactory放进三级缓存,然后进入属性填充,发现需要 A;
  4. B 从三级缓存中拿到 A 的ObjectFactory,执行getEarlyBeanReference得到 A 的早期引用,放入二级缓存earlySingletonObjects;
  5. 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 是初始化阶段最容易被面试官连环追问的部分,也是实际验证优先级的最佳位置。真实的调用顺序是:

  1. @PostConstruct:由事件 10 的CommonAnnotationBeanPostProcessor触发;
  2. InitializingBean.afterPropertiesSet():事件 11;
  3. 自定义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事件 12XML 配置或@Bean(initMethod)指定初始化方法
BeanPostProcessor.postProcessAfterInitialization事件 13AOP 代理、全局包装、只读增强
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三个关键词,整张地图就在脑子里了。这篇文章对应的就是地图的全部主干道,看完之后如果遇到启动异常或代理失效之类的问题,你会比之前有更明确的排查方向。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:43:32

C#二次开发SolidWorks:遍历零件与装配体所有面面积并导出

1. 从“给模型算面积”说起&#xff1a;需求背景与API对象结构前一阵子在写自动化成本评估工具&#xff0c;产品结构里有一步要统计每个零件的表面面积——喷涂用量、电镀成本、包装膜尺寸、表面处理工时&#xff0c;全都指着这个数。过去靠手工在SolidWorks里一个个面点过去&a…

作者头像 李华
网站建设 2026/10/14 6:43:21

Ponytail插件保姆级教程:从注入原理到Skill玩法

最近在折腾前端调试&#xff0c;发现一个名字很有意思的浏览器插件&#xff1a;Ponytail。直译过来是“马尾辫”&#xff0c;可它跟发型没有半毛钱关系——准确说&#xff0c;它是一个网页脚本与样式注入工具&#xff0c;通过把一段你自定义的JavaScript或CSS挂在目标网页底部执…

作者头像 李华
网站建设 2026/10/14 6:42:55

让AI Agent自己动手操作浏览器:从原理到实战封装

这系列写到第七篇了。前面几篇都在聊怎么把 AI Skills 拆成可落地的实战技能&#xff0c;有数据处理、文档生成、代码审查&#xff0c;今天聊一个我很早就想写、但一直觉得时机没到的方向——让 AI 自己动手操作浏览器。说白了&#xff0c;就是让 AI Agent 不光能“想”&#x…

作者头像 李华
网站建设 2026/10/14 6:42:52

.NET生态主流ORM选型指南:EF Core、Dapper、SqlSugar对比与实战

做.NET开发的人应该都有过这种纠结&#xff1a;项目里到底该用哪个ORM&#xff1f;这问题看着简单&#xff0c;真到了拍板的时候&#xff0c;很多人还是靠惯性——别人用EF Core我也用&#xff0c;老项目用Dapper就一直用。我以前带团队做某跨平台系统时&#xff0c;前期在EF C…

作者头像 李华
网站建设 2026/10/14 6:42:34

2586份IEC标准合集:分类、版本管理与工程应用实战指南

做电气、电子、电力这行的人&#xff0c;手上没有几套标准全文&#xff0c;干活总像隔着一层纱。我早年调一批出口产品的电源部分&#xff0c;就因为对某条安规细节的理解和客户不一致&#xff0c;整批被打回来重做&#xff0c;工期白扔了两周。从那以后&#xff0c;我养成了一…

作者头像 李华