spring如何解决循环依赖:三级缓存机制
spring如何解决循环依赖,核心依靠三级缓存提前暴露半成品Bean,仅支持单例、setter注入场景,原型Bean、构造器注入、@Lazy未正确配置的单例Bean无法使用该方案,你可通过调整注入方式、使用@Lazy注解、重构代码拆分Bean职责三种手段处理循环依赖,三级缓存分别存放已实例化未填充属性的半成品Bean、Bean工厂对象、完成完整初始化的成品Bean,Bean创建时先放入一级缓存,填充属性阶段若检测到循环引用,就从二级缓存获取半成品对象,避免重复创建,BeanPostProcessor处理AOP代理对象时会借助三级缓存的ObjectFactory生成代理,防止注入原始裸对象。
spring循环依赖三级缓存结构
三级缓存是三个ConcurrentHashMap,各自存储不同状态的Bean实例,一级缓存singletonObjects保存完全初始化完毕的单例Bean,Bean创建完成后才存入;二级缓存earlySingletonObjects存放提前暴露的半成品Bean实例,已经完成实例化、属性还没填充;三级缓存singletonFactories存储Bean工厂ObjectFactory,用于生成Bean实例或者AOP代理对象。Bean开始实例化后,会向三级缓存存入ObjectFactory,属性填充阶段如果别的Bean需要引用当前对象,就调用ObjectFactory获取实例,放到二级缓存,之后直接复用二级缓存对象,不再重复调用工厂方法。
三级缓存的关键作用是支撑AOP代理。如果Bean需要动态代理,提前暴露阶段不能直接放原始对象,通过ObjectFactory延迟生成代理,保证循环依赖注入拿到的是代理对象,不是未增强的原始实例。只保留两级缓存可以解决普通非AOP场景循环依赖,但遇到需要AOP的Bean,会注入原始对象,引发业务逻辑异常。
spring循环依赖可行方案对比
| 解决方案 | 适用场景 | 开发成本 | 潜在问题 |
|---|---|---|---|
| setter注入+三级缓存 | 单例Bean,非构造注入,存在AOP | 低 | 构造注入场景失效 |
| @Lazy延迟注入 | 构造器注入场景,不想改动原有代码 | 低 | 会延迟依赖对象初始化时机 |
| 代码重构拆分Bean | 长期项目,循环依赖属于设计缺陷 | 高 | 改动量大,需要调整业务职责 |
| 原型Bean(不推荐) | 无合适场景,仅作了解 | 低 | spring原生不处理原型循环依赖,直接抛出异常 |
spring循环依赖无法处理的边界场景
原型作用域Bean每次获取都会新建实例,spring不会缓存任何半成品对象,检测到循环依赖直接抛出BeanCurrentlyInCreationException。构造器注入场景下,Bean实例化前就需要传入依赖对象,此时对象还没创建,无法提前暴露半成品实例,三级缓存机制无法生效。单例Bean配合@DependsOn注解强制Bean创建顺序,两个Bean互相设置依赖顺序,也会触发循环依赖报错。
spring5.3版本官方文档明确说明,构造注入产生的循环依赖不属于框架可自动处理范围,需要开发者干预。
这是代码设计层面的问题。
你排查循环依赖异常时,优先看报错堆栈的BeanCurrentlyInCreationException,确认Bean作用域和注入方式,先判断是否属于三级缓存支持范围。如果是setter注入单例Bean出现该异常,大概率是AOP代理逻辑自定义BeanPostProcessor破坏了提前暴露的对象引用;如果是构造注入,直接切换@Lazy或者重构类结构。
使用@Lazy注解时,要把注解加到依赖参数上,不能只加到类上。注解加到参数上,注入的是代理占位对象,在第一次调用方法的时候才会去容器获取真实Bean实例,打断Bean创建时的循环等待链。只在类上加@Lazy,仅延迟当前Bean的创建时机,无法解决构造注入互相引用的循环依赖。
重构方案优先提取公共业务到第三个独立Bean,两个互相引用的Bean同时依赖新Bean,消除双向引用。该方案可以从根源消除循环依赖,降低后续维护成本,在中大型业务项目中优先选择。
