资讯动态

Spring Bean创建全解析:从BeanDefinition到三级缓存

发布时间:2026/10/8 3:57:06 来源:尧图企业网站定制
Spring Bean的创建过程是理解Spring容器机制的一把钥匙。不管你平时写的是Spring Boot接口、Spring Cloud微服务还是传统SSM项目最终都是靠容器把Bean创建好、组装好、交到你的代码里。这项能力同时也是Spring面试题里的常客光Bean的完整生命周期和三级缓存解决循环依赖这两块就足以让不少候选人当场卡壳。这篇文章是把我在排查Bean相关问题和阅读Spring源码过程中的理解做了系统整理。内容适合正在准备Spring面试的开发者、想真正搞懂依赖注入底层逻辑的读者以及那些遇到Bean提前初始化、循环依赖报错却不知道从哪里查起的人。读完你能完整说出Bean从配置到销毁经历的所有阶段知道每个阶段提供了哪些扩展钩子也能回答为什么二级缓存不够非要三级缓存这类深度问题。下面直接进入正题。1. Bean创建的整体流程从配置到可用对象1.1 一切从BeanDefinition开始先说结论Spring容器里几乎没有Java对象直接拿来用这回事任何被容器管理的Bean都必须先被描述成一份BeanDefinition。不管你在XML里写bean标签、在配置类里写Bean方法、还是在业务类上标Component最终都会被解析器转换成BeanDefinition注册进BeanDefinitionRegistry。BeanDefinition里装的信息比想象中多beanClassName决定实例化时创建什么类型scope是单例还是原型lazy-init是否延迟初始化initMethodName和destroyMethodName属性值也就是依赖属性的配置构造器参数、自动装配模式等这些信息注册完之后容器才真正开始创建Bean。这里有一个很多新手容易忽略的点BeanDefinition注册和Bean创建不是一回事。Configuration里的Bean方法甚至大部分Component在容器启动早期也只是被注册成BeanDefinition并没有立刻创建实例。真正的实例化通常发生在容器refresh()过程中的finishBeanFactoryInitialization阶段。这也是为什么你在构造器里依赖另一个Bean、而那个Bean还没创建时会触发依赖Bean的提前创建。1.2 实例化前的全局拦截BeanFactoryPostProcessor在大量Bean开始创建之前Spring给了我们一个全局干预的时机BeanFactoryPostProcessor。它的作用对象是BeanDefinition本身。比如你可以在容器启动时扫描所有BeanDefinition把某个类的scope统一改成prototype或者给特定Bean补充属性值。这类扩展点的典型代表是PropertySourcesPlaceholderConfigurer和ConfigurationClassPostProcessor。前者负责解析占位符后者负责处理Configuration、ComponentScan等注解。举个具体例子你经常用的Value(${server.port})为什么能用属性文件的值替换占位符就是因为PropertySourcesPlaceholderConfigurer在Bean实例化之前遍历了所有BeanDefinition把字符串模板里的${...}替换成了实际属性值。这类处理必须发生在实例化之前否则对象都建好了替换就没有意义。我第一次读源码时很疑惑为什么Spring要先把所有BeanDefinition收集齐再做全局处理而不是解析到哪就创建到哪后来想明白这种两阶段设计是为了保证全局扩展点能对整体配置做统一处理同时也让Bean创建的编排更加可控。实际教训也有一条如果你要写自己的Spring扩展绝大多数场景优先考虑BeanPostProcessor而不是BeanFactoryPostProcessor。前者处理对象实例后者处理定义信息一旦搞错你修改的东西可能在实例化时根本没被用到。1.3 容器refresh()里的关键触发顺序Spring Boot启动后最终会走到AbstractApplicationContext.refresh()方法里面包含十几个步骤。跟Bean创建直接相关的是最后几步invokeBeanFactoryPostProcessors()执行BeanFactoryPostProcessorregisterBeanPostProcessors()注册所有BeanPostProcessorfinishBeanFactoryInitialization()实例化所有非懒加载的单例Bean这里有个顺序上的讲究BeanPostProcessor必须先注册但不会立刻执行真正的执行时机在每个Bean的实例化过程中。而BeanFactoryPostProcessor必须最先执行因为它修改的是图纸图纸不改完施工单位不能开工。很多人读源码时被容器启动流程绕晕我的记忆方法就三句话先改图纸BeanDefinition再挂监理BeanPostProcessor最后开工造房子实例化Bean。2. 实例化、属性填充、初始化Bean生命中三个关键阶段2.1 阶段一实例化从配置描述变成内存对象实例化解决的是对象内存从无到有的问题。Spring通过InstantiationStrategy来干活默认实现是SimpleInstantiationStrategy。从源码角度看实例化有四种常见路径使用无参构造器直接newInstance()使用构造器参数通过ConstructorResolver解析后创建使用静态工厂方法或实例工厂方法通过Bean方法返回对象其中构造器参数解析是整个阶段最复杂的地方。被标注了Autowired的构造器、参数里有引用类型时Spring需要先找到参数对应的Bean还要在多个候选构造器之间按规则做选择。最常见的坑是类里同时有无参构造器和带参构造器且都满足条件时Spring的匹配逻辑容易让人意外。从实战角度说我强烈建议一个类只保留一个必要的构造器。这是Spring官方文档推荐的实践也能让Spring在推断构造器时不用做复杂选择。配合Lombok的RequiredArgsConstructor写起来很干净同时减少了一大批莫名其妙的启动报错。为了验证生命周期各阶段顺序我经常写一个测试Bean在每一步打日志Component public class LifecycleBean implements InitializingBean, DisposableBean { public LifecycleBean() { System.out.println(1. 构造器执行); } Autowired public void setDependency(DependencyService dependency) { System.out.println(2. 属性填充依赖注入); this.dependency dependency; } PostConstruct public void postConstruct() { System.out.println(3. PostConstruct执行); } Override public void afterPropertiesSet() { System.out.println(4. afterPropertiesSet执行); } PreDestroy public void preDestroy() { System.out.println(6. PreDestroy执行); } Override public void destroy() { System.out.println(7. destroy执行); } }启动容器后控制台会按顺序打印1到4关闭容器时打印6和7。这份输出贴在简历项目里或者面试时口头复述都比干讲理论有说服力。2.2 阶段二属性填充依赖注入真正落地的位置对象创建出来后内部还全是null。属性填充阶段负责把依赖属性灌进去这是依赖注入真正的落地位置。属性填充主要处理三类来源Autowired、Resource、Value等注解驱动的注入XML里property配置自动装配模式byType/byName在doPopulateBean()中Spring会先通过InstantiationAwareBeanPostProcessor的postProcessProperties方法让扩展点参与属性注入。实际干活的基建是AutowiredAnnotationBeanPostProcessor它扫描字段和setter方法对Autowired注解的字段调用beanFactory.resolveDependency()来解析依赖。这里有个常被忽略的细节属性填充阶段的依赖解析如果遇到循环依赖会触发Spring的提前暴露。这也是为什么讨论三级缓存时大家都会说到对象创建到一半就被拿出去用了。属性填充的报错信息通常是BeanCreationException...Error creating bean with name...内部一般带着Unsatisfied dependency expressed through field这类提示。看到这个关键字第一反应就应该是有Bean的依赖没解析成功去查依赖注入的写法。另外提醒一个容易踩的点字段注入的Bean在单元测试里很麻烦因为没法直接替换依赖。我个人的偏好是RequiredArgsConstructor生成构造器注入。构造器注入在测试时直接new出来传参就行不需要Spring上下文。2.3 阶段三初始化BeanPostProcessor与AOP代理的诞生初始化解决的是对象建好后进行自定义配置和增强的问题。它和实例化是两码事。实例化是new出来了对象初始化是new完之后做设置。标准初始化流程大致如下检查Bean是否实现了Aware接口族BeanNameAware、BeanClassLoaderAware、BeanFactoryAware调用BeanPostProcessor的postProcessBeforeInitialization执行InitializingBean的afterPropertiesSet执行自定义init-methodPostConstruct在这里归CommonAnnotationBeanPostProcessor管调用BeanPostProcessor的postProcessAfterInitialization注意一个实际顺序PostConstruct注解的方法在Spring内部并没有被当成独立的初始化方式而是在CommonAnnotationBeanPostProcessor的postProcessBeforeInitialization阶段被调用。所以如果在类里同时放了PostConstruct和自定义initMethodPostConstruct一定先执行。AOP代理是什么时候生效的关键点在第5步。比如AnnotationAwareAspectJAutoProxyCreator就是一个BeanPostProcessor它在这个阶段判断当前Bean是否匹配切面表达式匹配就生成代理对象返回。这解释了为什么你在Bean的构造器里看不到代理生成但注入到其他Bean那里时拿到的已经是代理对象。面试中常问的一个Bean从构造到返回给调用方中间经过哪些后置处理器干预就要么把这一节的流程完整背下来。特别是postProcessAfterInitialization这一步它让Spring不用侵入业务代码就能给任意Bean附加AOP功能这是Spring强大扩展性的核心。2.4 辅助钩子Aware感知与销毁清理除了三个主要阶段还有两个辅助环节值得说。Aware感知发生在属性填充之后、正式初始化之前。实现BeanNameAware可以拿到Bean名称实现ApplicationContextAware可以拿到ApplicationContext容器对象。拿到容器引用在写框架组件时很有用但在业务代码里要尽量避免它会让Bean和容器耦合测试困难。销毁阶段发生在容器关闭时。流程是先执行PreDestroy再执行DisposableBean的destroy方法最后执行destroy-method。对于prototype作用域的Bean容器在创建后就不再管理它的销毁。这一点面试官经常考标准答案是prototype的Bean容器不负责销毁由调用方自己管理。另外补充一个容易出错的地方如果Bean方法上定义了destroyMethod而类里还有PreDestroy执行顺序会乱吗不会PreDestroy永远在前面。Spring在DisposableBeanAdapter里统一编排顺序顺序固定为注解回调在前、接口回调次之、配置方法最后。3. 三级缓存与循环依赖为什么二级不够非要三级3.1 三级缓存的结构和前置概念循环依赖就是A依赖B、B依赖A。常规思维下这里是死锁的创建A需要B创建B需要A。Spring解决这个问题靠的是三级缓存机制。三个Map的用途要记牢singletonObjects一级缓存存的是完全创建好的单例BeanearlySingletonObjects二级缓存存提前暴露的早期Bean引用对象已实例化但未完成属性填充singletonFactories三级缓存存ObjectFactory工厂再记一个关键操作AbstractAutowireCapableBeanFactory在属性填充前会调用addSingletonFactory方法把当前正在创建的Bean包装成一个ObjectFactory存入三级缓存。等别的Bean依赖它时通过这个工厂拿到早期引用。为什么不能只用一级缓存如果用一张Map一个Bean要么没创建完、要么创建好处在中间态时其他Bean无法获取引用循环依赖直接死锁。二级缓存能暴露早期对象如果单纯为了解决循环引用二级缓存确实够用。那三级缓存存在的意义到底是什么答案是AOP代理。如果一个Bean需要被代理早期暴露的引用必须是代理对象否则别的Bean已经引用了原始对象后续没法再替换。三级缓存里存的是ObjectFactory它允许Spring在暴露早期引用的那一刻才决定返回原始对象还是代理对象。如果用二级缓存把引用提前固化而BeanPostProcessor还没跑到创建代理那一步代理逻辑就很难做了。3.2 三级缓存解决setter循环依赖的完整推演我把这个流程用文字推演一遍方便你对照源码去读。第一步创建A实例化完成把A对应的ObjectFactory注册到三级缓存然后开始填充A的属性。 第二步A的属性填充需要B去容器找B。容器创建BB实例化完成后把B的ObjectFactory也放进三级缓存然后填充B的属性。 第三步B的属性填充需要A查找A时发现一级缓存没有二级缓存也没有但三级缓存里有ObjectFactory。调用这个工厂得到A的早期引用可能是原始对象也可能是代理对象放入二级缓存并注入到B中。 第四步B完成属性填充、初始化成为完整Bean后放入一级缓存B被返回给A。 第五步A拿到B后继续完成属性填充、初始化和各种后置处理最后也放入一级缓存。A和B都创建成功。这个过程的灵魂在于Bean是边创建边暴露的而不是等全部构建完再让别人用。这也是Spring能够在单线程模型内完成依赖图构建的关键。读源码时记住三个方法名addSingletonFactory往三级缓存放工厂、getSingleton查缓存并触发工厂调用、getEarlyBeanReference工厂里决定原始对象还是代理对象。三次缓存相关的源码量不大把这三个方法串起来读一遍基本就通了。3.3 构造器循环依赖为什么解不了有一个面试高频题Spring能解决构造器循环依赖吗答案是不能。原因很简单三级缓存暴露早期引用的时机发生在实例化完成之后、属性填充之前。如果是构造器参数注入那么实例化这一步就必须依赖另一个Bean此时当前Bean还没实例化完根本无法提前暴露。setter注入和字段注入能解决循环依赖本质上是因为对象先new出来了属性后面慢慢填。构造器注入是先把依赖备齐才能new先天绕不开。所以我在项目里推荐业务Bean用构造器注入不是因为它能解决循环依赖恰恰相反是为了尽早暴露循环依赖问题避免上线后出现莫名其妙的懒加载错误。循环依赖本身是设计上的坏味道能重构就重构而不是全靠框架兜底。4. 常见问题排查、实战避坑与手写Spring思路4.1 Bean创建失败时如何快速定位见过太多人一看到BeanCreationException就慌。其实堆栈再长只用看两部分第一是Error creating bean with name后面跟的Bean名这决定了当前创建失败的入口是谁第二是Caused by之后最底层的异常那才指向根本原因。归纳一下Bean创建失败最常见的原因有这几类构造器抛异常依赖的配置缺失、依赖的服务不可用或初始化逻辑里直接抛了运行时异常依赖注入失败Autowired的Bean不存在或者有多个候选Bean且没有Primary/Qualifier区分循环依赖且无法解决构造器循环依赖或者代理模式下的循环依赖触发提前引用不能被代理错误初始化方法抛错afterPropertiesSet实现里抛了异常或initMethod指向的方法不存在排查时我的习惯是先加启动参数--debug或者开启Spring启动日志定位失败发生在哪个阶段。如果是测试环境可以用SpringBootTest配合properties指定需要加载的配置逐模块缩小范围。有一次我在项目里排查一个Bean初始化失败把所有Component都去掉再一个个加回来虽然笨但特别有效。4.2 循环依赖相关的典型报错与应对第一个报错是BeanCurrentlyInCreationException。看到这个异常说明Spring明确判断当前Bean创建到一半又重复进入了创建流程。构造器注入循环依赖最常见报错堆栈里能看到Requested bean is currently in creation。第二个场景是Spring Boot 2.6以后默认禁止循环依赖即使setter注入也会报错。这个开关是spring.main.allow-circular-referencesfalse如果项目从旧版本升级上来可能突然就起不来了。解决办法有两种显式打开允许循环引用或者重构代码去掉循环依赖。我强烈建议别为了省事直接允许应该想想为什么两个Bean要互相依赖很多时候提取一个第三方服务或者拆分职责就能根治。还有一个跟AOP相关的坑循环依赖的Bean如果又需要AOP代理早期暴露引用时可能拿不到完整代理。Spring Boot 2.6默认关闭循环依赖的另一个考量正是为了减少这类不确定性问题。总之遇到循环依赖报错先别急着改配置先梳理依赖关系能拆就拆。4.3 初始化顺序引发的诡异问题有一次遇到线上问题一个Bean在PostConstruct里读取配置结果读到null。排查之后发现配置不是Spring标准的Environment而是另一个组件在ApplicationListener里动态写入的。问题就出在监听器触发时机和Bean初始化的先后上。这类问题没有统一解法只能靠经验判断。但有一个方法论遇到启动阶段某Bean的值不对不要只盯自己的初始化方法先搞清楚整个启动时序。Spring在refresh()过程中会发布好几个事件比如ApplicationPreparedEvent、ContextRefreshedEvent等。在这些事件监听器里打日志就能观察各Bean初始化的先后顺序。我的建议是涉及全局配置或跨模块初始化的逻辑尽量放到ApplicationRunner或ApplicationListener里执行不要散落在各个Bean的PostConstruct里否则执行顺序极难控制。特别是多模块项目A模块的初始化依赖B模块的数据如果都写在各自的PostConstruct里启动三次可能顺序都不一样。4.4 手写Spring的思路与面试必答点准备面试时很多人会去找手写Spring的教程但被源码绕晕以后就放弃了。这里我给一条核心路径实现一个简单容器解析BeanDefinition提供实例化和依赖注入的简单逻辑然后把BeanPostProcessor加进去。能用自己的代码复现属性注入和初始化钩子的顺序时再回头看Spring源码就会顺畅很多。一个最小可跑的简化容器核心就这么多public class SimpleBeanFactory { private MapString, Object singletonObjects new ConcurrentHashMap(); private MapString, BeanDefinition beanDefinitionMap new HashMap(); public Object getBean(String name) throws Exception { // 一级缓存命中直接返回 Object bean singletonObjects.get(name); if (bean ! null) { return bean; } // 根据BeanDefinition创建 BeanDefinition bd beanDefinitionMap.get(name); Object instance createInstance(bd); populateBean(instance); // 属性填充 initializeBean(instance); // 初始化 singletonObjects.put(name, instance); return instance; } }把createInstance、populateBean、initializeBean三步分别实现你就理解了Spring容器最核心的骨架。在这个基础上再往populateBean之前塞一个ObjectFactory三级缓存那部分也就通了。面试中被问Spring的核心扩展点有哪些时最容易漏掉三个点第一Spring解决循环依赖不是容器启动后统一处理的魔法而是创建单个Bean的过程中边创建边暴露。第二三级缓存的价值在AOP场景里才充分体现如果没有代理二级缓存就够用。第三BeanPostProcessor不只是初始化前后各执行一次那么简单Autowired注入、PostConstruct调用、AOP代理全是靠它实现的。理解到这一层才算真正理解了Spring的可扩展设计。写在最后我的一点实践经验最后分享一点自己的感受。如果你现在在面试前临时翻Spring源码建议别从refresh()开始背那是容器级视角信息量太大很容易迷失。先从单个Bean的创建链路读起跟一遍某个Bean从BeanDefinition到postProcessAfterInitialization的完整流程再回来看refresh()你会发现对Spring的理解突然清晰了。我在实际排查Bean初始化问题时还有一个小技巧遇到诡异问题先在类里加一个静态计数器在构造器、PostConstruct、BeanPostProcessor的各个阶段分别打印日志马上就能看出这个Bean被构造了几次、创建到哪一步断掉的。这个方法在排查代理类反复创建、Bean被多次实例化这类问题时特别管用。Spring的Bean创建过程说到底就是一套有先后顺序、有扩展点、有兜底机制的对象生产线。理解它不只是为了应付面试更是为了线上出问题时能少熬几个夜。

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

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

免费获取报价 →
↑