资讯动态

Spring源码面试:Bean生命周期、三级缓存与@Autowired解析

发布时间:2026/10/14 1:17:18 来源:尧图企业网站定制
面试季我常被问到一类问题“你读过Spring源码吗”说读过的人很多但能扛住追问的很少。就拿“Bean生命周期”这个最经典的问题来说面试官只要连续追问几个“为什么”为什么三级缓存能解决循环依赖、为什么构造器注入的循环依赖救不回来、Autowired是在哪个阶段生效的、为什么PostConstruct会先于InitializingBean执行大部分人就答不上来了。这篇东西写给两类人一类是准备面试、想从“背八股”进化到“讲原理”的Java工程师另一类是已经写了几年业务代码但对Spring容器始终停留在“会用”层面的朋友。我会从源码路径出发把IOC容器启动、Bean实例化、初始化回调、循环依赖、自动装配、构造方法推断这些点一个个拆开结合面试中真实会被追问的细节来讲。读完你能做到的是在面试现场用自己的话把这条链路的来龙去脉讲清楚而不是背下一堆结论。1. 从“背结论”到“看源码”面试追问链是怎么建立的1.1 一个典型的IOC连环追问很多面试官喜欢从“Spring Bean的生命周期”开场因为这个知识点覆盖面广、由浅入深能快速探测候选人的深度。完整的追问链大概是这样的第一层Bean生命周期有哪些阶段—— 背过的人都能答出实例化、属性填充、初始化、销毁。第二层初始化阶段有哪几种注册方式顺序是什么—— 开始有人卡壳尤其是SmartInitializingSingleton和PostConstruct的执行顺序。第三层循环依赖是怎么解决的三级缓存分别存的什么—— 多数人能背出“三级缓存”但说不清为什么需要三级二级够不够。第四层Autowired是在哪个阶段、由哪个类完成的—— 到这里基本就能筛掉一大半人。第五层如果构造器注入发生了循环依赖为什么不报错换成setter为什么就能解决—— 能答上来的人通常才是真正读过源码的。这条追问链的本质是面试官在验证你对“容器如何管理对象”这件事有没有建立真实的心理模型。源码阅读的意义不在于背类名和行号而在于帮你建立起这个模型BeanDefinition是什么、BeanFactoryPostProcessor和BeanPostProcessor的分工差异、容器创建Bean的那几个关键方法各自做了什么。有了模型再多的问题都是在这个模型上推算而不是靠记忆硬扛。1.2 源码阅读的三个正确姿势我在带团队和帮人准备面试时发现读源码最容易犯的错是“从头读源码”。AbstractApplicationContext.refresh()这一个方法就有十几行核心调用每一行往下追又是一个巨大的调用树追到第三天人就崩了。正确姿势应该是这样主线优先。每次只追一条主线比如今天只搞懂“一个Bean从getBean到返回实例路径上有哪些方法”其他的分支看到了先记笔记、不要深挖。一天解决一个问题比一周通读一遍有效得多。场景驱动。用最常见的场景去验证代码路径。比如启动一个最小配置的容器打断点看refresh()是怎么走到finishBeanFactoryInitialization()的再比如模拟一个setter注入的循环依赖打断点看三级缓存每一步的变化。场景越具体代码越容易读进去。版本固定。Spring的源码在不同版本之间有差异比如5.x和6.1之后循环依赖默认行为就有变化。读源码前先在pom里把spring-core的版本固定下来别拿5.2的代码去对6.x的包看会看得一头雾水。2. 容器启动的核心链路从配置类到BeanDefinition就绪2.1 ConfigurationClassPostProcessor启动时的第一个关键关卡容器启动的入口是AbstractApplicationContext.refresh()这里面有一个方法叫invokeBeanFactoryPostProcessors()它做了一件很多人知道但没细想过的事在所有Bean实例化之前先把配置类解析掉把Bean方法、ComponentScan扫描到的类、Import导入的类全部转换成BeanDefinition注册进容器。执行这个动作的类是ConfigurationClassPostProcessor它实现了BeanDefinitionRegistryPostProcessor而BeanDefinitionRegistryPostProcessor又继承自BeanFactoryPostProcessor。它的执行时机非常早早到容器里连一个普通Bean都还没开始创建。为什么要赶在这个时机做道理很简单如果先实例化了业务Bean后面扫描到的BeanDefinition就没法参与依赖注入了。好比装修房子你必须先定好每个房间的功能BeanDefinition再开始砌墙实例化砌完墙再改房间布局代价就大了。Spring把“收集房屋设计图”和“动工建造”两件事严格分开了这正是IOC容器设计的核心思想BeanDefinition是设计图Bean实例是成品。2.2 getBean与doCreateBean的分工BeanDefinition收集完毕之后refresh()会走到finishBeanFactoryInitialization()在这里触发对所有非懒加载单例Bean的实例化。这一段有个很经典的“分工”外部调用的统一入口是getBean()它负责处理单例缓存、作用域、循环依赖这些全局逻辑而真正new对象、填充属性、执行初始化的方法叫doCreateBean()。我之前看代码时容易混的就是这一层。getBean()更像前台接待它先检查一级缓存有没有现成的没有就找三级缓存看能不能提前暴露都不行才把活儿交给doCreateBean()。doCreateBean()是具体干活的工人负责createBeanInstance反射构造对象、populateBean属性填充、initializeBean初始化回调这三板斧。面试中如果说“getBean创建了一个Bean”严格讲是不准确的getBean的职责是“获取或创建”。这一个小细节讲出来就会让面试官觉得你是真看了源码而不是在背流程。3. Bean生命周期深度拆解实例化前后到底发生了什么3.1 实例化、属性填充与初始化三段式的真实边界很多面试题答案会把Bean生命周期列为“实例化→属性填充→初始化→销毁”四步这方向对但边界划得太粗。真正到源码层面边界要清晰得多。实例化对应的是doCreateBean()里的createBeanInstance()。这一步只负责通过构造方法反射出一个对象此刻这个对象内部所有属性都是默认值null、0、false它只是一个“半成品”。构造方法推断就在这里发生到底是走无参构造还是有参构造参数从哪里来都在这一步决定。属性填充对应populateBean()。这一步Spring会把BeanDefinition里记录的属性值和依赖项填入对象。如果有Autowired字段或setter方法也是在这一步由AutowiredAnnotationBeanPostProcessor在postProcessProperties()中完成的。但要注意填充的是bean里声明的属性和依赖PostConstruct方法还不在这步调用。初始化对应initializeBean()。这一步才轮到各种初始化回调。到这里对象已经是一个“完整可用”的Bean了所有依赖都就位回调方法才开始执行。很多人把初始化和实例化混在一起说面试一追“PostConstruct是在对象构造完之后立刻执行吗”一下就露馅了中间还隔着属性填充这一步。3.2 三种初始化方式的真实执行顺序Spring里注册初始化回调的方式有好几种最常见的是这三类PostConstruct注解方法由CommonAnnotationBeanPostProcessor处理在postProcessBeforeInitialization()阶段调用。实现InitializingBean接口的afterPropertiesSet()方法在initializeBean()里的invokeInitMethods()中直接调用。XML或Bean中声明的initMethod自定义方法也在invokeInitMethods()中调用但排在afterPropertiesSet()之后。它们的执行顺序是先PostConstruct再afterPropertiesSet()最后initMethod。很多人能背出这个顺序但说不清机制。其实顺序来自两套不同的执行通道PostConstruct走的是BeanPostProcessor的前置处理通道它在initializeBean()的开头部分先执行而InitializingBean和initMethod在invokeInitMethods()里执行后者在前置处理之后。我建议面试时别只背顺序要说清“PostConstruct之所以最早是因为它挂在BeanPostProcessor.postProcessBeforeInitialization()上扫描时机天然在最前面”。这样一讲连为什么这么设计都顺带交代了把注解回调和接口回调拆在不同阶段就是让扩展点可以叠加、可以排序而不是耦合到一个方法里硬编码。3.3 销毁阶段的细节与失效场景销毁阶段对应的是doCreateBean的逆过程入口在ConfigurableApplicationContext的close()。这里有个值得注意的细节Spring对容器关闭时销毁单例Bean的调用顺序是先执行PreDestroy注解方法再执行DisposableBean.destroy()最后执行自定义destroyMethod。但有一个很容易踩坑的场景原型Bean的销毁不归Spring管。每次getBean拿到一个新的prototype实例返回给你之后容器就不再跟踪它了所以PreDestroy和DisposableBean在prototype作用域下都不会被执行。很多项目里用prototypeBean存放数据库连接或线程池等到容器关闭时发现资源没释放排查到最后查出来是这个问题。正确做法是自己在业务代码里管理原型Bean的清理或者把它做成单例来持有再在单例的销毁逻辑里去清理。4. 循环依赖三级缓存的设计与边界4.1 三级缓存与“半成品”的提前暴露循环依赖说白了就是A依赖BB又依赖A。在构造器注入模式下这是个死结构造A需要B构造B又需要A谁都没法先new出来。但在setter注入或字段注入时Spring用一套“允许先给半成品”的机制解决了问题。这套机制就是DefaultSingletonBeanRegistry里的三个MapsingletonObjects一级缓存存放已经完整创建好的成品单例Bean。earlySingletonObjects二级缓存存放提前暴露的早期引用半成品。singletonFactories三级缓存存放ObjectFactory工厂对象用来生成早期引用。整个流程是这样的A开始实例化得到一个原始对象在属性填充之前Spring先把一个 ObjectFactory 存进三级缓存这个工厂对应的lambda内部封装了getEarlyBeanReference()逻辑也就是允许SmartInstantiationAwareBeanPostProcessor对早期引用做增强比如生成代理。属性填充时发现依赖B于是去创建BB的创建过程中填充属性时发现依赖A于是转回找A从三级缓存取出工厂执行lambda拿到A的早期引用半成品塞进B。B初始化完成后成为成品进入一级缓存。回到A继续完成自己的属性填充和初始化最后也进入一级缓存。面试时最基础的要求是能把这个流程复述出来。但如果只讲到这一步面试官大概率会接着问下一个问题“二级缓存不是也能存半成品吗为什么非要三级”4.2 二级缓存够不够的问题这是IOC问题里少有的“设计哲学”级考点。答案是如果Spring不打算支持AOP代理或者不考虑代理对象与原始对象的区分二级缓存确实够用。设想一下只存在两级缓存一级存成品二级存半成品。A实例化后在属性填充前直接把半成品放进二级缓存B拿到这个半成品引用完成注入一切正常根本不需要三级。但问题是如果A是一个需要被AOP代理的BeanAOP生成的代理对象通常是BeanPostProcessor后置处理的结果而循环依赖发生时属性填充阶段就要把A的引用给B了那时候A还没执行后置处理如果直接把原始半成品给BB里持有的就是原始对象A而不是最终容器里的代理对象A代理就失效了。三级缓存里那个ObjectFactory存在的意义是允许在“需要提前暴露A的引用”那一刻就通过getEarlyBeanReference()把代理逻辑跑一遍。也就是说B拿到的不是简单原始对象而是可能已经被代理过的早期引用。等到A自己走完初始化后置处理容器会判断已经产生的代理和最终要生成的代理是否一致从而保证循环依赖场景下代理不失效、单例不变形。所以在源码里AOP相关的AbstractAutoProxyCreator是一个SmartInstantiationAwareBeanPostProcessor它重写了getEarlyBeanReference()来提前创建代理。这解释了为什么三级缓存设计成工厂而不是直接放对象工厂是懒加载的只有真正出现循环依赖需要提前引用时才执行不出现循环依赖时这个lambda根本不执行也就不存在代理提前暴露的额外开销。4.3 哪些循环依赖救不回来三级缓存不是万能的有三个非常典型的场景会把循环依赖直接干成启动失败构造器注入的循环依赖。因为Spring必须调用构造方法才能拿到对象连半成品都产生不了。流程走进createBeanInstance()时参数解析需要BB又要A死锁直接抛BeanCurrentlyInCreationException。prototype作用域的循环依赖。三级缓存的工厂机制只针对单例Bean设计每个prototype都是要现new的A没构造完就去引BB引A无法提前暴露一样报错。Async或过早AOP代理的场景有时也会因为代理链路复杂化暴露出问题虽然用了三级缓存可以延迟但配置不当依然可能引发循环依赖。还有一种情况特别容易踩坑从Spring Boot 2.6开始容器默认禁止循环依赖spring.main.allow-circular-referencesfalse。很多老项目从2.5升到2.7之后启动直接报“The dependencies of some of the beans in the application context form a cycle”就是这个默认策略变了。处理办法不是无脑改成true而是先审视代码为什么会产生循环依赖绝大多数业务场景下的循环依赖都属于设计问题通过调整依赖方向就能拆掉。改配置是最后选项。5. Autowired背后的执行机制与构造方法推断5.1 AutowiredAnnotationBeanPostProcessor的装配路径Autowired注解的真正执行者是AutowiredAnnotationBeanPostProcessor。它也是一个BeanPostProcessor但它的main逻辑不是postProcessBeforeInitialization而是postProcessProperties()。这个方法的执行点就落在populateBean()的属性填充阶段。这里有个面试官常挖的细节Autowired是“先按类型去找Bean再按名字去兜底”。这句描述来自AutowiredAnnotationBeanPostProcessor内部使用的DefaultListableBeanFactory的resolveDependency()流程。源码真实路径是先做类型匹配找到唯一的候选直接返回找到多个候选时才会看哪个Bean的名字跟当前字段名或setter参数名一致一致就选它不一致再抛NoUniqueBeanDefinitionException。通过这个机制Autowired对同类型多实现的处理逻辑是最优先匹配与字段名相同的Bean这跟Resource“先按名字再按类型”的默认策略刚好反着的。顺带一提很多人会用Autowired来注入一个接口的列表比如List 这是走的ObjectProvider和List依赖解析分支源码会收集所有该类型Bean并注入有序集合。这个细节用在回答“如何优雅地实现策略模式”的追问里非常加分。5.2 构造方法推断多个构造器的抉择除了字段和setter注入还有一种重要的注入方式是构造器注入而这背后有个容易忽略的机制——构造方法推断。Spring在createBeanInstance()中要决定用哪个构造方法实例化Bean。推断逻辑的核心在ConstructorResolver和AutowiredAnnotationBeanPostProcessor的determineCandidateConstructors()中。简单归纳是这样的类里只有一个构造方法直接用它不论有没有Autowired。没有构造方法默认无参构造。有多个构造方法且都没有AutowiredSpring 4.3之后会选择参数最长的那个构造方法前提是所有参数都能在当前容器中解析到Bean否则回退无参构造。有多个构造方法其中一个标注了Autowired优先用它。这个机制解决了老版本中“必须写无参构造”的麻烦。很多现代代码风格喜欢一切走构造器注入其实就是看到了构造器注入能保证依赖不可变、容易做单元测试这个优势。而这里有个坑是如果写了两个构造方法一个无参一个有参没标Autowired业务Bean里又到处go野地new结果Spring按规则选中长参数构造器可能直接报参数解析错误排查半天才发现是构造器推断不是按直觉来的。5.3 三种注入方式的实用选择结合源码和工程实践我对三者的选择建议是构造器注入最稳依赖明确且不可变但构造器参数多时类看起来会重字段注入写起来最简洁缺点是不易测试且容易形成隐式依赖setter注入灵活性最好适合可选依赖和运行时切换但略啰嗦。源码层面三者的处理路径也稍有不同字段注入和setter注入都走populateBean由AutowiredAnnotationBeanPostProcessor处理构造器注入则是走createBeanInstance在实例化阶段就完成依赖解析直接在构造参数里把依赖取出来。理解了这个分工你就能解释为什么构造器注入的Bean在构造时就知道所有依赖而字段注入在构造完、属性填充时才知道谁的依赖有问题——这会影响你对报错日志的判断。6. 面试怎么“答深”从报错日志到回答层次6.1 循环依赖报错日志的三种形态面试官问“循环依赖”时如果你能顺手说出几种典型报错长什么样会很加分。我总结过三种最常遇到的形态一BeanCurrentlyInCreationException。日志特征是出现“is in dependency resolution”和“currently in creation”。多发生在构造器注入或prototype循环依赖。定位思路是先看哪个Bean名出现在“currently in creation”里再找依赖链中谁引了它。形态二启动报cycle。特征是一串A→B→A的链路描述Spring Boot 2.6默认禁用循环依赖时常见。这种最好修因为链路直接打出来了顺着链路重构依赖方向即可。形态三NoUniqueBeanDefinitionException。它不是循环依赖但面试爱混着问。出现的原因是类型匹配时找到多个候选Bean又没有用Primary或Qualifier消歧。回答时可以顺带引出前面讲过的类型优先、名字兜底的机制展示源码级别的理解。6.2 我把一个锚点答案拆给你看前面讲的细节比较多我整理一个能串起来讲的锚点答案实际面试时从“初始化顺序”切入展开效果非常稳“Bean实例化之后会进入属性填充在这个阶段由AutowiredAnnotationBeanPostProcessor通过postProcessProperties来处理Autowired的依赖解析。填充完成后进入initializeBean先执行Aware接口回调再执行BeanPostProcessor的postProcessBeforeInitializationPostConstruct就挂在这个阶段随后调用invokeInitMethods先执行afterPropertiesSet再执行initMethod然后在postProcessAfterInitialization阶段生成代理。关于循环依赖容器的三级缓存里存的是ObjectFactory而不是直接存半成品对象这个设计是为了在需要提前暴露引用时能配合getEarlyBeanReference生成AOP代理从而保证B拿到的引用和最终容器中的Bean保持一致。”这段话用到的信息点全是从源码流程里提炼出来的环环相扣。面试官如果接着追问“postProcessAfterInitialization和三级缓存里的代理会不会重复创建”你可以继续讲AOP代理的判断逻辑和已代理对象不重复代理的细节。能答到这一层面试已经赢了。6.3 常见问题速查表问题一句话核心深度加分点Bean生命周期有哪些阶段实例化、属性填充、初始化、销毁能说出BeanPostProcessor挂载在初始化前后PostConstruct比InitializingBean早的原因一个走前置处理器一个走invokeInitMethods讲出两个通道不同顺序不是硬编码而是机制使然循环依赖为什么三级缓存不是两级三级缓存存ObjectFactory延迟到需要时生成代理结合AOP代理失效场景回答构造器注入为什么不支持循环依赖构造器阶段拿不到半成品能指出报错类名BeanCurrentlyInCreationExceptionAutowired和Resource区别前者类型优先名字兜底后者名字优先能用源码路径resolveDependency解释原型Bean为什么销毁回调不执行容器不跟踪原型实例生命周期能给出资源和线程池的清理方案我个人的经验是读源码这件事不用贪多也不需要把整个Spring的源码从头翻到尾。找几个高频面试题的切入点对着断点一步步走一遍把“为什么”想透收获远比通读大得多。每次项目里遇到奇奇怪怪的Bean问题比如初始化顺序不对、某个Bean注入成了null、代理没生效回到源码的流程上找通常几分钟就能定位。这套思维一旦建立面试和日常工作其实是同一个能力。

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

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

免费获取报价 →
↑