资讯动态

Spring Bean生命周期详解:从实例化到销毁的完整链路

发布时间:2026/10/9 9:07:14 来源:尧图企业网站定制
面试官Spring Bean 生命周期详解面试被问到Spring Bean生命周期很多人第一反应是背那套“实例化→属性填充→初始化→销毁”四段论然后面试官追问一句“BeanPostProcessor是在哪一步介入的”就卡壳了。再追问“构造方法抛异常了Bean还会走初始化吗”“原型Bean的生命周期和单例有什么本质区别”基本就只剩尴尬了。这篇文章我结合多年面试候选人和被面试的经验把Bean生命周期从头到尾拆一遍不光讲标准流程还把常被追问的机制原理比如三级缓存、循环依赖、代理对象生成时机一并说清楚。文章面向无论是刚学Spring的初学者还是准备跳槽的初中级Java工程师都能拿走一套能直接用的答题框架。1. 先建立整体认知生命周期不是一条线是两个阶段1.1 不要背反了真正被管理的只有“完整链路”很多人理解Bean生命周期是站在“对象”的视角去看new出来、塞属性、初始化、用完销毁。这个理解没有错但不够准确。Spring里说的Bean生命周期严格来说是从BeanDefinition被解析注册开始到Bean实例被创建、强化、初始化最后被销毁回收的整个过程。它不是一个单纯的对象存活过程而是包含了“定义”和“使用”两大阶段。我习惯把Bean生命周期拆成两条线BeanDefinition阶段Spring读取配置XML、注解、JavaConfig把每一个bean或Bean包装成一个BeanDefinition对象注册到BeanFactory。这一步很多人忽略但它决定了后面实例化时的“图纸”长什么样。Bean实例阶段从getBean()触发开始实例化、填充属性、执行Aware回调、经过BeanPostProcessor处理、执行初始化方法最后放入单例池或返回原型Bean。面试时如果你能主动把生命周期拆成这两个阶段面试官会立刻觉得你不是在背八股而是真的理解了Spring的设计意图。1.2 网上流传的“生命周期流程图”为什么总是对不上网上的生命周期图五花八门有的把BeanNameAware放在属性填充前面有的把InitializingBean放在init-method后面有的干脆画了一大堆箭头。原因是Spring的版本一直在演进从2.x到5.xBeanPostProcessor的注册时机、注解驱动后的AutowiredAnnotationBeanPostProcessor介入方式都有差异但没有本质性的流程变化。我的建议是不要试图背某一幅图而是背下面这个经过验证的标准调用顺序BeanDefinition加载与解析 ↓ 实例化前InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation ↓ 构造方法实例化推测构造方法处理循环依赖时提前暴露早期引用 ↓ 实例化后MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition ↓ 属性填充populateBean处理Autowired、Resource、Value ↓ Aware回调BeanNameAware → BeanClassLoaderAware → BeanFactoryAware ↓ BeanPostProcessor.postProcessBeforeInitialization ↓ InitializingBean.afterPropertiesSet ↓ 自定义init-method ↓ BeanPostProcessor.postProcessAfterInitializationAOP代理在此生成 ↓ Bean就绪放入单例池 ↓ 容器关闭时PreDestroy → DisposableBean.destroy → 自定义destroy-method这个链路就是核心主线。下文我会把每个节点掰开揉碎讲清楚特别是那些容易被忽略但面试官一定追问的点。2. 核心节点逐个拆解每个步骤背后的设计意图2.1 实例化不等于“new一个对象”构造方法的选择逻辑很多人以为Bean实例化就是用反射newInstance()其实里面大有文章。Spring在实例化一个Bean时会根据BeanDefinition里记录的构造信息来决定创建方式优先级大体是如果指定了constructor-arg参数或ConstructorProperties走对应的构造方法。如果没有指定且类只有一个构造方法直接用这个构造方法。如果有多个构造方法Spring会进行构造方法推测constructor inference根据容器内可用Bean的类型和数量选一个“最适合”的构造方法。这里有个高频面试点循环依赖时Spring为什么只对“单例、允许提前暴露”的Bean做三级缓存处理构造方法注入的循环依赖为什么解决不了答案在于Spring通过三级缓存放的是“早期实例化的引用”但这个引用必须是从构造方法返回之后才有的。如果是构造方法注入A的构造方法需要BB的构造方法又需要A两边都在构造方法阶段谁都没法提前暴露那就死锁了。踩坑提示如果你面试时说“Spring可以解决所有循环依赖”一定会被扣分。正确的说法是Spring通过三级缓存解决的是“单例Bean 属性注入setter/字段注入”场景下的循环依赖构造方法注入的循环依赖目前无法解决除非用Lazy延迟代理。2.2 属性填充populateBean不是简单setter属性填充阶段Spring会遍历Bean的属性元信息把所有需要注入的属性逐个处理。这个阶段主要干三件事根据属性名/类型进行自动装配byName/byType以及注解驱动的Autowired等。处理Value占位符和SpEL表达式把配置值注入进去。处理Resource、Inject等JSR标准注解。这个阶段的幕后主力是InstantiationAwareBeanPostProcessor的postProcessProperties()方法比如AutowiredAnnotationBeanPostProcessor就是在这里把Autowired标记的字段或方法注入的。一个容易被忽视的细节populateBean之前有个postProcessAfterInstantiation方法如果它返回falseSpring会直接跳过整个属性填充阶段。这个钩子在实际项目中很少用但理解它的位置有助于回答“我想拦截Bean属性设置怎么办”这类问题。2.3 Aware回调让Bean感知容器Aware是一组标记接口Spring在属性填充完成后会依次回调。常见的Aware接口包括Aware接口回调内容典型用途BeanNameAware注入Bean在容器中的名字获取当前Bean的idBeanClassLoaderAware注入加载当前Bean的ClassLoader做类加载操作BeanFactoryAware注入当前BeanFactory手动获取其他BeanApplicationContextAware注入ApplicationContext获取环境、发布事件等注意一个细节ApplicationContextAware是由ApplicationContextAwareProcessor这个特殊的BeanPostProcessor回调的它会在所有普通Bean的postProcessBeforeInitialization阶段被触发。所以实际时序里如果你同时实现了BeanFactoryAware和ApplicationContextAware前者的回调在populateBean之后立刻执行而后者要等到第一个BeanPostProcessor处理时才触发。面试时提到Aware时顺带说一句“这组回调的核心是让Bean能拿到容器的运行时信息但副作用是让Bean和Spring容器耦合了现代写法更推荐通过Autowired或ObjectProvider来获取”会让面试官觉得你有架构意识。2.4 BeanPostProcessor整个生命周期里最强大的扩展点BeanPostProcessor是整个生命周期里最强大、最常考、也最容易被绕晕的环节。它有两个方法Object postProcessBeforeInitialization(Object bean, String beanName) Object postProcessAfterInitialization(Object bean, String beanName)这两个方法一个在afterPropertiesSet之前调用一个在init-method之后调用。注意参数里的返回值是Object意味着你可以在任意一步把传入的Bean替换成另一个对象比如代理对象Spring后面用的就是你的返回值。几个关键点AOP代理就是在postProcessAfterInitialization阶段生成的。AbstractAutoProxyCreatorAnnotationAwareAspectJAutoProxyCreator的父类在这里检查Bean是否需要被代理如果需要就返回代理对象。postProcessBeforeInitialization里的典型应用是ApplicationContextAwareProcessor和InitDestroyAnnotationBeanPostProcessor负责处理PostConstruct和PreDestroy。BeanPostProcessor本身也是Bean但它们必须在其他普通Bean实例化前被提前实例化并注册所以Spring在refresh()容器时会先主动调用registerBeanPostProcessors这也是为什么你自定义的BeanPostProcessor不能依赖其他普通Bean的原因。我在面试中常问的一个变形题是“如果把BeanPostProcessor的调用顺序搞反了会发生什么”比如把postProcessAfterInitialization里做代理然后在postProcessBeforeInitialization里提前使用代理对象会导致目标方法没有被增强。回答这个问题能验证候选人是否理解了执行顺序的不可逆性。2.5 初始化两大金刚InitializingBean和init-method初始化阶段有两个入口InitializingBean.afterPropertiesSet()Bean实现了该接口后被回调。自定义init-methodXML中init-method属性、Bean(initMethod init)注解指定的方法。执行顺序是先afterPropertiesSet再init-method。Spring官方文档给的推荐是尽量不用接口而是用PostConstruct或init-method因为接口方式会让业务代码侵入Spring API。这里有个容易混淆的点PostConstruct到底在哪个阶段执行它并不在InitializingBean里面而是由CommonAnnotationBeanPostProcessor属于InitDestroyAnnotationBeanPostProcessor在postProcessBeforeInitialization阶段反射调用。所以实际顺序是PostConstruct → InitializingBean.afterPropertiesSet → 自定义init-method我实测验证过多版本Spring这个顺序在Spring 4.x和Spring 5.x包括Spring Boot 2.x/3.x里保持一致。3. 三级缓存与循环依赖生命周期里最烧脑的设计3.1 三级缓存分别存了什么三级缓存DefaultSingletonBeanRegistry是理解Spring单例Bean生命周期的关键。三级缓存对应三个Map// 一级缓存存放创建完成的成品Bean MapString, Object singletonObjects // 二级缓存存放提前暴露的早期单例Bean还没完成属性填充 MapString, Object earlySingletonObjects // 三级缓存存放ObjectFactory用于生成早期Bean的代理对象 MapString, ObjectFactory? singletonFactories为什么要三级缓存而不是两级这个问题的标准回答是三级缓存是为了解决“A循环依赖B而A还需要被AOP代理”的情况。具体推演一下场景A依赖BB依赖A同时A需要被AOP增强。A实例化后被放入三级缓存此时是一个原始的A对象未填充属性、未代理。A填充属性时发现依赖B于是去创建B。B实例化后填充属性时发现依赖A于是从三级缓存中拿到A的ObjectFactory调用getObject()。如果A需要代理ObjectFactory在这一步会生成A的代理对象提前代理如果不需要代理就返回原始A对象。B拿到A的引用可能是代理继续完成自己的创建最终放入一级缓存。A从B的创建过程中返回继续完成自己的创建。如果把三级缓存换成两级直接存早期对象那A在被B引用时拿到的就是“原始未代理”的对象后续A即使被代理了B里面持有的也还是原始对象代理逻辑就失效了。三级缓存里的ObjectFactory把“是否代理”的决定延迟到真正被引用的那一刻这是关键设计。3.2 面试时如何回答“三级缓存”问题我建议用“是什么→为什么→怎么做”的结构先讲三级缓存分别是什么。再讲为什么需要三级缓存解决循环依赖 保证代理对象的一致性。最后用一个具体例子A/B相互依赖 A需要代理把创建过程说一遍。这里还有一个加分项SpringBoot 2.6.0之后默认allowCircularReferencesfalse循环依赖默认被禁止但很多老项目会手动改回来。能把这个演进说出来证明你不是只看过老博客。实操提醒Spring Boot 2.6及以上版本如果启动日志出现“The dependencies of some of the beans in the application context form a cycle”优先检查是否真的需要循环依赖如果确实需要可以设置spring.main.allow-circular-referencestrue但更推荐重构代码消除循环依赖。4. 销毁阶段比你想的更讲究4.1 销毁顺序与各个回调容器关闭时比如Spring Boot的优雅停机、context.close()Spring会调用doClose()紧接着通过destroySingletons()销毁所有单例Bean。销毁阶段的回调顺序PreDestroy → DisposableBean.destroy → 自定义destroy-method这个顺序刚好和初始化阶段相反但有一点容易忽略销毁只针对单例Bean原型Bean不会自动销毁。原型Bean每次getBean都是新实例容器不持有它们的引用自然也无法管理它们的销毁需要业务代码自己负责清理。4.2 常见的销毁场景与坑数据库连接池、线程池的Bean销毁通常通过PreDestroy或destroy-method来关闭资源。如果Bean没被正确销毁可能导致连接泄漏。Shutdown HookSpring Boot中通过registerShutdownHook()注册JVM关闭钩子确保ApplicationContext能正常关闭执行所有销毁逻辑。Scope(prototype)的Bean即使指定了destroy-method也不会执行这个很多人实测过会踩坑。如果说初始化阶段是面试的重头戏那销毁阶段就是检验候选人是否真正有生产经验的分水岭。只要你能主动说出“原型Bean不执行销毁回调”这一点面试官对你的实战印象分就会高不少。5. 完整时序图用代码级视角串一遍为了让大家有一个整体画面我把整个getBean过程串成一段伪代码方便面试前快速回顾// 简化版的 AbstractBeanFactory.doGetBean 逻辑非源码便于理解 protected Object doGetBean(String name) { // 0. 先从单例池一级缓存查 Object sharedInstance getSingleton(name); if (sharedInstance ! null) { return getObjectForBeanInstance(sharedInstance, name); } // 1. 检查该 BeanDefinition 是否存在 // 2. 如果存在循环依赖正在创建走三级缓存获取早期引用 // 3. 创建 Bean单例 if (mbd.isSingleton()) { sharedInstance getSingleton(name, () - { return createBean(name, mbd, args); }); } // 4. 如果 Bean 是 FactoryBean再进行加工 return getObjectForBeanInstance(sharedInstance, name); } // 简化的 createBean → doCreateBean 流程 protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) { // 5. 实例化构造方法 BeanWrapper instanceWrapper createBeanInstance(beanName, mbd, args); Object bean instanceWrapper.getWrappedInstance(); // 6. 提前暴露将 ObjectFactory 放入三级缓存仅单例且允许循环引用 addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); // ---- 此时其他 Bean 可以从三级缓存拿到早期引用 ---- // 7. 属性填充 populateBean(beanName, mbd, instanceWrapper); // 8. 初始化 exposedObject initializeBean(beanName, exposedObject, mbd); // 9. 注册 DisposableBean如果配置了销毁方法 registerDisposableBeanIfNecessary(beanName, bean, mbd); return exposedObject; }面试时能把这段“逻辑”讲清楚远比背一篇源码译文更打动面试官。不需要你逐行记源码但核心思路先查缓存、再创建、提前暴露、填充、初始化、注册销毁必须烂熟于心。6. 手写Spring为什么推荐你用最小实现理解生命周期6.1 自己写一个Mini容器需要哪些核心类很多人在“手写Spring”面试环节露怯其实不必慌。用最小实现拆解生命周期只需要四个核心类AnnotationConfigApplicationContext或自己定义的MiniApplicationContext负责扫包、注册BeanDefinition、refresh。DefaultSingletonBeanRegistry维护一级缓存单例池实现getSingleton逻辑。AbstractBeanFactory实现getBean的模板流程。AbstractAutowireCapableBeanFactory实现doCreateBean包含实例化、属性填充、初始化。如果你能写出上面这套骨架面试官对你的源码理解程度基本就放心了。6.2 手写过程中的关键决策点我建议你在练习时重点关注三个决策点Aware回调怎么触发在initializeBean前判断Bean是否实现BeanNameAware等接口主动调用对应方法。BeanPostProcessor链表如何管理需要有一个ListBeanPostProcessor按序执行postProcessBeforeInitialization和postProcessAfterInitialization返回值替换原Bean。单例池何时放Bean单例Bean必须在doCreateBean完成初始化后放入一级缓存而不是实例化后立刻放入否则拿到的是“半成品”。我写过一个约300行的小型容器实测能覆盖Component扫描、Autowired字段注入、单例池、BeanPostProcessor扩展点和PostConstruct回调。写完之后再回看Spring源码很多之前不懂的抽象就通了——手写不是造轮子是给自己装一块“调试器”。继续这个话题如果面试官当场要求“画一下Bean生命周期”你该画什么很多人习惯画那种一大张的流程图标满各种接口名反而让面试官抓不住重点。我的经验是用一条竖向时序线从左到右分成“准备阶段、创建阶段、使用阶段、销毁阶段”四个区块中间标注关键的BeanPostProcessor介入点就够了。面试官想听到的不是你会背多少接口而是你能不能在合适的粒度上用自己的话把“对象从无到有再到销毁”这件事讲清楚。所以下面我换一种“白话版”的表达方式把上面所有细节浓缩成一个故事帮你用得起来。7. 白话版总述用一段话说清楚Bean的一生你可以这样回答面试官Spring在启动时会扫描配置把每一个Bean的定义信息BeanDefinition注册进容器。当代码里第一次getBean()时容器先查单例池没有的话开始创建。创建分三步第一步通过构造方法把对象new出来此时它还是个“素人”第二步把依赖的属性、配置值填进去就像给房子通水电第三步就是各种“开光仪式”——先回调一堆Aware接口让Bean知道自己在哪个容器、叫什么名字然后执行BeanPostProcessor的before方法接着执行PostConstruct、afterPropertiesSet、自定义init方法最后执行BeanPostProcessor的after方法。如果这个Bean需要被AOP代理after这一步就会返回一个代理对象。到此成品Bean被放进一级缓存单例池供后续使用。容器关闭时再按PreDestroy、DisposableBean、自定义destroy方法的顺序做清理资源该关的关、该释放的释放。这一段讲下来面试官基本就会觉得“这个人有底子”。8. 高频追问与避坑实录8.1 追问一Autowired 是在哪一步注入的在属性填充populateBean阶段由AutowiredAnnotationBeanPostProcessor的postProcessProperties方法处理。它先通过MetadataReader读取Bean类上所有Autowired、Value、Inject标注的字段和方法然后逐个解析依赖并注入。8.2 追问二BeanPostProcessor和InitializingBean的执行顺序能互换吗不能。BeanPostProcessor的postProcessBeforeInitialization在afterPropertiesSet之前执行postProcessAfterInitialization在init-method之后执行。Spring源码中initializeBean方法的调用关系是写死的// AbstractAutowireCapableBeanFactory.initializeBean 内部 Object wrappedBean bean; if (mbd null || !mbd.isSynthetic()) { wrappedBean applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); } invokeInitMethods(beanName, wrappedBean, mbd); // 这里先后调 afterPropertiesSet 和 init-method wrappedBean applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);8.3 追问三原型Bean的生命周期和单例有什么区别区别有两点一是原型Bean默认不走销毁回调容器不管理它的销毁二是原型Bean拿到的每次都是新实例没有一级缓存。但原型Bean依然会走实例化、属性填充、Aware、BeanPostProcessor、初始化这些正向流程。补充一个冷门考点Lookup方法注入和ObjectProviderT就是用来解决“单例Bean中依赖原型Bean”问题的而不是把单例Bean改成原型。8.4 追问四Spring Boot中Bean生命周期有没有额外的阶段Spring Boot本身没有改变Bean生命周期的核心流程但引入了ApplicationContextInitializer、EnvironmentPostProcessor、自动配置类Configuration中Bean等额外环节。它们发生在容器刷新之前或配置加载阶段不影响Bean“出生到销毁”的骨架。8.5 追问五自循环依赖A依赖它自己是怎么处理的自循环依赖如果通过属性注入Spring的三级缓存是能处理的A实例化后放入三级缓存填充属性时发现依赖自己从三级缓存拿回早期引用注入后继续完成初始化。但如果通过构造方法注入依然会报错。9. Spring AOP与生命周期代理时机全解析9.1 代理对象生成不在属性填充而在初始化之后AOP自动代理的核心是AnnotationAwareAspectJAutoProxyCreator它也是一个BeanPostProcessor。在postProcessAfterInitialization中它会检查当前Bean是否匹配Aspect里定义的切点表达式如果匹配就生成代理对象返回。这里有个实际场景可以帮大家理解如果Bean内部方法是this调用方式比如this.doSomething()代理不生效因为this指向的是原始对象而不是代理对象。这就是“自调用不走AOP”的经典坑。解决办法是注入自身代理、使用AopContext.currentProxy()或拆分调用链。9.2 三级缓存里的提前代理是怎么回事循环依赖时Bean还没走到postProcessAfterInitialization但为了让循环依赖的另一方能拿到正确的引用三级缓存存的ObjectFactory会提前执行getEarlyBeanReference()。如果Bean需要被代理这里会创建代理对象放入二级缓存。这个“提前代理”的对象与后面初始化完成后生成的代理对象必须是同一个否则循环依赖的引用不一致。Spring通过AbstractAutoProxyCreator里的earlyProxyReferences集合来记录哪些Bean已经提前代理过后面postProcessAfterInitialization看到该Bean已经提前代理就不会再生成一个新代理保证对象一致性。避坑提示当你在业务代码里通过 Autowired 注入一个被循环依赖AOP的Bean时拿到的其实是“提前代理”的那个对象。如果这个代理在提前代理时状态不全部分依赖还没注入可能在特定时间点访问它的部分字段会得到null。生产环境遇到这种问题别先怀疑三级缓存多半是设计上就不该有循环依赖。10. 实战建议怎样把生命周期知识变成面试加分项到这里核心内容讲得差不多了。我最后分享几个直接可用的建议帮你把理论知识转成面试现场的输出能力。10.1 准备一张“记忆卡”把你自己的答案浓缩成一张卡片上面写五条生命周期分BeanDefinition和Bean实例两个阶段。正向流程实例化→属性填充→Aware→Before初始化→InitializingBean→init-method→After初始化。销毁流程PreDestroy→DisposableBean→destroy-method。BeanPostProcessor是核心扩展点AOP代理在After初始化。三级缓存解决单例属性注入的循环依赖构造方法循环依赖无解。面试前抽5分钟过一遍这张卡就够。10.2 讲给“外行”听练就通俗表达如果你能用“房子装修”的类比把生命周期讲给非技术朋友听打地基是实例化、走水电是属性填充、装窗帘是BeanPostProcessor、入住是放入单例池、退租是销毁说明你已经内化了这套知识。我在准备面试时就是靠这个办法把Spring源码从“背过”变成“理解”的。10.3 动手写一个最小容器强烈建议找个周末自己写一个仅支持单例Bean、Autowired字段注入、BeanPostProcessor的迷你容器。写的过程中你会遇到很多“原来是这个原因”的顿悟时刻比如为什么构造方法注入无法解决循环依赖、为什么单例池放的是初始化后的Bean等等。这比刷十篇源码解析都管用。11. 一些体制外的经验感悟做了这么多年Java开发我越来越觉得Spring的Bean生命周期这个知识点表面上是面试八股实际上是一个工程师对“容器化思维”理解深浅的分水岭。能把生命周期讲明白的人通常对依赖注入、AOP、代理、作用域这些概念都会有自己的体系化理解而只会背流程的人一旦被追问到“为什么需要三级缓存”“AOP代理到底在哪一步生成”很快就会漏出短板。我个人最深的体会是面试时不要追求“一次把所有细节说完”而是先抛主干再根据面试官的追问方向展开。比如提到BeanPostProcessor时停一下观察对方是否对AOP代理感兴趣提到三级缓存时主动提一句“它解决的是单例属性注入下的循环依赖”既显得严谨又留了对话空间。抓住主动权远比被动填空更有感染力。如果你正在准备面试把这篇文章读透再关上屏幕用自己的话复述一遍最好能写成一页笔记。等你什么时候能把生命周期讲得像讲日常生活一样自然这个知识点才算真正长在你身上了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