拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Bean生命周期详解:从实例化到销毁的完整流程与最佳实践

SpringBean的生命周期这个问题我从刚学Spring就被面试官问过工作几年后又因为线上启动变慢、AOP代理不生效这些怪问题回头认真把生命周期重新盘了一遍。说句大实话它绝对不只是用来应付八股问答的知识而是把IoC容器的实例化、属性填充、Aware回调、代理生成、初始化、销毁这些环节全部串起来的主线。只要把这条主线看明白了后面看Spring源码、排查启动慢、处理循环依赖、自定义Starter都会顺手很多。这篇内容适合正在啃Spring基础的同学也适合写了好几年Service却对Bean到底怎么被创建出来感觉模糊的Java工程师。我尽量把原理、代码、坑都放在一起方便你直接对照着验证。1. 为什么SpringBean的生命周期值得从头盘一遍1.1 面试题背后考察的是框架的底层整合能力很多同学背SpringBean生命周期会背出“实例化、属性填充、初始化、销毁”这几个字但面试官一旦追问“AOP代理是在哪一步生成的”“为什么构造器注入的循环依赖会报错”“PostConstruct和init-method到底谁先执行”能答上来的人就少了很多。原因很简单生命周期不是一个孤立的知识点它是整个Spring容器运行机制的承重墙。比如你在项目里用了事务注解或切面底层其实就是一个BeanPostProcessor在Bean初始化之后帮你生成了代理对象。你在PostConstruct里写初始化逻辑本质上也是容器在初始化阶段通过内置的BeanPostProcessor回调了你的方法。理解了这套机制就不再是“为了背而背”而是能把Spring的IoC、AOP、事务、事件监听、自动配置全部串到一条线上去理解。1.2 生命周期贯穿IoC、AOP、事务三层从使用者的视角来看Spring就像一个大工厂你只负责声明“我要一个什么Bean”剩下创建、装配、增强、销毁都由容器完成。但这个工厂不是黑盒子它对外提供了很多扩展点。BeanPostProcessor、BeanFactoryPostProcessor、InitializingBean、Aware接口族这些都是生命周期在不同阶段暴露出来的钩子。事务、Redis、MQ等组件的Starter本质上都是靠这些钩子把核心逻辑注入到容器里。比如Spring事务管理就是在Bean初始化之后判断是否有Transactional注解有就生成一个事务增强代理。如果你不了解生命周期遇到“事务不生效”这种问题只会查配置很难想到可能是Bean提前被代理或者循环依赖导致早期暴露造成的。所以我觉得只要还在用Spring写Java生命周期这道坎早晚都得过越早过越划算。2. 完整生命周期流程拆解Bean从创建到销毁的每一步2.1 起点BeanDefinition的注册与合并很多人以为Bean生命周期是从构造方法开始的实际上容器里第一件事不是创建对象而是准备“对象的图纸”也就是BeanDefinition。无论是Component扫描到的类还是Bean方法返回的类型容器都会把类的元信息、作用域、懒加载标记、初始化方法名、销毁方法名等打包进一个BeanDefinition对象里。这一步非常关键因为后续所有创建步骤都要参考这份图纸。比如Scope(prototype)、Lazy、DependsOn这些注解都会被读取并设置到BeanDefinition中。如果你的配置类里有父类或者抽象配置Spring还会在getMergedLocalBeanDefinition这里做BeanDefinition的合并把子定义的属性补齐。我排查过一些奇怪的Bean属性丢失问题最后定位到就是BeanDefinition合并阶段没有生效说明这一步直接决定了后面Bean长什么样。2.2 创建实例从实例化前拦截到真正new出来有了图纸之后容器开始创建Bean实例。这里有一个很容易被忽略的扩展点InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation。这个方法的返回值可以是一个代理对象如果返回了非空对象Spring会直接跳过后面的默认实例化过程连构造方法都不会走。很多框架的AOP代理、远程调用代理就是在这里提前返回自定义对象。如果这个方法没有返回对象Spring就会走构造器反射来创建实例。对于单例Bean构造器默认会选择无参构造器如果是有参构造或构造器注入则需要从容器里解析依赖参数。这个地方是循环依赖最容易爆雷的位置singleton对象还在创建中就又要去获取另一个尚未创建完成的Bean如果对方需要构造器注入那基本就无解了。2.3 属性填充依赖注入发生的地方实例创建完成之后Spring开始给这个Bean“喂水喂饭”也就是属性填充。这里处理的是Autowired、Resource、Value、XML里的property配置。Spring会通过BeanWrapper把依赖注入到Bean中。字段注入和setter注入都在这个阶段完成。属性填充不是无条件执行的它的前置条件是InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation返回true。如果这个方法返回false容器会直接跳过属性填充阶段。这在某些特殊场景下可以避免对代理对象或半成品对象做不必要的注入不过我平时用得不多但在阅读Spring源码时会看到很多内置处理器在利用这个钩子控制注入行为。2.4 初始化前Aware回调与BeanPostProcessor前置处理属性填充完成之后Bean终于有了一个相对完整的状态但还不能直接交给业务使用。这里Spring会先做Aware回调比如BeanNameAware.setBeanName传入Bean名字BeanFactoryAware.setBeanFactory传入BeanFactory让你能在Bean内部感知到容器的存在。接下来是初始化前的BeanPostProcessor.postProcessBeforeInitialization。这个阶段是Spring扩展点最密集的地方之一。AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、ApplicationContextAwareProcessor等一大票内置处理器都挂在上面。你会在这里看到PostConstruct被调用也会看到ApplicationContextAware被回调。很多人困惑“PostConstruct到底算生命周期哪一步”严格来说它是在初始化前的BeanPostProcessor里执行的并不是一个单独的初始化内核阶段。2.5 初始化InitializingBean、init-method与初始化后置处理初始化阶段本身按顺序包含三件事先执行InitializingBean.afterPropertiesSet()再执行自定义的init-method也就是XML里的init-method或Bean(initMethod...)。这里有个容易搞混的点因为PostConstruct是在前面的BeanPostProcessor里被回调的所以实际顺序其实是PostConstruct在afterPropertiesSet之前。初始化完成后是最后一个强扩展点BeanPostProcessor.postProcessAfterInitialization。AOP代理的默认生成时机就在这里。AnnotationAwareAspectJAutoProxyCreator会在这个阶段判断Bean是否匹配切面如果匹配就创建代理对象并返回。所以你在Spring里拿到的很多Bean其实已经不是原始实例而是经过这个阶段包装后的代理对象。如果没有循环依赖这个时机基本是固定的。2.6 销毁PreDestroy、DisposableBean和destroy-method单例Bean在容器关闭的时候会执行销毁逻辑。这一段相对简单但顺序也有讲究先回调PreDestroy注解方法再执行DisposableBean.destroy()最后执行自定义的destroy-method。和初始化阶段一样PreDestroy也是CommonAnnotationBeanPostProcessor在销毁阶段通过BeanPostProcessor回调触发的。这里需要注意只有容器管理的单例Bean会保证执行销毁流程。原型Bean在交付给你之后容器就不再跟踪它了销毁方法默认不会被调用。这也是“原型Bean只有生命周期前半段”这句话的由来。如果业务里确实需要释放原型Bean的资源要么自己手动拿到DisposableBean适配器来调用要么换一种Scope设计否则很容易出现资源泄漏。2.7 初始化与销毁顺序速查表阶段触发机制执行顺序构造实例构造器反射无依赖时先执行无参构造器属性填充BeanWrapper注入Autowired、Resource、ValueAware回调BeanNameAware等在属性填充后、初始化前初始化前BeanPostProcessor.before内置BPP优先PostConstruct在此阶段初始化InitializingBeanafterPropertiesSet初始化方法init-methodBean(initMethod)配置初始化后BeanPostProcessor.afterAOP代理默认在此生成销毁前PreDestroy在destroy方法之前销毁DisposableBeandestroy()销毁方法destroy-methodBean(destroyMethod)配置3. 实操写个Demo把生命周期完整打印出来3.1 工程准备与依赖选择为了直观看到每个生命周期阶段我建议直接建一个Spring Boot工程不用引入额外的Web组件只需spring-boot-starter基础依赖就行。我用的是Spring Boot 3.x因此PostConstruct和PreDestroy需要从jakarta.annotation包导入如果你还在用Spring Boot 2.x用javax.annotation即可。这个差异在实际项目中经常遇到尤其是老项目升级SpringBoot3的时候包名一变很多人找不到注解在哪。创建一个普通Java类起名LifecycleLogBean让它实现BeanNameAware、BeanFactoryAware、InitializingBean、DisposableBean这几个接口然后加上PostConstruct、PreDestroy、自定义initMethod和destroyMethod再把每个阶段打印出来。为了验证属性填充我加了一个setName方法并用Value注入。这样你运行起来后日志会把每个环节的顺序完完整整地列出来。3.2 实现可打印生命周期的生命周期Beanpublic class LifecycleLogBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { private String name; public LifecycleLogBean() { System.out.println(1. 构造方法执行); } Value(${demo.name:default}) public void setName(String name) { this.name name; System.out.println(2. 属性填充 setName( name )); } Override public void setBeanName(String name) { System.out.println(3. BeanNameAware.setBeanName - name); } Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println(4. BeanFactoryAware.setBeanFactory); } PostConstruct public void postConstruct() { System.out.println(5. PostConstruct); } Override public void afterPropertiesSet() throws Exception { System.out.println(6. InitializingBean.afterPropertiesSet); } public void initMethod() { System.out.println(7. init-method); } PreDestroy public void preDestroy() { System.out.println(9. PreDestroy); } Override public void destroy() throws Exception { System.out.println(10. DisposableBean.destroy); } public void destroyMethod() { System.out.println(11. destroy-method); } }配置类里用Bean注册这个Bean并在注解上指定initMethod和destroyMethodConfiguration public class LifecycleConfig { Bean(initMethod initMethod, destroyMethod destroyMethod) public LifecycleLogBean lifecycleLogBean() { return new LifecycleLogBean(); } }3.3 自定义BeanPostProcessor输出前后置日志只是Bean自己打印还不够我还加了一个自定义的BeanPostProcessor专门针对LifecycleLogBean打印初始化前后的日志Component public class LifecycleBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifecycleLogBean) { System.out.println(5.5 BeanPostProcessor.postProcessBeforeInitialization); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof LifecycleLogBean) { System.out.println(8. BeanPostProcessor.postProcessAfterInitialization); } return bean; } }运行后我的控制台输出顺序如下1. 构造方法执行 2. 属性填充 setName(default) 3. BeanNameAware.setBeanName - lifecycleLogBean 4. BeanFactoryAware.setBeanFactory 5. PostConstruct 5.5 BeanPostProcessor.postProcessBeforeInitialization 6. InitializingBean.afterPropertiesSet 7. init-method 8. BeanPostProcessor.postProcessAfterInitialization ... 9. PreDestroy 10. DisposableBean.destroy 11. destroy-method需要注意的是第5和第5.5的顺序并不完全固定取决于容器里BeanPostProcessor的注册顺序。Spring内置的CommonAnnotationBeanPostProcessor通常在用户自定义BPP之前注册所以PostConstruct会先打印。如果在项目里把自定义BPP的注册顺序调整到内置之前输出顺序就会反过来。这也是生命周期流程里容易产生幻觉的地方建议对照源码去理解而不是死记某一个输出序列。4. 核心扩展点拆解每个钩子背后的设计意图4.1 BeanPostProcessor扩展点之王BeanPostProcessor是Spring生命周期里最强大的扩展点它就像给每个Bean在初始化前后都装上了拦截器。你可以在postProcessBeforeInitialization里修改Bean的属性也可以在postProcessAfterInitialization里替换整个Bean对象。AOP代理、注解驱动注入、ApplicationContext感知等功能本质上都是靠不同类型的BeanPostProcessor实现的。有一个容易踩的坑BeanPostProcessor自身也是由Spring容器管理的Bean而且它必须在其他普通Bean创建之前被实例化并注册否则无法拦截后续创建过程。所以在项目里如果看到自定义BeanPostProcessor里的依赖注入一直不生效或者启动顺序异常可以优先检查是不是这个处理器被创建得太晚或者构造器里用了需要代理的Bean。记住BeanPostProcessor不是普通业务Bean创建它的时候要尽量轻量不要在它的构造器里做太多依赖操作。4.2 Aware接口族让Bean主动感知容器Aware接口是Spring提供给Bean“认识容器”的一扇门。常见的有BeanNameAware、BeanFactoryAware、ApplicationContextAware、EnvironmentAware、ResourceLoaderAware等。它们的执行时机基本都在属性填充之后、初始化之前但具体实现方式略有区别。比如BeanNameAware和BeanFactoryAware是容器在AbstractAutowireCapableBeanFactory.invokeAwareMethods里直接回调的而ApplicationContextAware则是通过ApplicationContextAwareProcessor这个BeanPostProcessor在postProcessBeforeInitialization里被调用。这导致一个有意思的现象如果容器是纯粹的BeanFactory而不是ApplicationContextApplicationContextAware不会生效因为缺少对应的处理器。理解了这个差异以后遇到“为什么Bean拿不到ApplicationContext”这类问题就知道是容器类型或者处理器没注册导致的。4.3 三种初始化方式怎么选PostConstruct、InitializingBean、init-method这三种初始化方式功能上重叠但细节不同初始化方式执行依据适用场景PostConstructJSR250规范注解驱动业务代码中最为常见直观InitializingBeanSpring接口重写afterPropertiesSet框架内部、组件封装时使用init-methodXML或Bean(initMethod)配置不想让业务类实现Spring接口或者配置来自外部我的习惯是业务项目里优先用PostConstruct因为它语义清晰而且不用实现Spring接口和对象自身逻辑解耦。如果是自己封装Starter或者写框架组件会考虑InitializingBean因为组件内部不适合依赖注解扫描接口更直接。init-method主要用在配置类、老XML配置或第三方Bean不希望代码侵入时。销毁方法的选择逻辑也类似优先PreDestroy其次destroy-methodDisposableBean只在特殊场景下使用。5. 高频问题与排查经验5.1 循环依赖为什么能成立三级缓存干了什么Spring Bean生命周期中最经典的问题就是循环依赖。A依赖BB依赖A对于单例且使用的是setter注入或字段注入Spring可以通过三级缓存让循环依赖成立。三级缓存分别是singletonObjects成品缓存、earlySingletonObjects提前暴露的半成品缓存、singletonFactories单例工厂缓存。流程大致是这样的A创建后在属性填充阶段发现自己需要B于是去创建BB创建时又会依赖A此时容器发现A还没完成初始化但已经有一个singletonFactory放在三级缓存里于是通过这个工厂拿到A的早期引用把早期引用注入给BB创建完成后A再把B注入进来最后完成A的初始化。早期引用本质上改变了生命周期中的暴露时机本来A要到init之后才完全可用但因为循环依赖A在属性填充阶段就被提前暴露了。这里最大的坑是构造器循环依赖。因为构造器执行发生在属性填充之前A在构造时就需要B但A此时还没有实例被放入三级缓存Spring根本没有办法提前暴露所以构造器循环依赖会直接报错。解决办法一般是把构造器注入改成setter注入或者给其中一个依赖加Lazy延迟获取。实际排查时先看异常信息和日志明确是构造器循环依赖还是setter循环依赖再决定怎么改。5.2 AOP代理对象到底在哪一步生成正常情况下Spring AOP的代理对象是在Bean初始化之后通过AbstractAutoProxyCreator这个BeanPostProcessor的postProcessAfterInitialization生成的。也就是说你的Transactional、Async切面默认都是在Bean已经完成属性填充和初始化之后才包装上一层代理。但存在一个例外如果Bean参与了循环依赖那么代理可能会提前生成。因为三级缓存中的singletonFactory里保存的是getEarlyBeanReference方法这个早期引用查询会给SmartInstantiationAwareBeanPostProcessor一个机会在Bean还没有完成初始化的阶段就生成代理对象。这么设计是为了保证循环依赖中的多个Bean拿到的是同一个代理而不是原始对象和代理对象互相交错。我调过一个AOP不生效的问题最后发现是用户自己写了个InstantiationAwareBeanPostProcessor在postProcessBeforeInstantiation里直接返回了原始对象绕过了正常代理流程。如果你碰到类似“明明有切面但Bean方法没有被增强”的情况可以先检查是不是有自定义处理器提前拦截了实例化过程。整体来说理解AOP代理生成的默认时机和例外情况比死记“代理在初始化后生成”更有用。5.3 原型Bean的生命周期并不完整默认的Scope(singleton)下单例Bean从创建到销毁都由容器管理。原型Bean则不同每次getBean都会创建新实例完整的创建流程依然会走实例化、属性填充、Aware、初始化前、初始化、初始化后都会执行但容器不会保存这个Bean所以销毁阶段不会自动触发。这就意味着原型Bean用完后如果你不去处理资源释放是有泄漏风险的。我有一次排查数据库连接池连接数持续增长最后定位到某个原型Bean在初始化时创建了一个文件句柄但销毁方法从来没执行过句柄一直没释放。解决办法要么在业务调用处手动调用清理方法要么把这种需要资源管理的对象设计成单例不要图方便全部用原型。如果你确实希望原型Bean生命周期也能被容器接管可以考虑自定义Scope在get和remove阶段做更多控制但成本不低一般不建议轻易引入。5.4 启动变慢与Bean初始化顺序问题的排查思路启动变慢经常和Bean生命周期里的初始化逻辑直接相关。尤其是单例Bean默认预实例化Spring容器启动时会把所有非懒加载的单例Bean都创建并初始化一遍。如果里面有大量初始化网络连接、预热缓存、批量查询数据库的操作启动时间就会肉眼可见地变长。排查时可以用Lazy把不需要启动时初始化的Bean懒加载也可以用DependsOn显式指定Bean的创建顺序。遇到更复杂的情况建议打开Spring的debug日志或者用Actuator的Beans端点查看Bean创建顺序。平时开发时我也习惯在启动类上临时加一个ApplicationRunner把容器里关键Bean的状态打印出来确认再判断是Bean初始化顺序不对还是某个初始化逻辑阻塞了主线程。6. Spring Boot和现代Spring中的生命周期变化6.1 自动配置、条件注解与生命周期入口Spring Boot出现后Bean生命周期本身并没有改变但BeanDefinition的注册方式变得更“智能”了。自动配置类里大量使用ConditionalOnClass、ConditionalOnMissingBean等条件注解决定哪些BeanDefinition会被注册。这些判断发生在BeanDefinition加载阶段早于生命周期创建阶段。所以你会看到很多自动配置虽然写了一大堆Bean方法但最终容器里只创建了满足条件的Bean。这种设计对生命周期的影响主要体现在“启动顺序”和“Bean覆盖”上一个自动配置Bean可能在业务Bean之前被创建也可能被用户自定义的同类型Bean覆盖。排查时先看条件注解是否命中再看是否存在同名Bean导致的覆盖不要一上来就怀疑生命周期顺序。6.2 SmartLifecycle和ApplicationRunner的调用边界Spring容器在refresh完成后会调用SmartLifecycle的start方法。这里看起来和Bean生命周期有点交叉但实际上是独立的容器生命周期阶段。普通Bean的初始化发生在finishBeanFactoryInitialization阶段SmartLifecycle.start则是在finishRefresh阶段。因此如果需要在容器完全启动后再执行某些任务请用ApplicationRunner、CommandLineRunner或SmartLifecycle而不是在一个普通Bean的PostConstruct里去做。在PostConstruct里启动线程或连接外部服务如果遇到上下文还没完全刷新、依赖Bean尚未全部就绪很容易出现各种“神奇”的空指针。这个我踩过好多次后来统一规范业务初始化放在ApplicationRunner里组件内部资源准备放在PostConstruct但尽量不依赖其他业务Bean。6.3 工程落地建议生命周期知识怎么用到实际项目理解生命周期后我觉得最有价值的落地方式是检查项目里的初始化逻辑是否放在了正确的位置。不要急着把所有逻辑堆到构造器里也不要让业务初始化依赖“恰好某个Bean先创建”。我一般在团队里定的规范是构造器里只做必填参数校验和基础赋值PostConstruct里做需要依赖注入完成的初始化需要上下文完全就绪后执行的任务放到ApplicationRunner需要感知容器信息的Bean用Aware接口但避免在Aware回调里做重操作不在BeanPostProcessor里创建业务Bean以免引入额外的依赖顺序。这些规范看起来很普通但在实际项目中能省掉很多莫名其妙的启动问题。特别是团队大了以后大家对生命周期理解深度不一致代码里很容易出现“时间上碰巧能跑通”的隐性依赖一旦调整启动顺序或者扩容就会瞬间暴露。我个人的经验是SpringBean生命周期不能只当成面试题去背最好自己动手写一个带日志的Demo在工程里跑一遍再把常用的AOP、事务、事件监听插进去观察顺序变化。踩过几次坑之后你会发现很多看源码时觉得抽象的概念真正落到日志里就变得非常直观了。最后再分享一个排查技巧遇到Bean异常时先打开调试日志观察Bean创建到哪一步失败的是构造器、属性填充还是初始化前处理器基本就能快速缩小问题范围。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门