二级缓存就够了,Spring为何用三级?

上周面试一个候选人,简历上写着"精通Spring源码"。我问了一个常规问题:Spring为什么要用三级缓存解决循环依赖?他答得流利——"一级存成品、二级存半成品、三级存工厂"。我说,那你把三级缓存去掉,看看二级缓存能不能跑通?他愣了三秒。

大部分人对三级缓存的理解,停留在"背答案"层面。今天我们把源码跑一遍,你会看到一个反直觉的事实:绝大多数循环依赖场景,二级缓存就够用了。三级缓存不是为"普通循环依赖"准备的,是专门为AOP代理留的后门。

实验:只用两级缓存,能跑通吗?

先搭一个最简单的循环依赖。两个Service互相注入,不涉及AOP:

@Service
public class OrderService {
    @Autowired
    private UserService userService;
}

@Service
public class UserService {
    @Autowired
    private OrderService orderService;
}

Spring的处理流程(简化版,只保留两级缓存):

  1. 创建OrderService → 实例化(此时orderService只是一个空壳对象)
  2. 把空壳对象放进二级缓存 earlySingletonObjects
  3. 开始填充OrderService的属性 → 发现需要注入UserService
  4. 创建UserService → 实例化 → 把空壳放进二级缓存
  5. 填充UserService属性 → 发现需要注入OrderService
  6. 从二级缓存拿到OrderService的空壳 → 注入成功
  7. UserService初始化完成 → 放入一级缓存
  8. 回到OrderService → 属性填充完成 → 初始化 → 放入一级缓存

结果:正常启动,循环依赖被打破。

这时候你可能会问:那还要三级缓存干什么?答案是:上面的场景中,任何一个Bean都没有被AOP增强。一旦加了@Transactional,故事就变了。

AOP一来,二级缓存就崩了

给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

标签:科技   二级缓存   缓存   对象   初始化   放入   空壳   实例   属性   放进   原始

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302034903号

Top