新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Bean生命周期全解析:从实例化到销毁的完整流程与避坑指南

发布时间:2026/10/2 10:18:10来源:尧图网络
Spring Bean生命周期全解析:从实例化到销毁的完整流程与避坑指南
SpringBean生命周期这名字只要是搞Java的都绕不开。面试被问烂了实际开发中也总因为不了解它踩各种莫名其妙的坑。这篇文章我用一个对Spring容器比较熟悉的开发者的视角把Bean从诞生到销毁的全过程完整梳理一遍配合可以直接跑的验证代码把生命周期里每个阶段做什么事、执行顺序是什么、哪些地方容易出问题全部讲透。适合正在复习Spring原理的候选人也适合项目里遇到Bean初始化诡异问题、想系统排查的老开发。我尽量不用那种搬完源码就完事的讲法每一个阶段都补上设计意图和落地场景你读完不光是背下了顺序还能真正理解Spring为什么把流程设计成这样。1. 为什么必须理解Bean生命周期它在Spring里是什么地位1.1 生命周期管的是哪几件事很多刚学Spring的人最开始接触的就是Autowired、Component脑子里默认Bean就是new一下然后放到容器里。其实从Spring的角度看一个Bean从一个类的描述信息变成容器里一个随时可用的对象再变成被销毁回收的对象中间经过了一整套标准流程。这套流程里不光有构造和销毁还有属性填充、Aware回调、初始化前后处理器、代理生成等环节。用个不太严谨但好理解的类比Bean就像一个入职的员工。先有招聘需求BeanDefinition然后面试选人推断构造方法入职后分配工位、配电脑属性填充再参加入职培训初始化培训完才能正式干活被其他组件依赖。离职时还要交接、注销工牌销毁回调。这一整套入职离职流程就是Bean生命周期。Spring为什么搞这么复杂因为框架要在不侵入业务代码的前提下替你完成依赖注入、配置填充、代理增强这些事情必须卡在合适的时机做合适的动作。理解生命周期不仅能帮你回答面试题更重要的是排查线上问题。比如你可能会遇到PostConstruct里的日志没执行、构造器里调用了Autowired字段结果拿到null、自定义的BeanPostProcessor对某些Bean没生效。这些问题本质都是你不知道流程的某个环节在什么条件下才会触发。1.2 两条主线BeanDefinition与作用域聊生命周期之前得先清楚两个贯穿全程的底层概念。第一条主线是BeanDefinition。Spring容器启动时先把配置类、XML、Component扫描到的类统统解析成BeanDefinition对象这个对象里记录了这个Bean的类名、作用域、是否懒加载、初始化方法名、销毁方法名、属性值、构造参数值等信息。后面所有的实例化、属性填充都是照着BeanDefinition这张图纸来施工的。没有BeanDefinitionSpring根本不知道要给你创建什么。第二条主线是作用域Scope。默认是singleton也就是每个BeanDefinition对应一个实例prototype则每次获取都是新实例。这两类Bean的生命周期差异非常大单例Bean由容器管理完整生命周期销毁方法由容器在关闭时统一调用而prototype Bean创建完交给调用方后容器就不再管它了销毁方法不会自动触发。这个差异很多人用错了后面我会专门展开讲。理解这两条主线后下面拆解生命周期就有抓手了每个阶段到底开发了什么、哪些接口参与、什么条件下会跳过全部对应到具体源码行为上。2. 生命周期核心环节逐段拆解2.1 从BeanDefinition注册到实例化前的准备先看实例化发生之前Spring做了什么。容器启动时AbstractApplicationContext.refresh()会调用invokeBeanFactoryPostProcessors()把配置类里的Bean方法、ComponentScan扫描结果、Import导入的类全部转换成BeanDefinition并注册到BeanFactory。这一步完成后容器已经知道有哪些Bean要创建。真正创建Bean是后续finishBeanFactoryInitialization()阶段触发默认情况下单例非懒加载的Bean在这里一次性全部实例化。实例化之前有一个容易被忽略的环节BeanDefinition合并。如果有父子BeanDefinition子定义会继承父定义的属性、初始化方法等信息Configuration类里的Bean方法返回类型和实际类型不一致时也要在这里做推断。然后是实例化前拦截。这里的关键接口是InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()。如果这个方法的返回值不是nullSpring就直接用这个返回值当作Bean后续的构造、属性填充、初始化全都不走了。AOP里AbstractAutoProxyCreator就是在这里提前返回代理对象来实现某些场景的切面增强的。实际开发中很少直接写这个逻辑但理解它存在你就能解释为什么有的Bean没走构造器就出现了。实例化前还有一件重要的事推断构造方法。Spring从BeanDefinition里拿到构造器列表然后根据Autowired标注、参数个数、容器里可用的候选Bean决定用哪个构造器或者默认用无参构造器。这个推断过程如果失败比如有多个构造器且参数都配不上就会抛出BeanCreationException提示非常明确。2.2 实例化与属性填充Bean从空壳到完整对象构造方法执行完Bean对象在JVM里已经分配了内存但这时候它是个空壳——字段全是默认值依赖还没有注入。接下来进入属性填充阶段populateBean()。这里也藏着一个处理器InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()。如果它返回false整个属性填充过程直接跳过Bean里所有字段保持初始默认值。这个开关平时没人动但如果你做框架二次开发比如要自己接管整个对象的赋值逻辑就会用到它。属性填充内部干活的主力是AutowiredAnnotationBeanPostProcessor它处理Autowired、Value、Resource这类的注入注解。具体流程是先找所有需要注入的属性然后从BeanFactory里解析依赖。这里注意字段注入和setter注入的时机就在这一步如果你在构造器里直接访问Autowired字段拿到的一定是null因为构造器执行时属性填充压根还没开始。填充完成后会调用InstantiationAwareBeanPostProcessor.postProcessPropertyValues()新版本叫postProcessProperties()这时候依赖已经注入好了如果你想对某个Bean的字段做二次包装修改可以在这里动手。比如很多ORM框架用这个钩子给实体类填充代理集合。2.3 初始化阶段前后处理器的接力属性填充结束Bean进入了初始化阶段。这个阶段是Spring框架最灵活的一环也是面试考察的重点。你只要记住一个固定顺序先调用Aware接口的回调方法。再调用BeanPostProcessor.postProcessBeforeInitialization()也就是初始化前置处理。然后执行初始化逻辑先是PostConstruct注解方法再是InitializingBean.afterPropertiesSet()最后是XML或Bean(initMethod xxx)指定的init-method。接着调用BeanPostProcessor.postProcessAfterInitialization()也就是初始化后置处理。Aware回调里最常见的是BeanNameAware、BeanClassLoaderAware、BeanFactoryAware。这三个接口的回调顺序Spring源码里是固定的逐个判断并赋值。它的价值在于让你在拿到完整Bean之前知道自己叫什么名字、由哪个工厂管理。ApplicationContextAware比较特殊它不是由BeanFactory初始化阶段调用的而是由ApplicationContextAwareProcessor这个BeanPostProcessor处理所以顺序上它排在postProcessBeforeInitialization的最前头。PostConstruct和InitializingBean、init-method到底先执行哪个这个顺序是硬性的面试也爱问PostConstruct先执行然后afterPropertiesSet最后init-method。源码逻辑其实很简单Spring在AbstractAutowireCapableBeanFactory里拿到所有初始化回调后会统一封装成BeanInitializationException的逐个执行PostConstruct由CommonAnnotationBeanPostProcessor调用afterPropertiesSet由InitializingBean接口驱动init-method则由BeanDefinition里配置的方法名驱动。最后是postProcessAfterInitialization()AOP动态代理就发生在这里。AbstractAutoProxyCreator默认实现是AnnotationAwareAspectJAutoProxyCreator在这个方法里根据当前Bean是否匹配切点表达式决定创建代理。所以代理对象是Bean初始化完成之后才包装出来的代理内部再调用目标方法时生命周期已经走完了。2.4 使用与销毁阶段初始化结束Bean正式放进单例池singletonObjects之后所有地方getBean()拿到的都是这个已就绪的对象。单例Bean被容器持有引用一直活到容器关闭。销毁阶段也不是简单的一句GC回收Spring会依次执行三类回调PreDestroy注解方法、DisposableBean.destroy()接口方法、Bean(destroyMethod xxx)或XML配置的destroy-method。执行顺序和初始化阶段正好对应注解优先接口其次配置方法最后。销毁逻辑由DisposableBeanAdapter统一调度这个适配器会把三者包装成统一的销毁流程。这里有个很关键的细节容器销毁时只会对单例Bean执行销毁回调prototypeBean容器不管销毁。还有destroyMethod默认值在Bean注解里有个坑如果你用Bean注册了一个外部类对象类里恰好有个无参的且名为close或shutdown的public方法Spring默认会把它识别成destroyMethod容器关闭时就给你调了。这个行为Spring 5.0之后可以通过Bean(destroyMethod )显式关闭。3. 用代码把整个生命周期打点记录3.1 搭建验证环境光看理论还不够我建议你自己跑一遍代码把日志打出来看顺序。不用依赖SpringBoot直接引入spring-context就行。下面是我常用的验证工程。创建一个普通的Bean让它实现各种接口、加上各种注解方法Component public class LifeCycleBean implements InitializingBean, DisposableBean, BeanNameAware, BeanFactoryAware { private String name; public LifeCycleBean() { System.out.println(1. 构造器执行); } Autowired public void setName(String name) { System.out.println(2. 属性填充setName 被调用); this.name name; } PostConstruct public void postConstruct() { System.out.println(3. PostConstruct 执行); } Override public void afterPropertiesSet() throws Exception { System.out.println(4. InitializingBean.afterPropertiesSet 执行); } public void customInit() { System.out.println(5. init-method 执行); } Override public void setBeanName(String name) { System.out.println(Aware: BeanNameAware 执行beanName name); } Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println(Aware: BeanFactoryAware 执行); } PreDestroy public void preDestroy() { System.out.println(6. PreDestroy 执行); } Override public void destroy() throws Exception { System.out.println(7. DisposableBean.destroy 执行); } public void customDestroy() { System.out.println(8. destroy-method 执行); } }再写一个全局的BeanPostProcessor专门打印已经执行到哪一步另外再写一个InstantiationAwareBeanPostProcessor把更细的拦截点也打出来Component public class MyBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifeCycleBean) { System.out.println(BeanPostProcessor.beforeInitialization 执行); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifeCycleBean) { System.out.println(BeanPostProcessor.afterInitialization 执行); } return bean; } }让配置类注册Bean并指定自定义的initMethod和destroyMethodConfiguration public class AppConfig { Bean(initMethod customInit, destroyMethod customDestroy) public LifeCycleBean lifeCycleBean() { return new LifeCycleBean(); } }最后写个入口类启动容器后主动关闭看完整输出public class LifeCycleApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); System.out.println( 容器启动完成Bean 可以使用 ); context.close(); System.out.println( 容器已关闭 ); } }3.2 完整日志输出与分析启动后日志顺序大致是这样的1. 构造器执行 2. 属性填充setName 被调用 Aware: BeanNameAware 执行beanNamelifeCycleBean Aware: BeanFactoryAware 执行 BeanPostProcessor.beforeInitialization 执行 3. PostConstruct 执行 4. InitializingBean.afterPropertiesSet 执行 5. init-method 执行 BeanPostProcessor.afterInitialization 执行 容器启动完成Bean 可以使用 6. PreDestroy 执行 7. DisposableBean.destroy 执行 8. destroy-method 执行 容器已关闭 对照这个输出你会发现几个值得注意的点构造器之后紧跟着属性填充然后才是各种Aware回调这跟前面讲的一致。PostConstruct排在afterPropertiesSet和init-method前面所有BeanPostProcessor的前置处理都在自己的初始化逻辑之前。像setName这种Autowired注入的时机其实发生在populateBean阶段但因为走的是setter注入它的执行顺序完全取决于AutowiredAnnotationBeanPostProcessor遍历处理属性的过程所以日志里setName排在构造器后、Aware回调前这就是属性填充阶段。还有一个细节BeanPostProcessor.afterInitialization执行完后整个Bean就进入可用状态了。你可能会问Autowired的字段、Value的值都是什么时候真正写进去的答案是在populateBean阶段就已经处理完代理包装也只是对目标对象做了个包装层不会重新注入一遍。3.3 Aware接口与代理乱入时的顺序变化上面的代码顺序是最标准的场景。如果某个Bean被AOP代理了你再看日志会发现afterInitialization阶段输出的beanName可能带着类似$代理类$的类名而不是原始目标类。这就是postProcessAfterInitialization里创建代理的结果。注意如果你在Before增强里访问Autowired字段拿到的是代理对象的字段而目标对象的字段是在属性填充阶段就设置好的代理对象只是引用目标对象而已这个理解挺重要。另外ApplicationContextAware的处理顺序不在BeanFactoryAware后面它由ApplicationContextAwareProcessor负责而ApplicationContextAwareProcessor本身是一个BeanPostProcessor它的postProcessBeforeInitialization里会先处理EnvironmentAware、EmbeddedValueResolverAware、ResourceLoaderAware、ApplicationEventPublisherAware、MessageSourceAware、ApplicationContextAware所以如果你同时实现了BeanNameAware和ApplicationContextAware后者实际是在postProcessBeforeInitialization阶段才被回调的比BeanNameAware晚不少。想验证的话可以在自定义Bean里实现ApplicationContextAware看看日志输出位置就知道了。4. 高级场景循环依赖、代理与容器的真实行为4.1 三级缓存与提前暴露生命周期走到属性填充时如果A依赖B、B又依赖A会出现一个经典问题创建A时发现需要注入B于是去创建BB创建时又发现需要注入A此时A还没创建完。Spring单例Bean解决这个问题的办法是提前暴露一个半成品引用这个机制通常称为三级缓存。三级缓存对应三个Map第一级singletonObjects保存完整Bean第二级earlySingletonObjects保存早期暴露的Bean注意这个Bean可能还是原始对象没经过初始化第三级singletonFactories保存的是ObjectFactory真正执行的是getEarlyBeanReference方法。生命周期在这个场景下的实际表现是A的构造器执行完addSingletonFactory把三级缓存的ObjectFactory注册进去B创建时依赖A走到getSingleton会依次查一级、二级、三级缓存三级缓存的ObjectFactory被调用返回A的早期引用默认是原始对象如果A需要代理会在getEarlyBeanReference提前创建代理。B拿到A的引用后完成填充A再继续走初始化。很多人问二级缓存能不能省掉设计上三级缓存里的ObjectFactory除了返回早期引用还承担了让AOP代理提前介入的责任。如果直接存原始对象到二级缓存那对于先出现循环依赖再发生AOP代理的场景代理的生成时机就会乱可能所有人拿到的都不一样。三级缓存把决策延迟到真正需要的时刻确保同一个Bean的早期引用和最终引用一致。4.2 代理对象介入后的生命周期变化代理跟生命周期的关系也是高频考点。正常AOP代理生成发生在postProcessAfterInitialization也就是初始化的最后一步。但如果Bean存在循环依赖代理就可能在三级缓存getEarlyBeanReference时提前生成。这意味着你明明在afterPropertiesSet里改了自己的字段最后暴露给外部的却是个代理对象外部调用方法走的是代理逻辑。这个提前生成代理的行为由SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference触发。AbstractAutoProxyCreator重写了这个方法保证循环依赖场景下提前暴露的Bean也是代理对象。没有循环依赖时它就不会走这一步等到postProcessAfterInitialization才创建代理。所以同一种AOP配置下有循环依赖和没有循环依赖代理生成时机完全不同排查某些切面生效而某些没生效的问题时要注意。如果你在PostConstruct里打印this.getClass()发现类名带了CGLIB或者JDK Proxy字样不要奇怪那只说明某个后处理器提前把它代理了。SpringBoot AspectJ的代理默认是CGLIB从SpringBoot 2.x开始proxyTargetClass默认就是true。4.3 prototype与单例的关键差异prototype作用域的Bean生命周期跟单例差别很大这是容易用错的地方。官方的说法是prototypeBean在每次获取时都会创建新实例容器只负责创建和初始化不负责销毁销毁回调不会自动执行。具体的表现是PreDestroy、DisposableBean.destroy()、destroy-method对prototypeBean都不生效。如果你想在原型Bean销毁时做清理只能自己在业务代码里调用销毁逻辑。之前见过有人用prototypeBean管理数据库连接以为容器关闭会帮他释放结果连接一直没关硬生生引出连接池泄漏问题。另外注意prototypeBean里注入的单例Bean是正常的但反过来单例Bean注入prototypeBean时注入进去的是同一个实例不会每次获取都是新的。这就要用到Lookup方法注入或ObjectProviderT来绕过这个限制。本质上这已经不算生命周期问题而是作用域嵌套的问题但排查时经常被误判成Bean被缓存了。5. 常见问题排查与避坑指南5.1 典型故障速查表现象可能原因排查方向PostConstruct没执行方法不是public、类被代理时用了错误的字节码生成方式、方法在父类但父类没被扫描确认方法签名、断点查看调用栈重点看CommonAnnotationBeanPostProcessor是否注册构造器里Autowired字段是null构造器执行早于属性填充改用构造器注入或把初始化动作挪到PostConstructBeanPostProcessor对某个Bean没生效该Bean不是Spring管理的被postProcessBeforeInstantiation提前拦截该后处理器注册顺序靠后检查Bean来源确认是Component或Bean注册init-method方法名配错BeanDefinition中指定的方法不存在容器启动时直接报NoSuchMethodException看启动日志单例Bean销毁回调不执行容器没正常closeBean是prototype作用域确认AnnotationConfigApplicationContext.close()被调用排除System.exit直接结束进程Value注入失败属性填充阶段StringValueResolver未生效或配置类没开启ConfigurationProperties检查Environment里是否有对应key看AutowiredAnnotationBeanPostProcessor处理时是否有异常循环依赖报BeanCurrentlyInCreationException存在构造器循环依赖或prototype循环引用构造器循环无解需要改设计字段循环默认单例可用5.2 我踩过的几个坑和心得第一个坑是PostConstruct在继承体系下的失效问题。有一个公共父类里面写了PostConstruct初始化方法子类被Spring扫描注册后父类方法确实会被调用但如果这个方法在父类里不是public或者子类重写了同名方法但没加注解行为就变得很怪。后来养成了习惯PostConstruct方法一律声明为public void且不加参数。多个类层次里Spring会按继承顺序依次调用各级父类的PostConstruct执行顺序是父类先于子类。这个细节文档里写得少但实际遇到过一次之后印象就很深。第二个坑是afterPropertiesSet抛异常时的处理。如果你的初始化逻辑里依赖了外部RPC或者数据库一旦抛出异常这个Bean就会被标记为创建失败。但注意已经执行过的PostConstruct不会回滚外部副作用不会撤销。我见过有人在PostConstruct里发了MQ消息结果后续afterPropertiesSet报错消息已经发出去又得手工补发。后面凡是这种先发通知再初始化的逻辑我都改成放到ApplicationReadyEvent事件里处理等所有Bean都创建完再发。第三个坑更隐蔽自定义BeanPostProcessor处理了某个Bean后afterInitialization返回的对象会被直接注册进单例池。如果你不小心返回了一个新的对象所有对这个Bean的注入都会变成新对象而原始对象后续可能就没人管了。写过一次框架代码踩过这个坑查了很久才发现是返回的对象引用不一致。记住一条铁律postProcessBeforeInitialization和postProcessAfterInitialization没有特殊需求时必须返回原Bean引用别没事瞎换对象。再说个小技巧排查生命周期相关问题最有效的手段不是看文档而是全局加日志。按经验我常写一个打点Bean直接放在BeanFactoryPostProcessor后面把BeanDefinition注册时的方法名、initMethodName、destroyMethodName都打出来。很多所谓生命周期诡异问题其实就是某个Bean的initMethod被意外设置成了父类方法或Lambda方法名日志一眼就能看出来。如果你碰到的场景更复杂比如想拦截Bean创建前的BeanDefinition注册或者想对某类Bean统一做字段加密BeanDefinitionRegistryPostProcessor和InstantiationAwareBeanPostProcessor组合使用基本能覆盖90%的定制需求。我第一次系统搞懂生命周期是靠着反复读AbstractAutowireCapableBeanFactory里createBean的源码配合上面这种日志工程边跑边看。把这套流程走一遍比死记硬背顺序有用得多以后再遇到Spring容器的初始化问题你就不会只停留在猜测层面了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

