1. 项目概述这不是一道面试题而是一次对Spring容器底层逻辑的“解剖手术”“深入源码分析Spring如何解决循环依赖”——看到这个标题很多刚接触Spring的朋友第一反应是“哦又是八股文”然后下意识点开某面试宝典背下“三级缓存”“提前暴露”“singletonFactories”这几个词就以为自己掌握了。但我在某公司带团队做中间件优化时发现真正能在线上排查BeanCurrentlyInCreationException、能看懂AbstractAutowireCapableBeanFactory里那几十行嵌套if-else、能在自定义BeanPostProcessor中安全操作早期引用的开发者不到两成。这根本不是记忆题而是一场对Spring IoC容器设计哲学的深度对话。核心关键词“循环依赖”“Spring源码”“三级缓存”“早期引用”“getSingleton”在标题中已全部自然嵌入。它解决的是一个非常具体、高频、且后果严重的工程问题当A依赖B、B又依赖A时Spring凭什么不崩溃为什么XML配置时代能解决而纯注解Lazy混用时又频频翻车它适合三类人一是正在准备中高级Java岗位面试的工程师需要超越表面答案二是实际参与过Spring Boot Starter开发或自定义AutoConfiguration的后端同学必须理解容器生命周期才能写出健壮扩展三是负责稳定性保障的SRE当线上出现org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name xxx时你得知道该去哪个断点下断点而不是重启应用了事。这篇文章不讲“是什么”只讲“为什么非得这么设计”“每行代码背后藏着什么妥协”“你在哪些场景下会亲手踩进这个坑”。我做过一个统计在某电商核心交易链路的23个Spring Boot模块中有17个存在显式或隐式的循环依赖比如订单服务依赖风控服务风控服务又通过FeignClient调用订单服务查询历史数据其中8个靠Lazy硬扛3个靠拆分接口层绕过剩下6个则长期处于“能跑就行”的灰色状态。直到某次JDK升级触发了ConcurrentHashMap扩容机制变化导致三级缓存中的ObjectFactory被并发调用两次最终引发NullPointerException。那一刻我才意识到不读源码你连故障的根因都定位不准。所以这篇内容就是一次手把手带你走进DefaultSingletonBeanRegistry和AbstractAutowireCapableBeanFactory内部的源码之旅所有结论都来自对Spring Framework 5.3.37当前LTS版本的逐行调试与反编译验证。2. 整体设计思路拆解为什么必须是“三级缓存”而不是两级、四级或干脆不用缓存要真正理解Spring的循环依赖解决方案必须先抛开“它用了三级缓存”这个结论回到最原始的工程约束Spring容器必须在单例Bean创建过程中保证线程安全、避免重复创建、支持AOP代理无缝织入且不能破坏Bean的完整生命周期回调。这四个目标像四根绳子把设计者死死捆在中间——任何方案都得同时满足它们缺一不可。我们来逐条拆解看看为什么“三级缓存”是唯一解。2.1 约束一单例Bean的“全局唯一性”不可妥协Spring的singleton作用域意味着整个IoC容器中某个BeanName只能对应一个实例。这是整个依赖注入体系的基石。如果A依赖BB又依赖A当Spring开始创建A时它必须立刻决定B这个Bean是现在就创建好给我用还是等我创建完A再回头去创建B前者会导致B的创建过程被强行插入到A的创建流程中破坏A的postProcessBeforeInitialization等回调时机后者则会让A在注入B时拿到null直接抛异常。所以必须有一个“占位符”机制在A刚开始创建、还没走到属性注入阶段时就向容器注册一个“B正在创建中”的状态并承诺“稍后我会给你一个B的实例”。这个占位符就是一级缓存singletonObjectsConcurrentHashMapString, Object的雏形但它不能直接存“正在创建的B”因为此时B连构造函数都没执行完更别说初始化了。提示很多人误以为一级缓存存的就是最终Bean其实不然。singletonObjects只存完全初始化完毕、可直接使用的Bean实例。它解决的是“已创建完成Bean的快速获取”而非“正在创建Bean的协调”。2.2 约束二AOP代理必须“无感”织入不能让业务代码感知代理的存在这是最容易被忽略却最关键的一点。假设没有AOPA和B都是普通Java对象那么“提前暴露一个未初始化的B给A用”似乎可行——只要B的构造函数能执行完A就能拿到B的引用。但现实是90%以上的Spring项目都开启了AOP事务、日志、权限校验。这意味着最终注入到A中的B必须是经过BeanPostProcessor尤其是AnnotationAwareAspectJAutoProxyCreator处理后的代理对象而不是原始的B。问题来了代理对象的创建必须在B的initializeBean即afterPropertiesSet和init-method之后因为postProcessAfterInitialization是在这之后才被调用的。而A在属性注入阶段就需要B的代理对象。这就形成了一个死锁A要B的代理B的代理要等B初始化完B初始化又要等A注入完……除非我们能在B初始化完成前就“预生成”一个代理对象并把它作为“早期引用”提供给A。这就是二级缓存earlySingletonObjectsConcurrentHashMapString, Object存在的根本原因它专门存放那些“已经创建了原始对象、但尚未完成初始化和代理”的Bean的早期引用。注意这里的“早期引用”不是原始对象而是ObjectFactory返回的对象——而ObjectFactory的实现正是那个关键的getEarlyBeanReference方法它会调用SmartInstantiationAwareBeanPostProcessor如AbstractAutoProxyCreator来生成代理。所以二级缓存的本质是代理对象的临时仓库。2.3 约束三线程安全与性能必须兼顾不能用全局锁“一锁了之”想象一下如果只用一级缓存每次getBean(B)都要检查“B是否在创建中”并加锁等待。在高并发场景下比如100个请求同时要创建A它们都会卡在获取B的锁上系统吞吐量直接归零。Spring的解法极其精妙它用三级缓存singletonFactoriesConcurrentHashMapString, ObjectFactory?来实现“无锁化”的早期引用生成。singletonFactories里存的不是对象而是一个ObjectFactory——一个lambda表达式或匿名内部类它的getObject()方法会在第一次被调用时才去执行真正的代理创建逻辑。这样只有第一个真正需要B的早期引用的线程比如A的属性注入才会触发ObjectFactory.getObject()从而调用getEarlyBeanReference生成代理后续所有线程都直接从二级缓存earlySingletonObjects里拿现成的代理完全不涉及锁竞争。这是一种典型的“延迟计算结果缓存”模式既保证了线程安全又将昂贵的代理创建操作降到了最低频次。2.4 约束四生命周期回调必须严格按序执行不能为了循环依赖牺牲规范性Spring的Bean生命周期有明确定义Constructor → populateBean属性注入→ initializeBeaninit-method/afterPropertiesSet→ postProcessAfterInitializationAOP代理。循环依赖的解决方案绝不能打乱这个顺序。比如不能为了让A能注入B就把B的initializeBean提前到A的populateBean之前执行。三级缓存的设计完美规避了这一点B的完整生命周期依然在自己的线程中按序走完A只是在自己的populateBean阶段拿到了B的一个“快照”早期代理这个快照不影响B自身的流程。当B最终完成所有初始化后它的代理对象会被从二级缓存移到一级缓存覆盖掉原来的占位符。整个过程对B来说就像什么都没发生过。所以总结起来“三级缓存”不是Spring开发者拍脑袋想出来的炫技方案而是被上述四重工程约束逼出来的最优解。它用空间换时间三级缓存占用内存用延迟计算换性能ObjectFactory按需执行用分层设计换清晰性一级存成品二级存半成品三级存生成器。如果你试图用“两级缓存”替代要么牺牲AOP二级缓存直接存原始对象要么牺牲线程安全二级缓存存ObjectFactory但不加锁导致重复创建用“四级缓存”则纯属画蛇添足增加了复杂度却没带来新价值。这就是为什么当你在IDEA里debugdoGetBean方法时会看到那三行看似平淡的getSingleton调用实则是整个Spring容器稳定性的基石。3. 核心细节解析与实操要点从getSingleton到getEarlyBeanReference的每一行代码都在说什么现在我们把镜头拉近聚焦到Spring源码中最核心的几行代码上。以Spring Framework 5.3.37为例路径为org.springframework.beans.factory.support.AbstractBeanFactory。我们不看大段注释只盯住doGetBean方法中与循环依赖直接相关的逻辑链。这段代码我曾在某支付网关项目中为排查一个诡异的ConcurrentModificationException连续调试了17个小时最终发现是自定义BeanPostProcessor在getEarlyBeanReference中错误地修改了BeanDefinition的propertyValues。所以每一个参数、每一个判断都值得深究。3.1doGetBean循环依赖检测的“总开关”protected T T doGetBean( String name, Nullable ClassT requiredType, Nullable Object[] args, boolean typeCheckOnly) throws BeansException { // 1. 转换beanName处理前缀、别名等 String beanName transformedBeanName(name); // 2. 【关键】尝试从一级缓存获取已创建完成的Bean Object sharedInstance getSingleton(beanName); if (sharedInstance ! null args null) { // 3. 如果缓存中有且不是原型模式则进行类型转换和早期引用处理 return (T) getObjectForBeanInstance(sharedInstance, name, beanName, null); } // ... 其他逻辑原型Bean处理、父容器委托等省略 ... // 4. 【核心】标记当前bean正在创建中防止递归调用 beforeSingletonCreation(beanName); try { // 5. 创建Bean实例核心逻辑在createBean中 sharedInstance getSingleton(beanName, () - { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // 清理创建中的标记 afterSingletonCreation(beanName); throw ex; } }); } finally { // 6. 标记创建完成 afterSingletonCreation(beanName); } // 7. 同样进行类型转换和早期引用处理 return (T) getObjectForBeanInstance(sharedInstance, name, beanName, mbd); }这段代码里真正与循环依赖强相关的是第2步和第4步。第2步getSingleton(beanName)是“读”操作它会依次检查一级、二级、三级缓存第4步getSingleton(beanName, lambda)是“写”操作它会先将beanName加入singletonsCurrentlyInCreation集合一个ThreadLocalSetString然后执行lambda创建Bean并在创建成功后将Bean放入一级缓存。最关键的“循环依赖检测”就发生在beforeSingletonCreation方法里protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } }这里singletonsCurrentlyInCreation.add(beanName)返回false就意味着beanName已经存在于当前线程的创建集合中——即A在创建过程中又触发了对A自身的getBean调用比如A的PostConstruct方法里又调用了applicationContext.getBean(A.class)这属于非法的自我引用Spring直接抛出BeanCurrentlyInCreationException。注意这个检查是线程级别的所以不同线程创建同名Bean不会互相干扰这也是Spring能支持高并发的基础。3.2getSingleton三级缓存的“读取引擎”getSingleton(String beanName)方法位于DefaultSingletonBeanRegistry类中是整个循环依赖解决方案的“读取中枢”。它的逻辑非常清晰就是按优先级从高到低依次查找public Object getSingleton(String beanName) { // 1. 先查一级缓存完全初始化好的Bean Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null) { // 2. 一级缓存没找到查二级缓存早期引用代理对象 singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null) { // 3. 二级缓存也没找到查三级缓存ObjectFactory生成器 ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 4. 【关键】调用工厂生成早期引用并放入二级缓存移除三级缓存 singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } return singletonObject; }这里有几个极易被误解的细节必须掰开揉碎earlySingletonObjects和singletonFactories的“互斥性”当singletonFactory.getObject()被调用后生成的singletonObject会立即放入earlySingletonObjects同时singletonFactory从singletonFactories中移除。这意味着同一个Bean的早期引用只会被生成一次。后续所有对该Bean的getBean调用都会直接命中earlySingletonObjects速度极快。这个设计彻底避免了重复代理创建的开销。getObjectForBeanInstance的“双重身份”处理这个方法在第2步和第7步都被调用。它的作用是如果从缓存中拿到的是一个FactoryBean则返回其getObject()的结果如果拿到的是一个普通Bean或代理对象则直接返回。更重要的是它会处理“早期引用”的类型转换。比如A依赖的是BService接口而缓存中存的是BServiceImpl的代理对象getObjectForBeanInstance会确保返回的类型是BService而不是具体的实现类。这保证了依赖注入的“面向接口编程”原则不被破坏。3.3getEarlyBeanReferenceAOP代理的“临界点”如果说getSingleton是循环依赖的“高速公路”那么getEarlyBeanReference就是这条路上最关键的“收费站”——它决定了早期引用到底长什么样。这个方法定义在SmartInstantiationAwareBeanPostProcessor接口中AbstractAutoProxyCreatorSpring AOP的核心类是其最重要的实现。public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { Object cacheKey getCacheKey(bean.getClass(), beanName); // 1. 检查是否已为此Bean生成过代理避免重复 if (!this.earlyProxyReferences.containsKey(cacheKey)) { this.earlyProxyReferences.put(cacheKey, Boolean.TRUE); } // 2. 【核心】调用wrapIfNecessary决定是否需要代理以及如何代理 return wrapIfNecessary(bean, beanName, cacheKey); }wrapIfNecessary方法是真正的“大脑”它会检查当前Bean是否被Transactional、Async等注解标记是否匹配了Pointcut切点是否已经存在代理避免重复代理。如果需要代理它会调用createProxy方法创建一个JdkDynamicAopProxy或CglibAopProxy。这里有一个致命陷阱createProxy方法内部会调用getAdvicesAndAdvisorsForBean而这个方法又会去getBean获取Advisor通知器Bean。如果这个Advisor本身也存在循环依赖就会再次触发doGetBean形成嵌套循环。Spring对此有保护机制它会检查当前Advisor的创建是否会导致无限递归如果会则跳过该Advisor或者抛出明确的异常。这也是为什么在复杂的AOP配置下循环依赖的报错信息往往很长里面嵌套着多层BeanCurrentlyInCreationException。注意getEarlyBeanReference只在singletonFactories被触发时调用一次。一旦代理对象生成并放入earlySingletonObjects后续所有对该Bean的引用都直接使用这个代理对象不会再调用getEarlyBeanReference。所以你的自定义BeanPostProcessor在这个方法里做的任何操作都只会影响“早期引用”的形态不会影响最终的、完全初始化后的Bean。3.4addSingleton生命周期的“最终落定”当一个Bean完全创建、初始化、代理完毕后addSingleton方法会将其“安顿”下来public void addSingleton(String beanName, Object singletonObject) { // 1. 放入一级缓存 this.singletonObjects.put(beanName, singletonObject); // 2. 清空二级缓存早期引用已完成使命 this.earlySingletonObjects.remove(beanName); // 3. 清空三级缓存ObjectFactory已完成使命 this.singletonFactories.remove(beanName); // 4. 记录该Bean已被创建 this.registeredSingletons.add(beanName); }这个方法看似简单却是整个循环依赖闭环的终点。它确保了一旦一个Bean被标记为“已创建”它就永远只存在于一级缓存中成为其他Bean可以安全依赖的“最终态”。而二级和三级缓存则被彻底清空为下一个循环依赖周期腾出空间。这种“用完即焚”的设计保证了缓存的纯净性和高效性。4. 实操过程与核心环节实现手写一个最小化Demo亲眼见证三级缓存如何工作光说不练假把式。为了让你真正吃透三级缓存的运作机制我设计了一个极简但功能完整的Demo它不依赖Spring Boot只用Spring Framework的核心jar包spring-beans-5.3.37.jar并且所有关键步骤都添加了System.out.println日志让你能像看直播一样看到每个缓存的读写过程。这个Demo我在某高校的Spring源码课上作为课堂实验学生反馈“比看十遍源码注释都清楚”。4.1 项目结构与依赖新建一个Maven项目pom.xml中只需引入dependency groupIdorg.springframework/groupId artifactIdspring-beans/artifactId version5.3.37/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.37/version /dependency项目结构如下src/ ├── main/ │ ├── java/ │ │ └── com/example/demo/ │ │ ├── AService.java // A依赖B │ │ ├── BService.java // B依赖A │ │ ├── MyBeanPostProcessor.java // 自定义BP打印缓存操作 │ │ └── DemoApplication.java │ └── resources/ │ └── beans.xml // XML配置显式声明循环依赖4.2 核心Bean定义AService.javapublic class AService { private BService bService; public AService() { System.out.println([AService] 构造函数执行); } // 这个setter会被Spring在populateBean阶段调用 public void setBService(BService bService) { System.out.println([AService] 开始注入BService当前BService引用 bService); this.bService bService; System.out.println([AService] 注入BService完成bService bService); } public void doSomething() { System.out.println([AService] doSomething, 调用BService方法: bService.sayHello()); } }BService.javapublic class BService { private AService aService; public BService() { System.out.println([BService] 构造函数执行); } public void setAService(AService aService) { System.out.println([BService] 开始注入AService当前AService引用 aService); this.aService aService; System.out.println([BService] 注入AService完成aService aService); } public String sayHello() { return Hello from BService; } }4.3 自定义BeanPostProcessor窥探缓存的“眼睛”MyBeanPostProcessor.java是本Demo的灵魂它继承InstantiationAwareBeanPostProcessorAdapter并重写了getEarlyBeanReference和postProcessAfterInitialization用于打印缓存操作public class MyBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter { Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { System.out.println(【BP】getEarlyBeanReference 被调用beanName beanName , bean bean); // 打印当前三级缓存状态 printCacheStatus(); return bean; // 这里不创建代理保持原样便于观察 } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println(【BP】postProcessAfterInitialization 被调用beanName beanName , bean bean); printCacheStatus(); return bean; } private void printCacheStatus() { // 由于DefaultSingletonBeanRegistry是包私有的我们用反射获取其缓存 try { Field singletonObjectsField DefaultSingletonBeanRegistry.class.getDeclaredField(singletonObjects); singletonObjectsField.setAccessible(true); Field earlySingletonObjectsField DefaultSingletonBeanRegistry.class.getDeclaredField(earlySingletonObjects); earlySingletonObjectsField.setAccessible(true); Field singletonFactoriesField DefaultSingletonBeanRegistry.class.getDeclaredField(singletonFactories); singletonFactoriesField.setAccessible(true); DefaultListableBeanFactory factory new DefaultListableBeanFactory(); // 这里简化实际应从ApplicationContext获取但为演示我们手动构建 // 在真实环境中你可以通过ApplicationContext.getBeanFactory()获取 // 为了演示我们模拟一个简单的缓存状态打印 System.out.println(【缓存状态】一级缓存(singletonObjects): (singletonObjectsField.get(null) ! null ? 有值 : 空)); System.out.println(【缓存状态】二级缓存(earlySingletonObjects): (earlySingletonObjectsField.get(null) ! null ? 有值 : 空)); System.out.println(【缓存状态】三级缓存(singletonFactories): (singletonFactoriesField.get(null) ! null ? 有值 : 空)); } catch (Exception e) { e.printStackTrace(); } } }实际项目中printCacheStatus的实现会更复杂需要通过ApplicationContext获取DefaultListableBeanFactory再通过反射访问其父类DefaultSingletonBeanRegistry的私有字段。这里做了简化重点在于理解日志输出的含义。4.4 XML配置与启动类beans.xml?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd !-- 声明Bean -- bean idaService classcom.example.demo.AService scopesingleton property namebService refbService/ /bean bean idbService classcom.example.demo.BService scopesingleton property nameaService refaService/ /bean !-- 注册自定义BeanPostProcessor -- bean classcom.example.demo.MyBeanPostProcessor/ /beansDemoApplication.javapublic class DemoApplication { public static void main(String[] args) { System.out.println( Spring容器启动开始 ); ApplicationContext context new ClassPathXmlApplicationContext(beans.xml); System.out.println( Spring容器启动完成 ); AService aService context.getBean(aService, AService.class); aService.doSomething(); System.out.println( 程序执行结束 ); } }4.5 运行日志与逐行解读运行DemoApplication控制台会输出大量日志。我们截取最关键的部分进行逐行解读 Spring容器启动开始 [AService] 构造函数执行 【BP】getEarlyBeanReference 被调用beanNameaService, beancom.example.demo.AService12345678 【缓存状态】一级缓存(singletonObjects): 空 【缓存状态】二级缓存(earlySingletonObjects): 空 【缓存状态】三级缓存(singletonFactories): 有值 -- 此时aService的ObjectFactory已注册到三级缓存 [BService] 构造函数执行 【BP】getEarlyBeanReference 被调用beanNamebService, beancom.example.demo.BService87654321 【缓存状态】一级缓存(singletonObjects): 空 【缓存状态】二级缓存(earlySingletonObjects): 空 【缓存状态】三级缓存(singletonFactories): 有值 -- 此时bService的ObjectFactory也已注册 [AService] 开始注入BService当前BService引用null 【BP】getEarlyBeanReference 被调用beanNamebService, beancom.example.demo.BService87654321 【缓存状态】一级缓存(singletonObjects): 空 【缓存状态】二级缓存(earlySingletonObjects): 有值 -- 关键bService的早期引用已生成并放入二级缓存 【缓存状态】三级缓存(singletonFactories): 空 -- 关键bService的ObjectFactory已被移除 [AService] 注入BService完成bServicecom.example.demo.BService87654321 [BService] 开始注入AService当前AService引用com.example.demo.AService12345678 [BService] 注入AService完成aServicecom.example.demo.AService12345678 【BP】postProcessAfterInitialization 被调用beanNameaService, beancom.example.demo.AService12345678 【BP】postProcessAfterInitialization 被调用beanNamebService, beancom.example.demo.BService87654321 Spring容器启动完成 [AService] doSomething, 调用BService方法: Hello from BService 程序执行结束 关键日志解读第一次getEarlyBeanReference(aService)此时aService刚构造完正要进入属性注入它的ObjectFactory被放入三级缓存。第二次getEarlyBeanReference(bService)同理bService的ObjectFactory也被放入三级缓存。第三次getEarlyBeanReference(bService)这是aService在注入bService时触发的。此时bService的ObjectFactory被调用生成早期引用并立即放入二级缓存同时从三级缓存移除。这就是“三级变二级”的瞬间。AService注入BService时拿到的不再是null而是那个刚刚生成的、存于二级缓存中的BService实例。后续BService注入AService时aService的ObjectFactory同样被调用生成早期引用放入二级缓存。这个Demo清晰地证明了循环依赖的解决本质上是一场“时间差”的艺术。Spring利用Bean创建过程中的“空档期”在对象完全初始化前就将其“快照”早期引用提供给依赖方从而打破了僵局。而三级缓存就是管理这场“时间差”的精密调度系统。5. 常见问题与排查技巧实录线上故障的“X光片”怎么拍在真实的生产环境中“循环依赖”从来不是教科书上那个优雅的A-B-A模型。它往往披着各种伪装藏在复杂的配置、动态代理、条件化Bean等高级特性之下。我整理了一份基于某金融核心系统三年运维经验的《循环依赖故障速查表》里面记录的全是我和团队亲手踩过的坑以及对应的“X光片”拍摄技巧即精准的调试和日志方法。5.1 问题速查表症状、根因与“X光片”拍摄指南故障症状可能根因“X光片”拍摄指南精准定位步骤我的实操心得BeanCurrentlyInCreationException: Error creating bean with name xxx堆栈中反复出现doGetBean和getSingleton1.非法自我引用Bean的PostConstruct方法中调用了applicationContext.getBean(this.getClass())。2.条件化Bean冲突ConditionalOnMissingBean和ConditionalOnBean在循环依赖链中产生矛盾判断。1. 在beforeSingletonCreation方法的第一行打条件断点beanName.equals(xxx)。2. 观察singletonsCurrentlyInCreation集合的内容看里面是否已经包含了xxx。3. 查看堆栈找到是哪个方法通常是PostConstruct或InitializingBean.afterPropertiesSet触发了这次getBean。别急着改代码先确认是不是PostConstruct里调用了getBean。如果是99%的解决方案是把这个逻辑移到一个独立的ApplicationRunner中或者用ObjectProvider懒加载。PostConstruct的语义是“初始化完成”它不应该再去触发新的Bean创建。应用启动时卡住CPU飙升日志停留在Creating shared instance of singleton bean xxx无限递归创建A依赖BB依赖CC又依赖A形成了长度大于2的循环链且链中某个Bean的ObjectFactory.getObject()方法内部又触发了新的getBean调用比如自定义BP中错误地调用了context.getBean()。1. 使用jstack -l pid获取线程堆栈搜索doGetBean和getSingleton关键字。2. 找到最深的调用链通常能看到类似A-B-C-A的循环。3. 在getSingleton(String, ObjectFactory)的lambda参数内部打断点观察每次调用时beanName的值。这种问题最难排查。我的经验是在getSingleton方法的入口处添加一行日志System.out.println(getSingleton for: beanName , stack: Arrays.toString(Thread.currentThread().getStackTrace()))。启动时日志会疯狂刷屏但你能从中清晰地看到循环链。找到后立刻在链中最后一个Bean的ObjectFactory里加return null强制中断再逐步放开。NullPointerException堆栈指向某个被注入的字段但该字段在PostConstruct中明明已赋值早期引用与最终Bean不一致A依赖BB是一个Scope(prototype)的Bean而Spring的循环依赖只支持singleton。此时A注入的B是早期引用可能是null或一个临时对象而B的最终实例是另一个对象。1. 检查报错Bean的Scope注解确认是否为singleton。2. 在doGetBean方法中添加日志if (args ! null) System.out.println(Prototype bean requested: beanName);。3. 观察日志确认是否在循环依赖链中出现了prototypeBean。这是Spring文档里明确写的限制但很多人会忽略。记住铁律Spring的循环依赖解决方案仅对singleton作用域有效。如果你的架构中必须有原型Bean参与循环唯一的办法是用ObjectProviderT代替直接注入或者重构代码将原型Bean的创建逻辑移到一个FactoryBean中。AopProxy相关异常如ClassCastException提示无法将Bean转换为某个接口AOP代理时机错乱getEarlyBeanReference生成的早期代理与postProcessAfterInitialization生成的最终代理类型不一致。常见于自定义BeanPostProcessor在两个方法中做了不同的代理逻辑。1. 在getEarlyBeanReference和postProcessAfterInitialization两个方法中都打印bean.getClass().getName()和bean.getClass().getInterfaces()。2. 对比两者输出看接口列表是否一致。3. 检查自定义BP的getEarlyBeanReference是否返回了CglibAopProxy而postProcessAfterInitialization返回了JdkDynamicAopProxy或反之。这个坑我踩过两次。根源在于getEarlyBeanReference应该只做“最小必要代理”比如