news 2026/10/15 15:38:35

Spring循环依赖原理:三级缓存如何保障AOP与线程安全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring循环依赖原理:三级缓存如何保障AOP与线程安全

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和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依赖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应该只做“最小必要代理”,比如
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 15:38:34

EnterpriseArchitect破解版风险与合规建模成本优化指南

简介&#xff1a;这份资源面向软件架构师、系统分析师及UML建模学习者&#xff0c;提供EnterpriseArchitect的安装与使用支持&#xff0c;帮助解决UML2.1建模、代码工程双向转换、项目管理、文档生成及数据库建模等需求。压缩包共5个文件&#xff0c;以msi安装程序为主&#xf…

作者头像 李华
网站建设 2026/10/15 15:37:51

vDisk一键部署自动化脚本:从设计思路到避坑指南全解析

干这行时间长了&#xff0c;你会发现一件事&#xff1a;很多听起来很高大上的问题&#xff0c;本质上都是重复性体力劳动。vDisk部署就是典型例子——不管你用的是哪家虚拟化平台&#xff0c;还是自己搭的一套东西&#xff0c;虚拟磁盘的创建、分区、格式化、挂载、甚至写引导&…

作者头像 李华
网站建设 2026/10/15 15:37:29

Playwright 元素定位与等待机制:小白也能看懂的稳定自动化指南

目录Playwright 元素定位与等待机制&#xff1a;小白也能看懂的稳定自动化指南一、先理解&#xff1a;Playwright 到底在做什么二、准备一个最小运行环境三、定位元素的核心原则&#xff1a;优先描述“用户看到的东西”1. 用角色和名称定位2. 用标签定位输入框3. 用 placeholde…

作者头像 李华
网站建设 2026/10/15 15:37:20

微信小程序对接团购核销:别手写对接了,无限聚核这组 AI 提示词教大模型接美团、抖音核销

核销服务由无限聚核提供&#xff0c;美团、抖音团购核销一站式接入。要把美团、抖音团购核销接到微信小程序或行业软件服务端&#xff0c;很多人仍在「对着文档抄路径、猜字段、改一版挂一版」。无限聚核把同一套接入纪律写成了文档站上的「AI 接入」提示词&#xff1a;你把流程…

作者头像 李华