通透理清三级缓存--看Spring是如何解决循环依赖的
文章目录
- 循环依赖是什么?
- 三级缓存
- 三级缓存是什么?
- 执行流程
- 疑点|困惑
- 第二级缓存的存在必要性
- 第三级缓存的存在必要性
- 问题一:违反“按需创建”原则 → 性能浪费
- 问题二:破坏“职责分离”与“扩展点机制”
三级缓存机制在 Spring 2.0 时代引入,主要解决两个问题:
- Setter 注入下的循环依赖(让应用能跑起来)
- AOP 代理对象的正确提前注入(让功能不被破坏)
循环依赖是什么?
循环依赖指的是两个类中的属性相互依赖对方:
例如 A 类中有 B 属性,B 类中有 A属性,从而形成了一个依赖闭环,如下图。
代码表现是这样的:
A.java
@Service
public class AService {private BService bService;@Autowiredpublic void setBService(BService bService) {this.bService = bService;}
}B.java
@Service
public class BService {private AService aService;@Autowiredpublic void setAService(AService aService) {this.aService = aService;}
}
应用启动后就会出现:
***************************
APPLICATION FAILED TO START
***************************Description:The dependencies of some of the beans in the application context form a cycle:┌─────┐
| a defined in file [...A.class]
↑ ↓
| b defined in file [...B.class]
└─────┘
三级缓存
Spring通过三级缓存提前暴露未完全初始化的对象引用,解决单例作用域 Bean 的 sette注入方式的循环依赖问题,接下来我将详细讲解三级缓存并分为有AOP和无AOP两种情况进行讨论,注意区分
三级缓存是什么?
三级缓存其实都是 Map类型的缓存
- 一级缓存(Singleton Objects)Map<String,Object>:这是一个Map类型的缓存,存储的是已经完全初始化好的bean,即完全准备好可以使用的bean实例。键是bean的名称,值是bean的实例。这个缓存在
DefaultSingletonBeanRegistry
类中的singletonObjects
属性中。 - 二级缓存(Early Singleton Objects)Map<String,Object>:这同样是一个Map类型的缓存,存储的是早期的bean引用,即已经实例化但还未完全初始化的bean。这些bean已经被实例化,但是还没有进行属性注入等操作。这个缓存在
DefaultSingletonBeanRegistry
类中的earlySingletonObjects
属性中。 - 三级缓存(Singleton Factories)Map<String,ObjectFactory>:这也是一个Map类型的缓存,存储的是ObjectFactory对象,这些对象**可以生成早期的bean引用。**当一个bean正在创建过程中,如果它被其他bean依赖,那么这个正在创建的bean就会通过这个ObjectFactory来创建一个早期引用,从而解决循环依赖的问题。这个缓存在
DefaultSingletonBeanRegistry
类中的singletonFactories
属性中。
缓存级别 | 名称 | 存储内容 | 目的 |
---|---|---|---|
一级缓存 | singletonObjects | 完全初始化后的单例 Bean | 提供给用户使用的最终成品 |
二级缓存 | earlySingletonObjects | 提前暴露的“半成品”Bean(已实例化但未注入依赖) | 解决循环依赖时临时存放 |
三级缓存 | singletonFactories | Bean 的工厂对象(ObjectFactory ) | 用于生成早期引用(解决 AOP 代理问题) |
执行流程
一个Bean的创建流程可以分为以下五步
以这段代码为例 AB循环依赖
@Component
public class A{@Autowiredprivate B b;
}@Component
public class B{@Autowiredprivate A a
}
- 第一步:获取A对象 准备在缓存中获取A对象,分别在一级、二级、三级缓存中查找 如果找到就返回
- 第二步:创建A对象 一开始缓存中并没有A对象,因此直接进行下一步–创建对象,这一步通过反射创建A对象并注册为
ObjectFactory
,放入三级缓存中 ,这个工厂的使命是:当其他Bean需要引用BeanA
时,它能动态返回当前这个半成品的BeanA
(可能是原始对象,也可能是为应对AOP而提前生成的代理对象)此时BeanA的状态为已实例化但是还没注入属性也没初始化
- 第三步:填充A属性 在这一步A在填充过程中发现需要注入B对象,进入B的实例化过程中,该过程与上述第一、第二步相同,在创建B对象时将B对象注册为
ObjectFactory
并放入三级缓存中,此时三级缓存中同时存在BeanA
和BeanB
的工厂
-
第四步: 填充B属性 在填充B属性的过程中发现需要注入A对象,这时候重复第一步在缓存中查询A对象,这时候在三级缓存中可以找到了
- 在一级缓存(存放完整Bean)中未找到
BeanA
; - 在二级缓存(存放已暴露的早期引用)中同样未命中;
- 最终在三级缓存中定位到
BeanA
的工厂。
Spring立即调用该工厂的
getObject()
方法。这个方法会执行关键决策:若BeanA
需要AOP代理,则动态生成代理对象(即使BeanA
还未初始化);若无需代理,则直接返回原始对象。得到的这个早期引用(可能是代理)被放入二级缓存(earlySingletonObjects
),同时从三级缓存清理工厂条目。 - 在一级缓存(存放完整Bean)中未找到
-
第五步:B执行初始化方法与缓存转移 执行
@PostConstruct
、afterPropertiesSet()
等初始化方法将其转化为完整可用的Bean。随后,BeanB
被提升至一级缓存(singletonObjects
),二级和三级缓存中关于BeanB
的临时条目均被清除。此时BeanB
已准备就绪,可被其他对象使用
-
第六步:回溯A的执行 与上面第五步相同,A接着执行初始化方法与缓存转移,值得注意的是:如果A是一个需要代理的对象,会从二级缓存中获取此前存放的半成品代理对象(保证单例)并通过
BeanPostProcessor
(BPP后置处理器)进行AOP增强完善,随后将此代理对象放入一级缓存中同时清二级缓存
以上就是三级缓存的完整执行流程
疑点|困惑
第二级缓存的存在必要性
在上述流程中似乎并没有发现第二级缓存发挥了怎样的作用,似乎都是使用的第三级缓存所返回的对象,那二级缓存的作用到底是什么呢?
思考一下下述场景 当A被多处循环依赖时
@Component
public class A{@Autowiredprivate B b;
}@Component
public class B{@Autowiredprivate A a;
}@Component
public class C{@Autowiredprivate A a;
}@Component
public class D{@Autowiredprivate A a;
}
这个场景中A被多处依赖,如果没有二级缓存,B、C、D所持有的都会是从三级缓存的ObjectFactory
所生成的不同对象,这样就破坏了单例性。此外,当存在AOP时,Bean执行初始化方法时候会经过BeanPostProcessor
处理,此时需要获取先前创建的半成品AOP代理对象进行完善以保证其他Bean依赖的是同一代理对象。所以,第二级缓存是为了保证Bean的单例性
第三级缓存的存在必要性
第三级缓存核心是为了正确处理需要 AOP 代理的 Bean——需要AOP代理时返回代理对象,不需要时返回原始对象
那么疑问来了:既然最终都要创建代理,为什么不在实例化后立即判断是否需要代理并创建,直接放入二级缓存,这样不就省去三级缓存?
我的理解主要有两点:按需创建&职责分离
这里先用一句话总结三级缓存存在的意义:
三级缓存(
singletonFactories
)存在的核心意义是:延迟代理对象的创建,直到真正需要时才生成,从而避免不必要的性能开销,并支持扩展点(如SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference
)在早期暴露阶段动态决定是否/如何代理。
为什么“实例化后就检查是否需要代理”看似合理,实则不可行?
问题一:违反“按需创建”原则 → 性能浪费
很多 Bean 虽然符合 AOP 切入点(如标注了 @Transactional
),但 并没有被循环依赖。
@Service
public class UserService {@Transactionalpublic void save() { ... } // 有事务,但没人循环依赖它
}
→ 如果在实例化后立即创建代理 → 即使没有循环依赖,也提前创建了代理对象!
代价:
- 代理对象创建是有成本的(反射、CGLIB字节码生成、Advisor匹配等)
- 多数 Bean 并不需要提前暴露 → 提前代理 = 浪费性能 + 内存
Spring 设计哲学:只有在发生循环依赖、需要提前暴露时,才创建早期代理对象。
问题二:破坏“职责分离”与“扩展点机制”
三级缓存中存放的是 ObjectFactory
,它调用的是:
() -> getEarlyBeanReference(beanName, mbd, bean)
而 getEarlyBeanReference
会触发:
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {bean = bp.getEarlyBeanReference(bean, beanName);
}
👉 这是一个扩展点!
举例:
AbstractAutoProxyCreator
:用于 AOP 代理- 未来你也可以自定义
SmartInstantiationAwareBeanPostProcessor
来在早期阶段包装 Bean(不只是代理,可能是装饰器、验证器、日志包装等)
如果直接在实例化后就判断是否代理 → 就绕过了这个扩展点机制,丧失了灵活性。
最终结论:
第三级缓存存在的意义,不是“偷懒”或“复杂化”,而是为了在解决循环依赖的同时,做到:按需创建、性能最优、扩展性强、语义安全。
如果在实例化后就立即创建代理,虽然能“简化”缓存层级,但会导致大量无用代理、破坏扩展机制、违背懒加载原则 —— 是典型的“为了简化结构而牺牲架构弹性与性能”的反模式。
Spring 选择三级缓存,是经过深思熟虑的平衡之选。