Spring如何解决循环依赖问题:三级缓存机制
Spring如何解决循环依赖问题,核心依靠三级缓存(singletonObjects、earlySingletonObjects、singletonFactories)对单例Bean的提前暴露,仅适用于默认作用域singleton、通过setter注入或者字段注入的场景,构造器循环依赖无法依靠该机制解决,SpringFramework官方文档《CoreTechnologies》章节明确标注了这个边界。你在排查时要区分单例Bean与prototype原型Bean,原型Bean不会进入三级缓存,出现循环依赖会直接抛出BeanCurrentlyInCreationException异常,这是最容易混淆的判断点。
Spring如何解决循环依赖问题:三级缓存工作流程
一级缓存singletonObjects存放已经完成实例化、属性填充、初始化全部流程的成品单例Bean;二级缓存earlySingletonObjects存放已经实例化但还未完成属性填充的早期Bean对象;三级缓存singletonFactories是ObjectFactory对象工厂,作用是生成Bean的早期引用,如果Bean需要AOP代理,这个工厂会负责生成代理对象,避免后续重复创建代理实例。
以A依赖B、B依赖A的setter循环依赖为例,实例化A的时候,先把A的ObjectFactory放入三级缓存,接着执行A的属性填充,此时发现需要注入B,于是开始实例化B,B填充属性时又需要注入A,此时就从三级缓存拿到A的工厂,调用工厂得到A的早期引用,把这个早期对象放入二级缓存,再从三级缓存移除该工厂,B拿到A引用之后完成自身属性填充与初始化,成品B写入一级缓存,回到A的流程,A注入已经完成的B,继续完成初始化,最后A成品写入一级缓存,整个循环依赖完成解析。
三级缓存的设计并不是单纯存对象,核心价值是支持AOP代理对象的统一生成。如果只有二级缓存,没有三级缓存,在Bean不需要代理时看似可以跑通,但一旦Bean被切面增强,就会出现原始对象和代理对象不一致的问题,三级缓存通过ObjectFactory延迟决定是否生成代理,保证注入到其他Bean里面的引用和最终初始化完成的Bean是同一个对象。
Spring如何解决循环依赖问题:不同注入方式对比
构造器注入循环依赖是Spring三级缓存机制无法处理的类型,因为Bean还没有完成实例化,就需要在构造函数里拿到另一个还在创建中的Bean,此时没有办法把早期实例放入缓存,启动阶段就会直接报错。你如果使用@RequiredArgsConstructor这类Lombok注解实现全参构造注入,很容易触发这类启动异常。
| 注入方式 | 三级缓存是否支持 | 报错时机 |
|---|---|---|
| setter注入 | 支持 | 无,可正常启动 |
| 字段@Autowired注入 | 支持 | 无,可正常启动 |
| 构造器注入 | 不支持 | 容器启动阶段抛出异常 |
该机制的适用边界是单例Bean。
除了依靠三级缓存自动解析,你还可以主动改造代码规避循环依赖,常用手段包括抽取公共逻辑到独立Bean、使用@Lazy延迟注入,@Lazy会注入代理对象,推迟真实Bean初始化时机,打断创建链路,这种方案不依赖三级缓存,适合构造器循环依赖场景。你在改造时要评估业务耦合度,单纯加@Lazy只是延迟加载,并没有消除代码层面的循环耦合,后续维护依然要理清业务依赖关系。
Spring6.x版本延续这套三级缓存处理逻辑,但是官方更推荐开发者主动优化代码结构,减少循环依赖,而不是长期依赖容器自动兜底。你可以开启启动日志,观察Bean创建顺序,定位循环依赖的两个Bean,确认是否属于业务设计缺陷,若属于模块之间不合理互相调用,优先拆分模块,而不是持续使用自动解析方案掩盖设计问题。
如果你使用SpringBoot,底层依然复用SpringFramework的这套BeanFactory逻辑,不需要额外配置开启三级缓存,默认开启,只有当你手动修改BeanFactory相关配置、关闭允许循环依赖的相关开关时,原本可以自动解析的setter循环依赖才会转为启动报错,可通过spring.main.allow-circular-references配置项控制该行为,SpringBoot2.6之后该配置默认值为false,这是排查循环依赖启动报错非常关键的配置项。
