Spring 三级缓存:案例 + 流程图,搞懂循环依赖

1,281 阅读4分钟

一、从生活中的循环依赖说起

想象这样一个场景:你去公司上班需要工牌,但领取工牌需要先登记工位号,而工位号的分配又需要你提供工牌信息。这种"先有鸡还是先有蛋"的困境,在软件开发中就是典型的循环依赖问题。

在Spring框架中,当两个Bean互相依赖时:

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Service 
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

Spring是如何解决这个看似无解的问题的呢?答案就藏在三级缓存的巧妙设计中。

二、三级缓存结构全景图

先来看一张Spring容器内部的三级缓存结构示意图:

graph LR
    subgraph Spring容器
        direction TB
        cache1[("一级缓存<br>singletonObjects")]
        cache2[("二级缓存<br>earlySingletonObjects")]
        cache3[("三级缓存<br>singletonFactories")]

        cache3 -->|存放| ObjectFactory[[ObjectFactory]]
        cache2 -->|存放| EarlyBean[[早期对象]]
        cache1 -->|存放| MatureBean[[完整Bean]]
        
        style cache1 fill:#c1e1c1,stroke:#2d5d2d
        style cache2 fill:#f0e68c,stroke:#8b6914
        style cache3 fill:#add8e6,stroke:#00008b
    end

    subgraph Bean生命周期
        direction LR
        实例化 --> 填充属性 --> 初始化
    end

    cache3 -.-> |1.实例化后注册| ObjectFactory
    cache2 -.-> |2.提前暴露| EarlyBean
    cache1 -.-> |3.最终成品| MatureBean
  • 一级缓存(成品库)singletonObjects,存放完全初始化好的Bean。
  • 二级缓存(半成品库)earlySingletonObjects,存放提前暴露的原始对象。
  • 三级缓存(对象工厂库)singletonFactories,存放生成对象的工厂。

三、循环依赖解决全流程演示

让我们通过一个具体案例,看看Spring是如何玩转这三个缓存的:

场景设定

ServiceA 依赖 ServiceB,ServiceB 又依赖 ServiceA

伪代码简化版创建流程

  1. 开始创建ServiceA
  2. 实例化ServiceA(在堆中开辟内存空间)
  3. 将ServiceA的ObjectFactory放入三级缓存
  4. 填充ServiceA的属性(发现需要ServiceB)
  5. 开始创建ServiceB
  6. 实例化ServiceB
  7. 将ServiceB的ObjectFactory放入三级缓存
  8. 填充ServiceB的属性(发现需要ServiceA)
  9. 从三级缓存获取ServiceA的ObjectFactory
  10. ObjectFactory.getObject() → 得到ServiceA的早期引用
  11. 将ServiceA早期引用放入二级缓存,清除三级缓存
  12. ServiceB完成属性注入,初始化后放入一级缓存
  13. ServiceA继续完成属性注入和初始化,最终放入一级缓存

关键步骤图解

sequenceDiagram
    participant A as ServiceA
    participant B as ServiceB
    participant Cache3 as 三级缓存
    participant Cache2 as 二级缓存
    participant Cache1 as 一级缓存
    
    A->>Cache3: 注册ObjectFactory
    A->>B: 需要注入
    B->>Cache3: 查询A的工厂
    Cache3-->>B: 返回ObjectFactory
    B->>Cache3: 执行getObject()
    Cache3->>Cache2: 转移A的早期引用
    B->>Cache1: 完成初始化
    A->>Cache1: 完成初始化

四、为什么要用三级缓存?

1. 看似多余的三级缓存

很多同学会有疑问:为什么需要三级缓存而不是两级?我们通过对比实验来说明:

实验场景:使用AOP代理的Bean。

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    @Transactional // 需要生成代理
    public void method() {}
}

两级缓存方案的问题

  1. 第一次从缓存获取时直接创建代理对象。
  2. 如果后续初始化过程中Bean被修改,会导致代理对象与原始对象状态不一致。

三级缓存的优势

  • 延迟代理对象的生成时机。
  • 保证最终放入容器的对象是完整的代理对象。

2. 各级缓存的职责划分

缓存级别存储内容生命周期作用
一级缓存完整的Bean实例整个应用运行期间提供最终可用的Bean
二级缓存原始对象的早期引用创建到初始化完成前解决循环依赖
三级缓存生成对象的ObjectFactory实例化后到放入二级前支持AOP等需要后置处理的情况

五、经典问题解答

Q1:构造器注入为何无法解决循环依赖?

// 构造器注入示例
@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

当使用构造器注入时,对象在实例化阶段就需要完成依赖注入,此时:

  1. ServiceA实例化需要ServiceB。
  2. ServiceB实例化又需要ServiceA。
  3. 两者都无法完成实例化,导致死循环。

Q2:三级缓存与设计模式

这里运用了多种设计模式:

  1. 工厂模式:通过ObjectFactory延迟对象创建。
  2. 外观模式:AbstractBeanFactory统一处理缓存。
  3. 代理模式:处理AOP等需要生成代理的情况。

六、最佳实践与避坑指南

  1. 避免循环依赖(即使Spring能解决)

    • 使用@Autowired而非构造器注入。
    • 定期运行mvn dependency:analyze检查依赖。
    • 重构代码,引入中间层打破循环。
  2. 调试技巧

    // 查看缓存状态
    DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry)context.getAutowireCapableBeanFactory();
    System.out.println("一级缓存:" + registry.getSingletonNames());
    
  3. 性能优化

    • 合理设置Bean的作用域。
    • 避免过度使用@Autowired。
    • 及时清理不需要的Bean。

七、总结与思考

Spring 的三级缓存机制通过 ​提前暴露半成品对象​ 的巧妙设计,解决了循环依赖的难题。 三级缓存设计体现了几个精妙之处:

  1. 空间换时间:通过缓存提升性能。
  2. 关注点分离:每级缓存职责单一。
  3. 延迟加载:ObjectFactory的灵活运用。

最后:三级缓存也并不能解决所有的循环依赖问题,但了解其机制原理,能帮我更好的解决类似的抽象问题。