GitHub日榜速报:从热词反推技术趋势与项目筛选实战 2026/10/2 11:09:36

GitHub日榜速报:从热词反推技术趋势与项目筛选实战

1. 日榜速报到底在追什么:从热词反推当天榜单的骨架每天刷 GitHub Trending 的人很多,但真正把日榜当情报源来用的人不多。大部分人看日榜就是图个热闹,扫一眼仓库名,点进去看看 README,然后关掉。但如果你把日榜当成一…

阅读更多 →
Redis MCP Server 实战:用 Claude Code 操作 Redis 缓存治理 2026/10/2 11:09:23

Redis MCP Server 实战:用 Claude Code 操作 Redis 缓存治理

1. 从一条更新说起:Redis 接入 AI 到底意味着什么Redis 这个名字,做后端的人基本都绕不开。缓存、分布式锁、消息队列、排行榜、会话存储,几乎每个稍微有点规模的项目里都能看到它的身影。但最近圈子里讨论 Redis 的角度变了,不再…

阅读更多 →
社区团购小程序开发报价全解析:从功能拆解到避坑指南 2026/10/2 11:09:23

社区团购小程序开发报价全解析:从功能拆解到避坑指南

做社区团购小程序开发的这几年,我接到过不少来自杭州本地商家、社区团长、供应链老板的咨询,问题绕来绕去,最后都会落在一句话上:开发一套这样的小程序到底多少钱?这个问题的背后,往往是踩过模板坑、被低价…

