首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Spring Bean生命周期全景解析:三级缓存、循环依赖与初始化扩展点
📅 2026/10/11 8:15:54
✍️ 爱科研究院
👁 阅读 3,247
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/注解/配置类生成 BeanDefinitionBeanFactoryPostProcessor实例化创建 Bean 实例通过构造器或工厂方法 new 出对象InstantiationAwareBeanPostProcessor属性填充实例化之后按依赖注入规则填充属性Autowired、Resource、ValueAware 回调属性填充后注入容器相关信息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 依赖 BB 又依赖 A两个 Bean 都是单例。容器先创建 A实例化出 A 的原始对象后要把 B 注入进去可 B 还没创建于是转去创建 BB 又要注入 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打印一下它创建过程中的各个时机看看是不是卡在某个阶段了。绝大多数诡异问题最后都能在生命周期的时间线上找到答案。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/11 8:15:54
ITS_LIVE冰川流速数据集详解:从影像匹配到实践应用
2026/10/11 8:15:54
传递函数如何描述控制系统:从微分方程到零极点分析实战
2026/10/11 8:10:54
NemoClaw技术解析:3D点云抓取语义分割与姿态估计
2026/10/11 9:00:57
个人 NAS 入门避坑,普通人要不要自建存储?
2026/10/11 9:00:57
冷链车双司机换班,交接要对清哪些东西才不断档?
2026/10/11 9:00:57
南京资质齐全的定制商务车商家挑选全攻略
2026/10/11 9:00:57
外墙涂料推荐供应商实力与用户口碑深度解析
2026/10/11 9:00:56
从rea模块设计到落地:构建高可用实时数据读取层的工程实践
2026/10/11 8:55:56
鲁棒主成分分析(RPCA)的Python实现:低秩稀疏分解实战指南
2026/10/11 0:00:10
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/11 0:00:10
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/11 0:00:10
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/11 0:00:10
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/11 0:00:10
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/11 0:00:10
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/10 3:41:56
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/10 3:41:54
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/9 11:36:17
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)