
上周面试一个候选人,简历上写着"精通Spring源码"。我问了一个常规问题:Spring为什么要用三级缓存解决循环依赖?他答得流利——"一级存成品、二级存半成品、三级存工厂"。我说,那你把三级缓存去掉,看看二级缓存能不能跑通?他愣了三秒。
大部分人对三级缓存的理解,停留在"背答案"层面。今天我们把源码跑一遍,你会看到一个反直觉的事实:绝大多数循环依赖场景,二级缓存就够用了。三级缓存不是为"普通循环依赖"准备的,是专门为AOP代理留的后门。
先搭一个最简单的循环依赖。两个Service互相注入,不涉及AOP:
@Service
public class OrderService {
@Autowired
private UserService userService;
}
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
Spring的处理流程(简化版,只保留两级缓存):
结果:正常启动,循环依赖被打破。
这时候你可能会问:那还要三级缓存干什么?答案是:上面的场景中,任何一个Bean都没有被AOP增强。一旦加了@Transactional,故事就变了。
给UserService加上事务注解:
@Service
public class UserService {
@Autowired
private OrderService orderService;
@Transactional // 这个注解会让Spring生成UserService的代理对象
public void createUser() {
// ...
}
}
在二级缓存方案下,把刚才的流程重跑一遍。第2步,OrderService的空壳对象放进了二级缓存。第5步,UserService依赖OrderService,从二级缓存拿到空壳对象,注入完成。第7步,UserService开始初始化——初始化阶段会执行BeanPostProcessor,其中AbstractAutoProxyCreator检测到@Transactional,生成UserService的代理对象。最终UserService的代理对象被放入一级缓存。
现在回到第8步。OrderService拿到UserService的引用完成了属性填充,开始初始化。此时AbstractAutoProxyCreator发现OrderService也需要AOP增强(假设OrderService也有@Transactional),生成OrderService的代理对象,把代理对象放入一级缓存。
问题来了:UserService中注入的OrderService,是二级缓存里的原始空壳对象,不是最终放入一级缓存的代理对象。UserService拿到的OrderService是一个没有事务能力的裸对象——调用它的事务方法不会生效。
你可能会想:那我等AOP代理生成之后再放进二级缓存不就行了?但生成代理是在初始化阶段(initializeBean),而循环依赖发生是在填充属性阶段(populateBean),填充属性一定发生在初始化之前。这是一个时间上的死结。
Spring的解决方案:不提前决定要不要代理,而是把"怎么生成早期引用"这个逻辑存起来,等真正需要的时候再执行。
看看
DefaultSingletonBeanRegistry中三个Map的定义:
// 一级缓存:完全初始化完成的成品Bean
private final Map singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:提前暴露的半成品Bean(已经确定是原始对象还是代理对象)
private final Map earlySingletonObjects = new HashMap<>(16);
// 三级缓存:对象工厂(Lambda),调用getObject()时才真正创建早期引用
private final Map> singletonFactories = new HashMap<>(16);
关键的差异在三级缓存。存进去的不是对象,是一个ObjectFactory,一个Lambda。这个Lambda什么时候执行?不是创建Bean的时候,而是另一个Bean来获取依赖的时候。 这就把"决定返回原始对象还是代理对象"这个决策推迟到了最后一刻。
实际代码在doCreateBean方法中:
// AbstractAutowireCapableBeanFactory.java
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化Bean(通过反射调用构造器,创建原始对象)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 2. 决定是否提前暴露——放入三级缓存(而不是二级!)
boolean earlySingletonExposure = (mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName)); // 当前Bean正在创建中
if (earlySingletonExposure) {
// 关键:存的是工厂Lambda,不是对象本身
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 3. 填充属性(这里可能触发循环依赖,去拿其他Bean)
populateBean(beanName, mbd, instanceWrapper);
// 4. 初始化(这里执行BeanPostProcessor,包括AOP代理生成)
Object exposedObject = initializeBean(beanName, bean, mbd);
return exposedObject;
}
第2步只存了一个Lambda。这个Lambda的核心是getEarlyBeanReference:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
// SmartInstantiationAwareBeanPostProcessor的后置处理
// AbstractAutoProxyCreator就在这里决定是否需要生成早期代理
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp =
(SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
当别的Bean(比如UserService)来获取OrderService的依赖时,调用链路是这样的:
// DefaultSingletonBeanRegistry.java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先查一级缓存(成品池)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 2. 查二级缓存(半成品池)
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3. 查三级缓存(工厂池)—— 这里才真正执行Lambda
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject(); // 执行Lambda!
// 生成的对象放入二级缓存,同时移除三级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
流程跑一遍(带AOP的循环依赖场景):
Step 1:创建OrderService → 实例化 → Lambda放入三级缓存
Step 2:填充OrderService属性 → 发现需要UserService → 开始创建UserService
Step 3:创建UserService → 实例化 → Lambda放入三级缓存
Step 4:填充UserService属性 → 发现需要OrderService → 调用getSingleton("orderService", true)
Step 5:一级没有 → 二级没有 → 三级找到Lambda → 执行Lambda → getEarlyBeanReference发现OrderService需要AOP代理 → 返回代理对象 → 放入二级缓存
Step 6:UserService拿到OrderService的代理对象(此时OrderService还未初始化完,但代理对象已经具备了AOP能力)
Step 7:UserService填充完成 → 初始化 → 生成自己的AOP代理 → 放入一级缓存
Step 8:回到OrderService → 填充完成 → 初始化 → 生成自己的AOP代理
Step 9:比对:第5步生成的早期代理对象 vs 第8步生成的最终代理对象。Spring发现一致,安心放入一级缓存。
注意第9步有一个精妙的比对逻辑。在doCreateBean的最后:
// 最终放入一级缓存前,检查早期引用和最终对象是否一致
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false); // false=不允许早期引用
if (earlySingletonReference != null) {
// 如果一样,说明早期代理就是最终代理,直接用
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
// 如果不一样,说明出了问题(比如初始化阶段更换了Bean实例)
// 这种情况循环依赖的另一方拿到的已经是旧引用,Spring会报错
}
}
场景 | 用二级缓存够吗 | 说明 |
无AOP的循环依赖 | ✅ 够 | 原始对象直接暴露即可,不需要延迟决策 |
有AOP的循环依赖 | ❌ 不够 | 暴露时机早于代理生成时机,必须延迟决定"暴露原始还是代理" |
构造器循环依赖 | 几级都没用 | 构造器注入时Bean还没实例化,根本放不进缓存 |
三级的核心价值就一句话:把"生成早期引用"这个动作,从创建Bean的时候推迟到被依赖方来取的时候。 这样Spring可以在Lambda里执行getEarlyBeanReference,利用BeanPostProcessor提前判断是否需要生成代理——而不是等初始化阶段才决定。
二级缓存存的是结果(已确定的原始/代理对象),三级缓存存的是决策逻辑(Lambda),等需要的时候再执行这个决策。这就是为什么不能把三级合并到二级——合并之后要么提前生成代理浪费性能,要么存了不完整的代理导致不一致。
如果你不幸写出构造器循环依赖,Spring不会默默挂掉,而是直接甩你一脸异常。日志大致长这样:
BeanCurrentlyInCreationException: Error creating bean with name 'orderService':
Requested bean is currently in creation: Is there an unresolvable circular reference?
注意关键词 currently in creation。这说明OrderService已经在
singletonsCurrentlyInCreation集合里被标记了,但还没完成创建,而另一个Bean又来要它。
排查的时候看三点。第一,是不是用了构造器注入。构造器循环依赖三级缓存也无能为力——因为构造器执行时Bean还没实例化,根本没有对象能放进缓存。解法是换成@Autowired字段注入或@Lazy延迟加载。
第二,看看报错的两个Bean之间是不是存在AOP代理嵌套的情况。如果一个Bean的@PostConstruct方法里又去拿另一个正在创建的Bean,这种不在Spring创建流程里的获取,三级缓存也救不了。
第三,检查@Async注解。加了@Async的方法所在的Bean会被包装成另一个代理,异步代理的创建时机和事务代理不同,可能引发预期之外的循环依赖链路。
如果你已经上了Spring Boot 3.x,默认禁止了循环依赖(
spring.main.allow-circular-references=false),启动会直接报错,连三级缓存都没机会登场。这时候要么重构代码拆依赖,要么临时打开开关——但心里要清楚这是一个债务标记。循环依赖在小型项目里无伤大雅,但在几十个模块互相引用的中型项目里,它会让Bean的创建顺序变成一场噩梦。
想自己验证?下面这段代码直接复制到Spring Boot项目里就能跑:
@SpringBootApplication
public class CircularDependencyDemo {
public static void main(String[] args) {
ConfigurableApplicationContext ctx =
SpringApplication.run(CircularDependencyDemo.class, args);
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
// 验证:A中的B和容器中的B是同一个对象
System.out.println("A.this == 容器中的A: " + (a == ctx.getBean(A.class))); // true
System.out.println("B.this == 容器中的B: " + (b == ctx.getBean(B.class))); // true
// 如果有AOP,两个引用也是同一个代理对象
System.out.println("A的类名: " + a.getClass().getName());
// 输出类似:com.demo.A$SpringCGLIB$0(说明是代理)
System.out.println("B的类名: " + b.getClass().getName());
ctx.close();
}
}
@Service
class A {
@Autowired
private B b;
@Transactional // 加上这个,就会走三级缓存的完整路径
public void doSomething() {
System.out.println("A.doSomething, B is: " + b);
}
}
@Service
class B {
@Autowired
private A a;
@Transactional
public void doSomething() {
System.out.println("B.doSomething, A is: " + a);
}
}
跑起来之后你会发现:A拿到的B和B拿到的A,都是CGLIB代理对象。而且从容器中拿到的也是同一个代理实例。三级缓存保证了代理对象的全局唯一。
下次面试被问到的时候,不要背"一级存成品、二级存半成品、三级存工厂"了。说清楚两件事就行:第一,为什么二级不够——因为暴露时机在AOP代理生成之前;第二,三级怎么解决——把"生成什么"的决策推迟到真正被引用的时候。
道理其实不复杂,缺的只是把源码跑一遍的耐心。
更新时间:2026-06-30
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302034903号