阅读更多 →
结构体与方法:从数据建模到内存布局的工程实践全解析 2026/10/2 11:09:22

结构体与方法:从数据建模到内存布局的工程实践全解析

这是系列第五篇。前四篇我们依次聊了变量、控制流、函数和错误处理,现在轮到结构体与方法。说白了,结构体就是一种复杂数据类型,它能把一组互相关联的字段打包成一个整体,方法则是挂在这个整体上的行为。很多初学者在数组、map 甚…

阅读更多 →
Datawhale学习笔记:Agent记忆与RAG失效分析 2026/10/2 11:09:16

Datawhale学习笔记:Agent记忆与RAG失效分析

给 Agent 加了记忆和 RAG,它为什么还会答错?—— 失效环节拆解与优化实践 关键词:Agent、RAG、长期记忆、向量检索、幻觉、忠实度评估、混合检索、Rerank、记忆冲突消解 一句话结论:记忆和 RAG 不是「装上就灵」的保险,而是把「答错的来源」从模型参数知识转移到了整条管道…

阅读更多 →
RAG智能体全栈开发永久归档:从数据分块到Agentic RAG的选型与调优实录 2026/10/2 11:09:16

RAG智能体全栈开发永久归档:从数据分块到Agentic RAG的选型与调优实录

1. 为什么我要把 RAG 智能体全栈开发整理成一份永久归档 过去一年我几乎把市面上能跑的 RAG 智能体方案都折腾了一遍,从最朴素的“向量库加 LLM”到带图结构的 GraphRAG、本体驱动的 Ontology RAG,再到 Agentic RAG 这种让智能体自己决定检索策略的玩法。…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