首页 / 资讯中心 / 文章详情

Spring Bean生命周期全解析:回调顺序与实战避坑

Spring Bean生命周期全解析:回调顺序与实战避坑 ★ FEATURED ARTICLE
前阵子帮同事排查了一个挺诡异的启动问题某个Bean的PostConstruct方法里做了缓存预热代码翻来覆去看了三遍都没找到问题新环境启动后缓存却一直是空的。最后发现这个Bean根本就不是Spring容器管理的——他在另一个工具类里直接new了一份注解和生命周期回调一概不生效。这种事在开发中特别常见。很多人用Spring用了好几年遇到“Bean表现和预期不一样”时习惯性怀疑配置写错、注解漏加却很少往“这个Bean到底是不是容器在管”的方向想。而一旦用Bean生命周期的视角重新审视很多诡异问题都能瞬间找到答案。Bean生命周期说白了就是Spring容器里一个对象从“出生”到“销毁”的全过程实例化、属性填充、初始化回调、正式使用、销毁清理。这篇文章我会从完整的时间线讲起带大家亲手搭一个观测Demo把每个节点的执行顺序用日志看一遍最后盘点几个真实踩过的坑和排查方法。适合刚学Spring的读者建立整体认知也适合工作一两年的开发者把这块知识真正落到项目里。1. 为什么每个写Spring的人都该搞懂Bean生命周期1.1 从一次诡异的初始化失败说起回到开头那个问题。同事负责的缓存预热组件设计上是在PostConstruct里把一批热点数据加载进本地缓存。新环境启动后接口查询本地缓存总是Miss但数据库里明明有数据。当时的第一反应是组件坏了可代码检查了三遍也没问题本地跑又是好的。最后怎么发现的他在那个组件的构造器里打了个断点启动时根本没走到。这才回想起来另一个工具类里写了new CacheWarmer()而且那个工具类被一条静态调用链提前触发了。听起来很基础对吧但这种事在真实项目里发生的频率比你想象得高得多。手动new出来的对象生命周期完全脱离容器管理Autowired不生效、PostConstruct不执行、AOP代理也不会作用到它身上。表面上代码能跑一旦依赖这个对象做初始化或代理增强问题就会以各种奇怪的形式冒出来。1.2 Bean生命周期到底在管理什么在没有框架的年代对象都是自己管的自己new、自己set属性、自己处理依赖用完之后还得记得释放资源。Spring的核心IoC做的事就是把这摊子事接管过来。用户只需要描述“我需要一个什么Bean”容器负责决定什么时候创建、怎么填充属性、初始化前做哪些处理、销毁后怎么收尾。可以做一个类比Bean生命周期就像一个人入职一家公司的流程。简历通过以后先办入职实例化人事帮你录入信息、配好工位属性填充然后参加入职培训初始化回调之后才真正开始干活业务调用离职的时候要交接工作、退还设备销毁回调。容器就是那套HR系统在每个环节留好了固定“插槽”让你有机会在合适的时机插入自己的逻辑。这套“插槽”就是Spring留给我们的一堆回调接口和注解Aware系列、PostConstruct、InitializingBean、init-method、BeanPostProcessor、PreDestroy、DisposableBean、destroy-method。理解生命周期本质上是理解这些插槽在什么位置、按什么顺序触发。1.3 这份知识适合谁我一般把读者分成三类。第一类是刚学Spring的新手框架用起来能跑但不知道“为什么能跑”看完可以建立一张完整的认知地图。第二类是准备面试的开发者Bean生命周期是高频考点考官尤其喜欢追问“PostConstruct、InitializingBean、init-method谁先谁后”顺序答不上来基本就凉了。第三类是已经在排查类似问题的老手需要的不再是基础概念而是能在源码里快速定位阶段、知道在哪个环节插入自定义逻辑。实操部分和避坑章节主要是为第三类读者准备的。2. Bean从出生到销毁完整时间线里最容易被忽略的五个节点2.1 实例化对象刚成型还谈不上“能用”生命周期第一站是实例化。容器拿到Bean定义后通过构造器或者工厂方法创建对象。大多数情况下走的是无参构造器如果Bean定义里指定了构造参数容器会按类型做推断和匹配。这一步结束之后对象只是个“空壳”内部字段都是默认值依赖也没注入进来。这里有个新手很容易踩的第一个坑在构造器里做初始化逻辑比如调用远程接口、加载配置文件。因为构造器执行的时候依赖还没填充你在构造器里访问任何需要注入的字段拿到的都是null。道理很简单但确实有大量线上故障就是这么来的某人在构造器里访问了一个注入的Service平时运气好没出问题某天重新调整了Bean定义顺序就炸了。2.2 属性填充依赖注入真正落地的地方实例化完成后容器进入属性填充阶段。这个阶段做的事就是依赖注入Autowired注解的字段、Value配置、XML里配置的property都是在这里被容器找到并赋值的。Spring做的事情很简单找到目标Bean的依赖如果依赖还没创建就去创建创建好了再塞进来。这里牵出一个著名话题——循环依赖。如果A依赖BB又依赖A容器在填充A的属性时发现需要B就会去创建B创建B填充属性时又发现需要A这时A已经完成实例化但还没完成初始化。Spring用三级缓存里的“早期引用”把A先暴露给B让两个Bean先各自完成属性填充。需要注意这个机制只在字段注入或者Setter注入的循环依赖下有效构造器循环依赖是没法解决的因为构造器阶段对象还没创建出来根本没有“早期引用”可给。从Spring Boot 2.6开始默认就把循环依赖关掉了启动时直接报错逼你把设计改成不循环的。这是生命周期知识在实际项目里最痛的教训之一。2.3 初始化回调三兄弟的固定顺序属性填充完成后才进入真正意义上的初始化阶段。这一阶段有三个主流回调顺序固定PostConstruct → InitializingBean.afterPropertiesSet → init-method。回调方式所属体系执行顺序适用场景PostConstructJSR-250标准注解最先初始化后置动作不依赖Spring包InitializingBean.afterPropertiesSetSpring接口其次需要依赖注入完成后的校验或逻辑init-methodBean配置项最后Bean无需感知Spring适合外部类先说明一个容易误解的细节PostConstruct其实是在BeanPostProcessor.postProcessBeforeInitialization阶段被容器内置的CommonAnnotationBeanPostProcessor触发的从执行顺序上说它确实在afterPropertiesSet之前。后面做Demo时你会看到日志里它确实排在所有初始化逻辑的最前面。三种方式怎么选我的习惯是如果只是做一次简单的启动后置动作用PostConstruct最直观因为它是标准注解不依赖Spring的包如果需要逻辑复用可以考虑InitializingBean但代价是业务类要和Spring接口耦合init-method是配置级方案Bean本身不用实现任何接口适合外部类Bean。三者可以共存共存时顺序就是上面那张表。在初始化回调之前还有一组Aware回调会被触发BeanNameAware能告诉你自己在容器里的名字BeanFactoryAware能让你拿到BeanFactoryApplicationContextAware能让你直接操作ApplicationContext。它们让Bean获得感知容器的能力但过度使用会让业务代码和Spring容器耦合属于“知道有这机制、但尽量少用”的那种。2.4 BeanPostProcessor全局管家的两个钩子初始化阶段还有一个隐藏的关键角色就是BeanPostProcessor。它提供两个钩子postProcessBeforeInitialization在Aware回调之后、PostConstruct之前执行postProcessAfterInitialization在所有初始化动作完成后执行。注意BeanPostProcessor不是针对某一个Bean而是对容器里所有Bean生效。Spring自己的很多核心功能都靠它实现AutowiredAnnotationBeanPostProcessor负责处理AutowiredApplicationContextAwareProcessor负责触发Aware接口AOP代理实际上也是在postProcessAfterInitialization阶段用代理对象替换了原始Bean。正因为它是全局的写的时候要格外小心。如果在这里面做耗时的远程调用等于每个Bean初始化时都要等一次启动时间直接飙升。还有postProcessAfterInitialization返回的对象会被容器当成最终的Bean如果偷懒直接返回原始对象AOP代理就不会生效。不要随便返回nullnull会导致后续Bean无法正确装配我在线上遇到过由此引发的神秘NPE。2.5 销毁阶段优雅退场的三个步骤最后是销毁阶段。单例Bean在容器关闭时会被清理触发顺序PreDestroy → DisposableBean.destroy → destroy-method。如果你在容器里管理了线程池、打开了连接、注册了定时任务这里就是清理现场的地方。有个容易被忽略的点很多人只在IDE里直接停掉进程从没调用过close方法那Spring根本没机会执行销毁回调。JVM进程直接杀掉Spring帮不了你。想让容器监听JVM退出并优雅关闭需要调用registerShutdownHook或者用Spring Boot时应用本身已经注册了shutdown hook正常关闭能走完整个销毁流程。另外原型Bean的销毁回调不由容器负责。容器只跟踪单例Bean的完整生命周期原型Bean每次获取都会新建一个容器自然不会替你记着要销毁谁。如果你的原型Bean持有外部资源清理责任就得自己承担。3. 实操搭一个能看见每个节点的Bean生命周期观测Demo3.1 工程准备一个足够干净的Spring上下文为了不被Spring Boot的自动配置干扰输出我用最朴素的Spring上下文来演示。新建一个Maven工程引入spring-context和javax.annotation-apiJava 8配合Spring 5.3.x示例代码直接复制就能跑。dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency dependency groupIdjavax.annotation/groupId artifactIdjavax.annotation-api/artifactId version1.3.2/version /dependency /dependencies用注解配置类做演示AppConfig放在com.demo.lifecycle包下。如果你的项目用的是Spring 6或者Spring Boot 3注解包名要换成jakarta.annotation其余逻辑不变。3.2 写一个挂满回调的测试Bean这个Bean把能挂的回调全部挂上每一行日志都带上自己的阶段标记。我故意让它既实现接口、又加注解、又在Bean里指定init-method和destroy-method这样三种回调同时存在的顺序一眼就能看明白。public class LifecycleBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { private String name; private Collaborator collaborator; public LifecycleBean() { System.out.println([1] 实例化构造器执行); } Autowired public void setCollaborator(Collaborator collaborator) { System.out.println([2] 属性填充注入Collaborator); this.collaborator collaborator; } PostConstruct public void doPostConstruct() { System.out.println([5] PostConstruct 执行); } Override public void afterPropertiesSet() { System.out.println([6] InitializingBean.afterPropertiesSet 执行); } public void customInit() { System.out.println([7] 自定义init-method 执行); } PreDestroy public void doPreDestroy() { System.out.println([10] PreDestroy 执行); } Override public void destroy() { System.out.println([11] DisposableBean.destroy 执行); } public void customDestroy() { System.out.println([12] 自定义destroy-method 执行); } Override public void setBeanName(String name) { this.name name; System.out.println([3] BeanNameAware.setBeanName 执行beanName name); } Override public void setBeanFactory(BeanFactory beanFactory) { System.out.println([4] BeanFactoryAware.setBeanFactory 执行); } }Collaborator就做一个普通的空类不打印日志免得它的生命周期输出干扰观察主线。配置类里用Bean定义LifecycleBean并且显式指定initMethod和destroyMethodConfiguration ComponentScan(com.demo.lifecycle) public class AppConfig { Bean(initMethod customInit, destroyMethod customDestroy) public LifecycleBean lifecycleBean() { return new LifecycleBean(); } }这里顺手说一个很多人会误解的点Bean方法里return出来的对象并不是直接就是最终Bean。它同样要走完整生命周期initMethod、BeanPostProcessor、销毁回调都会作用在这个对象上。3.3 自定义BeanPostProcessor当“观测员”再写一个BeanPostProcessor。为了日志顺序稳定我只在postProcessAfterInitialization里打印因为这一阶段一定发生在所有初始化回调完成之后。postProcessBeforeInitialization的相对位置会受BPP注册顺序影响Demo里先不掺和。Component public class LifecycleLogPostProcessor implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifecycleBean) { System.out.println([8] BeanPostProcessor.postProcessAfterInitialization 执行beanName beanName); } return bean; } }用instanceof过滤只关心LifecycleBean避免其他Bean的日志刷屏。如果你也想看看before阶段的输出可以再加一个postProcessBeforeInitialization方法但要注意它相对PostConstruct的先后顺序取决于多个BPP之间的注册顺序输出位置可能会变这是正常现象。3.4 启动容器逐行解读日志里的顺序启动类很简单创建上下文、拿Bean、调用一下业务方法、关闭容器。public class LifecycleDemo { public static void main(String[] args) { AnnotationConfigApplicationContext ctx new AnnotationConfigApplicationContext(AppConfig.class); LifecycleBean bean ctx.getBean(LifecycleBean.class); System.out.println([9] 使用阶段调用业务方法); ctx.close(); } }运行后控制台输出如下[1] 实例化构造器执行 [2] 属性填充注入Collaborator [3] BeanNameAware.setBeanName 执行beanName lifecycleBean [4] BeanFactoryAware.setBeanFactory 执行 [5] PostConstruct 执行 [6] InitializingBean.afterPropertiesSet 执行 [7] 自定义init-method 执行 [8] BeanPostProcessor.postProcessAfterInitialization 执行beanName lifecycleBean [9] 使用阶段调用业务方法 [10] PreDestroy 执行 [11] DisposableBean.destroy 执行 [12] 自定义destroy-method 执行逐行拆解一下[1]到[2]之间容器先创建对象然后解析Autowired依赖。如果你在[1]对应的构造器里提前访问collaborator一定是null这就是为什么我反复强调不要在构造器里做业务初始化。[3]和[4]是Aware回调BeanNameAware先执行BeanFactoryAware后执行。这里拿到的beanName就是Bean方法的名称“lifecycleBean”。[5]到[7]是初始化回调三兄弟顺序跟理论完全一致。[5]的PostConstruct虽然从代码角度看是注解但实际执行者是容器内置的CommonAnnotationBeanPostProcessor在postProcessBeforeInitialization阶段触发所以它排在接口回调和配置级回调之前。[8]的BPP-after发生在所有初始化工作完成后。AOP代理、各种包装逻辑都在这个阶段发生。如果你在postProcessAfterInitialization里返回了一个代理对象后续getBean拿到的就是那个代理。[10]到[12]的销毁顺序同样是固定的注解先行、接口其次、配置最后。3.5 把这个Demo变成排查直觉这个Demo最大的价值不是让你背顺序而是让你建立一种“阶段感”。遇到任何Bean异常先问一句问题发生在哪个阶段举个例子你发现某个Bean的初始化逻辑和AOP切面行为不一致。如果切面没生效先排查是不是postProcessAfterInitialization返回了别的对象或者你的自定义BPP不小心覆盖了原有代理。再比如你想在容器里某个Bean初始化完成后立刻打印一行启动日志那么应该放在afterPropertiesSet还是init-method里两者都能做到但如果你同时实现了InitializingBean又指定了initMethod就要知道执行顺序不然日志里可能少了一段你预期中的输出。这个可复用的排查套路就是把“背知识点”变成“对照日志看阶段”后面遇到问题会顺手很多。4. 实战避坑常见的Bean生命周期问题与排查思路4.1 为什么PostConstruct没有执行这是最常见的生命周期问题原因通常不复杂但出现得莫名其妙。首先确认Bean是不是容器创建的。方法很简单在构造器加个断点或者加一行日志启动时看有没有走到。没走到说明要么类没有落在被扫描的包路径下要么有人在代码里手动new了一个。前者改扫描范围后者把new改成从容器里拿问题立刻消失。其次注意注解方法本身。PostConstruct方法保持public、无参数、无返回值最稳妥。我在老项目里见过private修饰的PostConstruct方法某些环境下解析正常换个容器版本就失效了。不纠结为什么能跑直接改成public就好。还有一种情况类被CGLIB代理后初始化逻辑可能在父类构造器里重复执行。这不是PostConstruct本身的问题而是代理机制和构造器逻辑搅在一起。解决思路还是那句构造器只做简单赋值别放业务动作。4.2 初始化顺序乱了怎么办多个Bean之间如果有隐式依赖比如A初始化时要调用B的方法但B的初始化还没完成拿到的可能就是个半成品。Spring默认不保证Configuration类里Bean方法的执行顺序也不保证无依赖Bean的初始化顺序。想解决推荐三个方向。一个是显式用DependsOn告诉容器“先初始化谁”。这种方式直白但要小心如果很多Bean之间都有依赖关系DependsOn会变成一张蜘蛛网维护成本很高。另一个是用SmartInitializingSingleton。它的afterSingletonsInstantiated方法会在所有单例Bean创建完成后统一回调适合做“所有Bean都准备好了再执行”的全局逻辑。我更推荐的方向是不要在单个Bean的初始化阶段去依赖另一个尚未初始化的Bean。把跨Bean的协作逻辑放到启动完成后的ApplicationRunner或ApplicationListener里去执行那个阶段所有Bean都已经初始化完毕不需要猜顺序。这是根治顺序问题最优雅的办法。4.3 销毁回调迟迟不执行销毁回调不执行九成和容器关闭方式有关。非Boot项目里直接杀进程Spring完全没机会走销毁流程需要显式调用ctx.registerShutdownHook()让容器监听JVM退出事件。Spring Boot应用默认已经注册了shutdown hook正常收到SIGTERM或者通过actuator触发关闭时销毁流程会执行。另一个容易踩的坑是Bean(destroyMethodclose)。Spring Boot对Bean方法有一个“销毁方法推断”的机制如果你的方法名叫close或者shutdown它会自动把它当成销毁方法。如果你并不想这样做需要显式设置destroyMethod来关掉推断。这个细节坑过不少人尤其是当你引入一个第三方类它的close方法内部行为和你预期完全不一样的时候。4.4 循环依赖下的生命周期“妥协”循环依赖场景下A在属性填充阶段引用了B的“早期引用”此时B的生命周期可能才走到一半。如果B在初始化回调里访问A拿到的A同样可能是没走完初始化的半成品调A的方法大概率NPE或者拿到错误数据。Spring Boot 2.6以后默认禁止循环依赖启动直接抛错。这不是框架变“怂”了而是循环依赖本身就是设计不良的信号。老项目如果还有循环依赖升级Boot版本时就会突然启动失败这也是生命周期知识在实际项目里最痛的教训之一。面对这种情况老老实实重构依赖关系比试图通过调配置绕过要好得多。4.5 排查Bean生命周期问题的方法组合拳最后给一套我实际在用的排查方法。第一看日志。把Spring容器的日志级别调到DEBUG能看到createBean、populateBean、initializeBean这些关键方法的调用轨迹。第二断点。在AbstractAutowireCapableBeanFactory的doCreateBean方法里打断点逐个排查目标Bean。这个类就是整个生命周期的核心执行者顺着调用栈往下走基本能定位到具体是哪个阶段出了问题。第三自定义BPP。像Demo里那样写一个只针对目标类的日志处理器快速圈定问题阶段。BPP是全局的但要学会在调试时“局部化观察”过滤掉无关Bean的噪音。第四看调用栈。出问题时线程栈能直接告诉你这个方法是被谁触发调用的顺着栈往上翻就能找到接线源头。这套组合拳用熟了再诡异的生命周期问题一般半小时内都能定位。5. 几点个人经验文章最后说几句纯个人的体会。第一不要在构造器里做任何业务初始化。这个观点我写了两遍因为它值得构造器只做字段的简单赋值初始化逻辑统一放到PostConstruct或者afterPropertiesSet里。这样可以避开绝大多数生命周期相关的问题。第二没必要把一个Bean的初始化逻辑散落在多个回调里。选一种方式用到底我一般选PostConstruct因为它是标准注解、不绑定Spring、又能确保在依赖注入完成后执行。第三源码是最终的老师。AbstractAutowireCapableBeanFactory的doCreateBean方法就那几百行关键代码顺着调用链读一遍比自己死记顺序强十倍。我第一次读完源码的收获不是记住了哪个回调先执行而是理解了容器设计者为什么要把这些阶段分开——每一段留一个扩展点框架才能在没有侵入业务代码的前提下完成那么多强大功能。第四排查线上问题时多问自己一句这个对象真的是容器在管吗这句话看起来简单却能帮你跳过很多假线索直接看到问题本质。
阅读完成 · 觉得有帮助?
咨询建站