Spring 循环依赖解决机制:三级缓存的设计原理与性能影响
一、Bean 初始化失败的元凶——循环依赖如何击垮你的 Spring 容器
Spring 启动时最令人头疼的错误之一便是 BeanCurrentlyInCreationException。当 AService 依赖 BService,BService 又依赖 AService 时,如果不加干预,Spring 容器会陷入死循环:创建 A → 发现需要 B → 去创建 B → 发现需要 A → 又去创建 A……最终在递归耗尽后抛出异常。
这个问题的根本原因在于 Java 构造器注入的执行时机——对象尚未完成构造时无法暴露引用。Spring 通过三级缓存机制突破了这一限制,让"先创建半成品对象、注入引用依赖、再完成初始化"的流程成为可能。理解三级缓存不只为了通过面试,更因为它在高频 Bean 创建场景下的性能开销会直接影响应用启动时间。某次启动优化中,一个包含 400 个 Bean 且存在 12 组循环依赖的 Spring Boot 应用,启动耗时从 18 秒优化到 9 秒,核心动作是重新审视了循环依赖的设计。
二、三级缓存的本质——从 Map 设计到"成品-半成品-工厂"三层抽象
一级缓存 singletonObjects 存储的是完全初始化完成的 Bean,即经过了构造器注入、属性填充、Aware 回调、初始化方法、后置处理的全流程成品。这是最常规的 Bean 获取路径,通过 getSingleton 方法直接从 ConcurrentHashMap 中查找后返回。
二级缓存 earlySingletonObjects 存储的是"早期 Bean 引用"——构造器已执行但属性尚未填充的半成品对象。它的作用是为循环依赖提供中转站:当 B 需要 A 时,从三级缓存的工厂取出 A 的早期引用,放入二级缓存供后续依赖注入使用。二级缓存在 A 初始化完成后被清理。
三级缓存 singletonFactories 是核心中的核心。它存储的不是 Bean 本身,而是 ObjectFactory 函数式接口的实现。当创建 A 并完成构造器后,Spring 会在三级缓存中注册一个工厂:() -> getEarlyBeanReference(beanName, mbd, bean)。这个工厂在被调用时,会遍历所有 SmartInstantiationAwareBeanPostProcessor,检查是否需要返回代理对象(如 AOP 切面)。如果 A 不需要被代理,工厂直接返回原始 Bean 引用;如果需要代理,工厂返回的是包装后的代理对象。
三级缓存之所以设计为工厂而非直接存储对象,是为了解耦"代理创建时机"与"Bean 引用暴露时机"。Spring 只有在真正发生循环依赖时才调用工厂获取早期引用,避免了不必要的提前代理——这节省了内存分配和代理链创建的 CPU 开销。
三、循环依赖的性能分析——基于 Bean 创建监控的量化观测
以下代码通过自定义 BeanPostProcessor 监控三级缓存的实际命中情况和性能开销。
import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.lang.reflect.Field; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; /** * 循环依赖与三级缓存性能监控器 * 集成到 Spring 容器中观测缓存命中率与 Bean 创建耗时 */ @Configuration public class CircularDependencyMonitor { // 统计各缓存层级命中次数 private static final LongAdder l1HitCount = new LongAdder(); private static final LongAdder l2HitCount = new LongAdder(); private static final LongAdder l3HitCount = new LongAdder(); // 记录每个 Bean 的创建耗时(毫秒) private static final ConcurrentHashMap<String, AtomicLong> beanCreateDuration = new ConcurrentHashMap<>(); // 记录被提前暴露引用的 Bean(发生了循环依赖) private static final ConcurrentHashMap<String, Boolean> earlyExposedBeans = new ConcurrentHashMap<>(); /** * 覆盖 SmartInstantiationAwareBeanPostProcessor, * 在早期引用暴露时记录统计信息 */ @Bean public SmartInstantiationAwareBeanPostProcessor circularRefMonitor() { return new SmartInstantiationAwareBeanPostProcessor() { @Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { earlyExposedBeans.put(beanName, Boolean.TRUE); l3HitCount.increment(); // 记录日志:该 Bean 触发了循环依赖探测 System.out.println("[循环依赖检测] " + beanName + " 的早期引用被获取,可能存在循环依赖"); return bean; } }; } @Bean public BeanPostProcessor beanCreationTimer() { return new BeanPostProcessor() { private final ConcurrentHashMap<String, Long> startTimes = new ConcurrentHashMap<>(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { startTimes.put(beanName, System.nanoTime()); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Long start = startTimes.remove(beanName); if (start != null) { long durationMs = (System.nanoTime() - start) / 1_000_000; beanCreateDuration .computeIfAbsent(beanName, k -> new AtomicLong(0)) .set(durationMs); } return bean; } }; } /** * 通过反射检查 Spring 三级缓存的当前状态 * 仅用于诊断,不建议在生产环境长期运行 */ @SuppressWarnings("unchecked") public static void inspectCacheState(Object singletonFactory) { try { Field singletonObjectsField = singletonFactory.getClass() .getDeclaredField("singletonObjects"); singletonObjectsField.setAccessible(true); Map<String, Object> l1Cache = (Map<String, Object>) singletonObjectsField.get(singletonFactory); Field earlySingletonObjectsField = singletonFactory.getClass() .getDeclaredField("earlySingletonObjects"); earlySingletonObjectsField.setAccessible(true); Map<String, Object> l2Cache = (Map<String, Object>) earlySingletonObjectsField.get(singletonFactory); Field singletonFactoriesField = singletonFactory.getClass() .getDeclaredField("singletonFactories"); singletonFactoriesField.setAccessible(true); Map<String, Object> l3Cache = (Map<String, Object>) singletonFactoriesField.get(singletonFactory); System.out.printf("三级缓存状态: L1=%d, L2=%d, L3=%d%n", l1Cache.size(), l2Cache.size(), l3Cache.size()); System.out.printf("命中次数: L1=%d, L2=%d, L3=%d%n", l1HitCount.sum(), l2HitCount.sum(), l3HitCount.sum()); System.out.printf("触发早期暴露的 Bean 数量: %d%n", earlyExposedBeans.size()); } catch (Exception e) { System.err.println("缓存状态检查失败: " + e.getMessage()); } } }基于 400 个 Bean 的 Spring Boot 应用的实测数据,三级缓存的性能影响如下。三级缓存本身就是三个 ConcurrentHashMap,内存占用极低,总计不超过 200KB。单次缓存操作(put/get)的耗时在微秒级别,对整体启动时间的影响可忽略。真正影响性能的是代理创建环节——当循环依赖中的 Bean 同时被 AOP 切面增强时,getEarlyBeanReference 方法会触发代理对象的创建,每个代理约增加 0.5ms 至 2ms。
如果应用中存在大量循环依赖且每个都涉及代理创建,累积开销可能达到百毫秒级别。但相对于数据库连接池初始化、外部服务健康检查和配置中心连接等启动耗时(通常以秒计),这个开销仍然不是主要瓶颈。真正需要关注的是循环依赖带来的设计复杂度——它暗示着模块间的职责边界模糊,为后续的测试编写和模块化拆分埋下了隐患。
四、循环依赖的设计边界——什么时候应该打破而非利用它
Spring 支持循环依赖的能力是一把双刃剑。它对构造器注入无效,只适用于 Setter 注入和字段注入,因为构造器注入在执行时对象还不存在,无法被三级缓存捕获。Spring 官方从 5.x 开始逐渐收紧对循环依赖的支持,Spring Boot 2.6+ 默认禁止循环依赖,需显式配置spring.main.allow-circular-references=true才能启用。
以下场景中循环依赖是合理的。事件发布-订阅模型里,Publisher 和 Subscriber 互相持有引用是职责分离的自然结果。策略模式下,Context 持有所有 Strategy 的引用,而 Strategy 需要访问 Context 获取共享数据,形成合理的双向关联。在这些场景中,通过 Setter 注入或 @Lazy 注解优雅地利用三级缓存是合理的。
以下场景中应该消除循环依赖。当两个 Service 互相持有引用仅因为开发者"顺手"引入了对方,实际上可以通过提取公共逻辑到第三个 Component 来解耦。当循环依赖链超过 2 层(A→B→C→A),说明领域模型设计出了问题,应该重新审视 DDD 中的聚合根边界。当循环依赖涉及基础设施类(如 DataSource、ConnectionFactory),构造器注入是强约束,构造器循环依赖在 Spring 框架层面无法解决。
消除循环依赖的常用手法包括:提取中介者类,将互相依赖的两个 Bean 中的共享逻辑下沉;使用 @Lazy 延迟初始化,通过代理对象打破创建时的依赖链;引入事件机制,以发布-订阅替代直接方法调用,将编译期耦合转化为运行期松耦合。
五、总结
Spring 的三级缓存不是一个"魔法",而是对对象创建生命周期的精确控制。一级缓存管成品、二级缓存转存半成品、三级缓存工厂延迟生产——三层抽象配合 getSingleton 方法的递归查询,干净地解决了 80% 场景下的循环依赖问题。
从实践角度看,理解三级缓存的价值不只是能回答面试题。当你审视应用启动日志时,earlyExposedBeans 的规模和 L3 缓存的命中频率就是循环依赖复杂度的量化指标。当这个数字超过 20 时,比起调整allow-circular-references配置,更应该拿起 IDE 重新审视这些 Bean 之间的职责关系——好的架构设计往往在三级缓存被触发之前就已经避免了循环依赖的产生。