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依赖B,B又依赖A,当Spring开始创建A时,它必须立刻决定:B这个Bean,是现在就创建好给我用,还是等我创建完A再回头去创建B?前者会导致B的创建过程被强行插入到A的创建流程中,破坏A的postProcessBeforeInitialization等回调时机;后者则会让A在注入B时拿到null,直接抛异常。所以必须有一个“占位符”机制:在A刚开始创建、还没走到属性注入阶段时,就向容器注册一个“B正在创建中”的状态,并承诺“稍后我会给你一个B的实例”。这个占位符,就是一级缓存singletonObjects(ConcurrentHashMap<String, Object>)的雏形,但它不能直接存“正在创建的B”,因为此时B连构造函数都没执行完,更别说初始化了。
提示:很多人误以为一级缓存存的就是最终Bean,其实不然。
singletonObjects只存完全初始化完毕、可直接使用的Bean实例。它解决的是“已创建完成Bean的快速获取”,而非“正在创建Bean的协调”。
2.2 约束二:AOP代理必须“无感”织入,不能让业务代码感知代理的存在
这是最容易被忽略,却最关键的一点。假设没有AOP,A和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。
这就是二级缓存earlySingletonObjects(ConcurrentHashMap<String, Object>)存在的根本原因:它专门存放那些“已经创建了原始对象、但尚未完成初始化和代理”的Bean的早期引用。注意,这里的“早期引用”不是原始对象,而是ObjectFactory返回的对象——而ObjectFactory的实现,正是那个关键的getEarlyBeanReference方法,它会调用SmartInstantiationAwareBeanPostProcessor(如AbstractAutoProxyCreator)来生成代理。所以二级缓存的本质,是代理对象的临时仓库。
2.3 约束三:线程安全与性能必须兼顾,不能用全局锁“一锁了之”
想象一下,如果只用一级缓存,每次getBean("B")都要检查“B是否在创建中”,并加锁等待。在高并发场景下,比如100个请求同时要创建A,它们都会卡在获取B的锁上,系统吞吐量直接归零。Spring的解法极其精妙:它用三级缓存singletonFactories(ConcurrentHashMap<String, ObjectFactory<?>>)来实现“无锁化”的早期引用生成。singletonFactories里存的不是对象,而是一个ObjectFactory——一个lambda表达式或匿名内部类,它的getObject()方法会在第一次被调用时,才去执行真正的代理创建逻辑。这样,只有第一个真正需要B的早期引用的线程(比如A的属性注入),才会触发ObjectFactory.getObject(),从而调用getEarlyBeanReference生成代理;后续所有线程,都直接从二级缓存earlySingletonObjects里拿现成的代理,完全不涉及锁竞争。这是一种典型的“延迟计算+结果缓存”模式,既保证了线程安全,又将昂贵的代理创建操作降到了最低频次。
2.4 约束四:生命周期回调必须严格按序执行,不能为了循环依赖牺牲规范性
Spring的Bean生命周期有明确定义:Constructor → populateBean(属性注入)→ initializeBean(init-method/afterPropertiesSet)→ postProcessAfterInitialization(AOP代理)。循环依赖的解决方案,绝不能打乱这个顺序。比如,不能为了让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 Class<T> 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集合(一个ThreadLocal<Set<String>>),然后执行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.3getEarlyBeanReference:AOP代理的“临界点”
如果说getSingleton是循环依赖的“高速公路”,那么getEarlyBeanReference就是这条路上最关键的“收费站”——它决定了早期引用到底长什么样。这个方法定义在SmartInstantiationAwareBeanPostProcessor接口中,AbstractAutoProxyCreator(Spring 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> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.37</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.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.java:
public 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.java:
public 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 version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 声明Bean --> <bean id="aService" class="com.example.demo.AService" scope="singleton"> <property name="bService" ref="bService"/> </bean> <bean id="bService" class="com.example.demo.BService" scope="singleton"> <property name="aService" ref="aService"/> </bean> <!-- 注册自定义BeanPostProcessor --> <bean class="com.example.demo.MyBeanPostProcessor"/> </beans>DemoApplication.java:
public 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 被调用,beanName=aService, bean=com.example.demo.AService@12345678 【缓存状态】一级缓存(singletonObjects): 空 【缓存状态】二级缓存(earlySingletonObjects): 空 【缓存状态】三级缓存(singletonFactories): 有值 <-- 此时aService的ObjectFactory已注册到三级缓存 [BService] 构造函数执行 【BP】getEarlyBeanReference 被调用,beanName=bService, bean=com.example.demo.BService@87654321 【缓存状态】一级缓存(singletonObjects): 空 【缓存状态】二级缓存(earlySingletonObjects): 空 【缓存状态】三级缓存(singletonFactories): 有值 <-- 此时bService的ObjectFactory也已注册 [AService] 开始注入BService,当前BService引用:null 【BP】getEarlyBeanReference 被调用,beanName=bService, bean=com.example.demo.BService@87654321 【缓存状态】一级缓存(singletonObjects): 空 【缓存状态】二级缓存(earlySingletonObjects): 有值 <-- 关键!bService的早期引用已生成并放入二级缓存 【缓存状态】三级缓存(singletonFactories): 空 <-- 关键!bService的ObjectFactory已被移除 [AService] 注入BService完成,bService=com.example.demo.BService@87654321 [BService] 开始注入AService,当前AService引用:com.example.demo.AService@12345678 [BService] 注入AService完成,aService=com.example.demo.AService@12345678 【BP】postProcessAfterInitialization 被调用,beanName=aService, bean=com.example.demo.AService@12345678 【BP】postProcessAfterInitialization 被调用,beanName=bService, bean=com.example.demo.BService@87654321 === 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和getSingleton | 1.非法自我引用: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依赖B,B依赖C,C又依赖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依赖B,B是一个@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参与循环,唯一的办法是:用ObjectProvider<T>代替直接注入,或者重构代码,将原型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应该只做“最小必要代理”,比如 |