Spring Bean 生命周期这个话题,几乎是每个做 Java 后端的人都会撞上的一堵墙。不管是初学 Spring Boot 时被各种莫名其妙的 Bean 初始化顺序搞得头疼,还是面试时被追问“Bean 什么时候实例化、什么时候初始化、什么时候销毁”,又或者是排查某个 Bean 里注入的属性居然是 null 的诡异问题,最后都要绕回生命周期。我最初看 Spring 源码时也觉得这块太散,后置处理器、Aware 接口、三级缓存、@PostConstruct 散落得到处都是,但真正把整条时间线捋顺之后,再去读 Spring 的 Bean 创建核心逻辑,就会有一种“原来如此”的通透感。这篇文章想做的,就是把我自己整理的这套生命周期全景图、关键机制和实战排查经验完整地分享出来,让想系统学习 Spring 的读者能一次搞清楚这件事。
这篇文章适合正在啃 Spring 源码的人、准备 Spring 面试的开发者,以及在项目中经常遇到 Bean 相关诡异问题的同学。我会从生命周期全景开始讲,然后深入三级缓存和循环依赖,再拆解初始化阶段的各个扩展点,最后带上一线排查问题的实录,内容尽量做到既能帮助理解,也能直接拿来用。
1. 生命周期到底在管理什么:先给 Bean 的一生画条时间线
很多教程一上来就扔一张包含十几个阶段的流程图,看着很唬人,但如果你不知道每个阶段到底“管什么”,记再多的名字也白搭。我一直觉得,Bean 的生命周期本质上就是“容器替你把一个普通对象变成可管理组件,再用完后再妥善回收”的完整过程。这里面不只有 new 和销毁,还夹着一堆用于注入依赖、执行初始化逻辑、植入代理对象的工作。
1.1 从“创建对象”到“容器销毁”,其实分成两大段
先把 Bean 的一生粗略切成两段:创建期和使用期。
创建期从容器启动时扫描或读取配置开始,Bean 定义(BeanDefinition)被解析出来,然后依次经历实例化、属性填充、初始化等步骤,最终得到一个完全可用的 Bean,放入容器缓存。这段时间是生命周期里最复杂、最核心的部分,Spring 的三级缓存、后置处理器、Aware 注入都集中在这里。
使用期就比较“平淡”了,Bean 已经被容器管理起来,业务代码通过依赖注入或者从容器中获取来使用它。但它并不是一直躺在那里不动,Spring 会通过 AOP 代理等机制包装它,你在使用期拿到的常常不是原始对象,而是增强后的代理对象。等到容器关闭时,进入销毁期,Spring 会调用销毁方法,释放资源。
之所以要这样设计,本质上是因为容器需要在“对象创建”和“业务使用”之间插入一系列控制逻辑。如果没有生命周期这个抽象,每次 new 出来的对象就是孤立的,依赖注入、AOP、事务管理这些能力都无从附着。可以说,理解了生命周期,你就理解了 Spring 作为一个 IoC 容器的核心控制逻辑。
1.2 一张表看懂 Bean 的完整生命周期
我把创建期到销毁期的关键节点整理成一张表,这份表格也是我在看源码时反复对照用的参考:
| 阶段 | 触发点 | 核心工作 | 主要扩展点 |
|---|---|---|---|
| BeanDefinition 解析 | 容器启动 | 读取 XML/注解/配置类,生成 BeanDefinition | BeanFactoryPostProcessor |
| 实例化 | 创建 Bean 实例 | 通过构造器或工厂方法 new 出对象 | InstantiationAwareBeanPostProcessor |
| 属性填充 | 实例化之后 | 按依赖注入规则填充属性 | Autowired、@Resource、@Value |
| Aware 回调 | 属性填充后 | 注入容器相关信息 | BeanNameAware、BeanFactoryAware、ApplicationContextAware |
| 初始化前 | 初始化前置处理 | 后置处理器可对 Bean 做包装或修改 | BeanPostProcessor.postProcessBeforeInitialization |
| 初始化 | 执行初始化逻辑 | 调用自定义初始化方法 | @PostConstruct、InitializingBean、init-method |
| 初始化后 | 初始化完成之后 | AOP 动态代理等关键操作发生在这里 | BeanPostProcessor.postProcessAfterInitialization |
| 使用中 | 业务调用 | Bean 被注入到其他组件或从容器获取 | 无 |
| 销毁前 | 容器关闭前 | 释放资源、执行清理逻辑 | @PreDestroy、DisposableBean、destroy-method |
这张表里最值得注意的点是“初始化后”这个阶段,Spring AOP 的代理对象就是在这个阶段生成的。很多初学者会困惑为什么自己打印 Bean 时看到的类名带$$EnhancerBySpringCGLIB,就是因为 Bean 在初始化后被后置处理器包了一层代理。这个阶段理解不到位,后面排查代理失效、事务不生效的问题时就会很痛苦。
2. 三级缓存和循环依赖:生命周期里最容易翻车的一段
聊生命周期很难绕过循环依赖和三级缓存,因为这俩是“实例化和属性填充”这两个阶段互相纠缠的产物。我记得第一次看三级缓存代码时,愣是盯着getSingleton方法看了半天才反应过来,它其实是为了解决“先有鸡还是先有蛋”的问题。
2.1 为什么单例 Bean 需要三级缓存
场景是这样的:A 依赖 B,B 又依赖 A,两个 Bean 都是单例。容器先创建 A,实例化出 A 的原始对象后要把 B 注入进去,可 B 还没创建;于是转去创建 B,B 又要注入 A,可 A 此时还没“初始化完成”。如果容器严格要求“完整 Bean 才能给别人用”,这个闭环就永远解不开。
Spring 的办法很巧妙:允许在“实例化完成但未初始化完成”的时候,先把 A 的早期引用暴露出去,让 B 先拿着这个引用继续走,等 A 初始化完了,B 拿到的引用也就自动可用了。
这里有个关键点:为什么是三级缓存,而不是两级?三级缓存存的是什么?简单说,一级缓存存的是成品 Bean,二级缓存存的是早期暴露的原始对象引用,三级缓存存的是“对象工厂”(ObjectFactory),这个工厂可以在需要时生成代理或原始对象。引入三级缓存的目的,是为了让 Spring 在存在 AOP 代理的情况下,也能保证 B 注入到的是最终的代理对象,而不是被 AOP 包装前的原始对象。如果你只搞两级缓存,代理对象和早期引用的一致性很难保证。
2.2 三级缓存各自存什么,什么时候用哪一级
我直接用代码逻辑来说明,Spring 在处理getSingleton(beanName)时的查找顺序是固定的:
- 先查一级缓存
singletonObjects,这是成品 Bean 的所在地,业务中拿到的 Bean 大部分来自这里。 - 再查二级缓存
earlySingletonObjects,这里存的是早期暴露的原始对象,可能还没走完初始化。 - 最后查三级缓存
singletonFactories,这里存的是ObjectFactory,调用它的getObject()才能得到早期引用。
三级缓存的 set 过程也很讲究:addSingletonFactory发生在实例化完成、属性填充之前,也就是 Bean 刚 new 出来就把工厂丢进三级缓存。等到真正需要提前暴露时,再从三级缓存里取出工厂,调用getObject()得到早期引用,放进二级缓存,同时从三级缓存移除。
注意:这个设计里最精妙的就是“延迟生成”。如果没有循环依赖,三级缓存的工厂可能永远不会被调用,Bean 会走完正常流程直接进一级缓存。也就是说,三级缓存是“备而不用”的机制,性能开销几乎为零。
2.3 循环依赖的三种情况:能解的、不能解的、和根本不该解的
不是所有循环依赖都能被解决。我把常见情况分成三类,方便排查时快速判断:
- 能解的:单例 Bean 的 setter 注入(属性注入)形成的循环依赖。这是 Spring 默认能处理的,因为实例化和属性填充分开了,有提前暴露的余地。
- 不能解的:构造器注入形成的循环依赖。因为构造器注入要求先有完整依赖才能实例化,Bean 还没 new 出来就卡住了,根本没有“早期引用”可以暴露。
- 根本不该解的:
@Async、事务代理等造成的循环依赖,以及原型作用域的循环依赖。即使“能解”,也不建议依赖这个机制解决问题,因为代码耦合度会很高。
我做项目时有一条原则:循环依赖能消除就消除,通常用“设计上拆开依赖关系”来解决,比如把互相依赖的逻辑抽到第三个组件中。毕竟三级缓存是兜底机制,不是让你用来设计业务架构的。
3. 初始化阶段的扩展点:@PostConstruct、InitializingBean、init-method 到底按什么顺序执行
如果说生命周期是一条时间线,那初始化阶段就是上面最热闹的几个时间点。很多面试题喜欢问:一个 Bean 同时实现了InitializingBean、定义了init-method,方法上还加了@PostConstruct,执行顺序到底是啥?这个问题我当年也答错过,后来看源码才彻底记住。
3.1 三个初始化钩子的执行顺序
直接给结论:@PostConstruct→InitializingBean.afterPropertiesSet()→init-method。
源码位置在AbstractAutowireCapableBeanFactory.initializeBean()里,真正执行初始化的方法是invokeInitMethods(),它会先检查是否实现了InitializingBean,再检查是否配置了自定义的 init 方法。那@PostConstruct凭什么排在最前面?因为它不是在这里执行的,而是在initBean之前通过CommonAnnotationBeanPostProcessor这个“初始化前置处理器”触发的。
换句话说,@PostConstruct本质上是后置处理器的钩子,而不是 Bean 自己实现的接口。这个细节很关键:如果你在一个类上同时用了这三种方式,并且还配置了 Bean 后置处理器,那就会形成一条执行链,越靠后优先级越低,越容易被后置处理器覆盖或增强。
我自己的经验是:现代 Spring Boot 项目里,能用@PostConstruct就用它,InitializingBean也可以,尽量少用init-method。因为在 XML 时代init-method是主流,但注解驱动时代它反而不直观,容易漏配。
3.2 后置处理器:Bean 生死的“全局拦截器”
后置处理器才是生命周期里真正的大佬。它分为两类,一类作用于 BeanDefinition 层面,就是BeanFactoryPostProcessor,它在 Bean 实例化之前执行,可以修改 Bean 的定义信息,比如把某个 Bean 的作用域从 singleton 改成 prototype;另一类作用于 Bean 实例层面,也就是BeanPostProcessor,它在每个 Bean 初始化前后都会被调用。
BeanPostProcessor的接口只有两个方法:postProcessBeforeInitialization和postProcessAfterInitialization。但它的子接口InstantiationAwareBeanPostProcessor还多了postProcessBeforeInstantiation和postProcessAfterInstantiation,能干预实例化过程本身。
举一个实际场景:写一个全局日志组件,想在每个 Service Bean 创建时自动记录它的类名,最简单的方案就是实现BeanPostProcessor,在postProcessAfterInitialization里判断bean instanceof Service然后记录。这个思路比在每一个类里加@PostConstruct干净得多,也是一些框架实现公共逻辑的底层套路。
提示:如果你实现了自己的
BeanPostProcessor,千万别在里面做太重的事情,因为容器里每个 Bean 创建都会经过它,性能影响是全局的。我见过有同事在里面的postProcessAfterInitialization里做远程调用,结果容器启动慢了好几秒,排查半天才定位到。
3.3 Aware 接口注入:让 Bean 知道自己在哪、叫什么
除了依赖注入,Spring 还提供了一组 Aware 接口,让 Bean 在初始化阶段能感知到容器的各种信息。常见的有BeanNameAware(拿到 Bean 名)、BeanFactoryAware(拿到 BeanFactory)、ApplicationContextAware(拿到 ApplicationContext)。
这些回调的触发时机也是确定的:属性填充完成后、初始化之前。源码里invokeAwareMethods()和applyBeanPostProcessorsBeforeInitialization()是先后执行的,具体顺序是 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware,然后是各种 BeanPostProcessor。
平时写业务代码时我几乎不用 Aware 接口,但在写框架组件或做一些通用工具时会频繁用到。比如自定义一个注解,想要在运行时拿到当前 ApplicationContext 去动态获取其他 Bean,那实现ApplicationContextAware是最直接的方案。
还有一个常见的坑:如果你在静态方法里想用容器中的 Bean,就得靠ApplicationContextAware把上下文存到静态字段里去。在这个过程中要格外注意执行时机,因为ApplicationContextAware回调发生时,Bean 还没有初始化完成,不能在这个方法里直接使用注入的其他 Bean 做复杂逻辑,否则容易拿到 null 或者引发循环依赖。
4. 生产环境实战:Bean 生命周期异常排查实录
理论聊太多容易飘,下面分享几个我真实踩过的坑。每一个问题在表面上都是“某功能突然不 work”,但定位到最后,全是生命周期某个节点的时机问题。
4.1 问题一:Bean 属性为 null,查了半天不是注入问题
有一次线上接口偶发 NPE,查日志发现是某个 Service 里的配置属性是 null。第一反应是看application.yml配置和@Value注解,结果配置都对。后来打点才发现,那个 Service 在构造方法里就试图使用注入的属性,可属性填充发生在构造方法之后,自然而然拿不到值。
这种问题本质上是混淆了“构造方法执行时机”和“属性注入时机”。构造方法执行时,Bean 刚被 new 出来,依赖还没灌进去。如果你必须在构造阶段使用其他 Bean 或配置值,有两种改法:一是把逻辑挪到@PostConstruct方法里,这时属性已经填充完成;二是用构造器注入,直接把依赖作为构造方法参数传入,而不是靠字段注入。
这是生命周期理解不到位时最容易出的问题。很多人只知道“构造方法里能写代码”,没意识到对 Spring 管理的 Bean 来说,构造方法只是实例化阶段的一部分,此时 Bean 还非常“原始”。
4.2 问题二:循环依赖报错,怎么判断该不该解
项目里有段时间一启动就报BeanCurrentlyInCreationException,报错信息会直接告诉你类似 “The dependencies of some of the beans in the application context form a cycle” 这样的信息。我一看,A 和 B 互相引用,但用的都是构造器注入,Spring 根本没法自动解决。
排查思路分两步:先确认是不是必须构造器注入。如果是字段(属性)注入,Spring 能通过三级缓存解决,报错概率很低。这个场景之所以报错,就是因为构造器注入让实例化阶段就卡住了,连早期引用都建立不了。
再确认这两个类是不是真的需要互相依赖。很多时候循环依赖是因为职责边界没划清楚,A 依赖 B 做校验,B 又依赖 A 做关联查询。这种我往往会引入一个 C 组件,把两者共同的依赖逻辑下沉,让 A、B 都只依赖 C。虽然多写一个类,但依赖关系变清晰了,后续维护也轻松很多。
经验:Spring Boot 2.6 版本开始默认禁止循环依赖,
spring.main.allow-circular-references默认是 false。即使不用 Spring Boot,我也建议把“允许循环依赖”当成一个需要审查的配置项,而不是顺手就打开。生命周期机制能帮你兜底,但不代表你应该依赖它。
4.3 问题三:初始化顺序错乱导致配置没生效
有一次在做多数据源切换的配置中心时,发现某个 Bean 里的配置值一直没刷新。最初的代码是在 Bean 的@PostConstruct方法里读取配置快照并缓存下来。问题在于@PostConstruct在 Bean 初始化阶段只执行一次,而配置中心的值后来发生了变化,等下一次刷新时缓存还是旧值。
这个问题的本质是“初始化”和“运行时更新”被混为一谈。生命周期里的初始化动作只负责 Bean 创建时的一次性启动,如果后续状态需要动态变更,得重新设计刷新机制,比如定时拉取配置后更新容器中的 Bean,或者直接注入一个动态配置源。
还有一些场景,希望容器启动时按特定顺序执行某些初始化动作。很多人会想到实现ApplicationRunner或CommandLineRunner,它们确实在容器刷新完成、Bean 全部就绪后执行,适合做一些全局启动任务。如果只依赖@PostConstruct,因为各个 Bean 创建顺序不一定符合业务预期,就很容易踩顺序坑。
5. 生命周期与 Spring Boot 注解驱动的融合
Spring Boot 时代,大部分人已经不用 XML 配置 Bean 了,日常开发的 Bean 声明方式主要是@Component、@Service、@Repository、@Controller,以及配合@Configuration类的@Bean方法。虽然声明方式变了,但生命周期的底层逻辑没有本质变化,变化的是扩展的入口更利于写业务代码了。
5.1 注解注入方式下的生命周期差异
@Bean方法定义的 Bean 和非@Bean方法定义的 Bean,生命周期其实完全一样,因为 Spring 最终都用BeanDefinition统一描述它们。差异更多体现在“配置方式”上,比如@Bean(initMethod = "init")和@Bean(destroyMethod = "destroy")可以直接指定初始化和销毁方法,这比实现接口更灵活。
不过要注意@Bean方法本身的执行时机。@Configuration类中的@Bean方法在容器启动时就会执行,方法内部 new 出来的对象实例会被注册为容器管理的 Bean。如果你在@Bean方法里依赖了其他 Bean 的实例,最好通过方法参数声明,让 Spring 帮你注入,而不是直接调另一个@Bean方法。虽然 Spring 的 CGLIB 增强可以保证这种调用返回同一个单例,但依赖通过参数传入会清晰得多,也能避免某些场景下代理失效的问题。
对@Component一类的注解类,最常用的生命周期钩子就是@PostConstruct和@PreDestroy。这两个注解来自 JSR 250 规范,本质上和 Spring 无关,只是在 Spring 容器里执行时机被固定了。我平时写缓存预热类、资源初始化组件,都会用@PostConstruct;写连接池管理类,用@PreDestroy做关闭资源,非常顺手。
5.2 微服务与 AI 场景下 Bean 生命周期的应用
把话题稍微扩展一下。现在的后端项目越来越复杂,Spring Cloud Alibaba、Spring AI 这些生态组件大量侵入日常开发。这些框架本质上都依赖 Spring 的 Bean 扩展机制来实现“自动配置”和“便捷注入”。比如微服务里的配置刷新、服务发现,很多都是通过各种后置处理器或监听器在生命周期阶段把能力注入到业务 Bean 中。
拿 Spring AI 场景来说,一个模型调用的客户端 Bean,往往会通过自动配置类创建,然后注入到业务代码里。如果你对这些 Bean 的生命周期节点不熟悉,遇到“模型客户端没有正确初始化”这种问题时就特别被动。而且随着spring-ai-alibaba这类组件在 nl2sql、RAG 场景里用得越来越多,理解 Bean 的创建顺序和销毁行为,能帮你更好地判断某个模型实例是应该作为单例常驻,还是每次请求重建。
还有一个小技巧:在排查自动配置类是否生效时,可以直接注册一个BeanPostProcessor,在里面打印创建过的 Bean 名,这样就能看到容器里 Bean 的实例化先后顺序。这个方法比打开一堆调试日志更直观。
最后分享一点实际体会
看了很多源码、排了不少实际问题之后,我的感受是:Spring Bean 生命周期不是一个需要“背”的知识点,而是一张可以不断对照实践的“地图”。你把实例化、属性填充、初始化、销毁这些节点记清楚,再搞明白后置处理器和 Aware 回调的插入时机,很多框架的黑盒行为就自动揭开了。尤其是当你开始手写 Spring 这类学习项目时,自己实现一遍 Bean 的创建流程,再回来看官方源码,那种能“对上号”的感觉特别爽。
如果你现在正被某个 Bean 相关的问题困扰,不妨先把报错信息里涉及到的 Bean 名记下来,然后用BeanPostProcessor打印一下它创建过程中的各个时机,看看是不是卡在某个阶段了。绝大多数诡异问题,最后都能在生命周期的时间线上找到答案。