引言

在Spring框架的日常开发中,循环依赖问题如同一个幽灵,时不时困扰着开发者。当Bean A依赖Bean B,而Bean B又依赖Bean A时,传统的创建流程会陷入死锁。本文将深入剖析Spring如何通过三级缓存机制破解这一难题,揭示其背后的设计智慧。

一、循环依赖的本质问题

循环依赖的根源在于对象创建的顺序性矛盾

@Component
public class ServiceA {
@Autowired
private ServiceB serviceB; // 需要ServiceB实例
} @Component
public class ServiceB {
@Autowired
private ServiceA serviceA; // 需要ServiceA实例
}

这种"鸡生蛋还是蛋生鸡"的问题,传统创建流程无法解决。

二、三级缓存机制全景解析

Spring通过三级缓存架构破解循环依赖:

classDiagram
class DefaultSingletonBeanRegistry {
-singletonObjects: Map~String, Object~ // 一级缓存:成品Bean
-earlySingletonObjects: Map~String, Object~ // 二级缓存:半成品(早期引用)
-singletonFactories: Map~String, ObjectFactory~ // 三级缓存:对象工厂
}

各级缓存的核心职责

缓存级别 存储内容 生命周期 作用
一级缓存 完全初始化的Bean 应用生命周期 提供最终产品
二级缓存 早期引用(半成品) 被依赖→初始化完成 临时周转
三级缓存 ObjectFactory对象 实例化→被依赖/初始化完成 延迟生成早期引用

三、破解循环依赖的全流程

以经典的A→B→A依赖链为例:

sequenceDiagram
participant C as 容器
participant L3 as 三级缓存
participant L2 as 二级缓存
participant A as BeanA
participant B as BeanB

Note over C: 创建BeanA
C->>A: 1. 实例化(分配内存)
C->>L3: 2. 添加ObjectFactory_A
C->>A: 3. 属性注入(发现需要B)

Note over C: 转向创建B
C->>B: 4. 实例化
C->>L3: 5. 添加ObjectFactory_B
C->>B: 6. 属性注入(发现需要A)

B->>L3: 7. 请求获取A
L3->>L3: 8. 调用ObjectFactory_A.getObject()
L3->>L3: 9. 执行getEarlyBeanReference()
alt 需要代理
L3->>L3: 10. 创建代理对象Proxy_A
else 无需代理
L3->>L3: 10. 保留原始对象
end
L3->>L2: 11. 存入早期引用
L3->>L3: 12. 移除ObjectFactory_A
L3->>B: 13. 返回A的早期引用

C->>B: 14. 完成B的初始化
C->>L1: 15. B成品放入一级缓存
C->>L3: 16. 移除ObjectFactory_B

C->>A: 17. 注入B(已就绪)
C->>A: 18. 完成A初始化
C->>L1: 19. A成品放入一级缓存
C->>L2: 20. 移除A的早期引用

关键步骤解析

  1. 三级缓存注册(步骤2/5):

    // 实例化后立即注册
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean));
  2. 早期引用生成(步骤9-11):

    protected Object getEarlyBeanReference(String beanName, Object bean) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    // 动态决策是否创建代理
    bean = ((SmartInstantiationAwareBeanPostProcessor) bp)
    .getEarlyBeanReference(bean, beanName);
    }
    }
    return bean;
    }
  3. 缓存状态转移(步骤12/16/20):

    • 被依赖后从三级缓存删除
    • 初始化完成后从二级缓存删除
    • 最终成品存于一级缓存

四、三级缓存的设计精妙之处

1. 双重延迟决策机制

public Object getEarlyBeanReference() {
// 延迟点1:只在被依赖时触发
// 延迟点2:动态决定是否创建代理
return (needsProxy ? createProxy(bean) : bean);
}

优势:避免为不需要代理或未发生循环依赖的Bean创建额外对象

2. 状态完整性保障

graph TD
A[属性注入完成] --> B[创建早期引用]
B --> C[代理可安全使用属性值]

当创建代理时,Bean已通过populateBean()完成属性注入,避免NPE风险

3. 对象版本统一性

// 最终代理一致性保证
public void initializeBean() {
if (earlyProxyReference != null) {
return earlyProxyReference; // 复用已创建的代理
}
return createProxy(bean); // 无循环依赖时创建
}

4. 资源高效利用

场景 传统方案 三级缓存方案 性能提升
无循环依赖 创建所有代理 不创建代理 节省90%内存
有循环依赖无代理 创建半成品副本 直接使用原始对象 减少对象创建
有循环依赖需代理 可能创建多个代理 单例代理 避免代理冲突

五、疑难场景解决方案

1. 代理对象循环依赖

@Service
public class UserService {
@Autowired
private OrderService orderService; @Transactional // 需要代理
public void createUser() {...}
}

解决方案

  • getEarlyBeanReference()中创建代理
  • 保证代理对象基于完成属性注入的状态

2. 多级循环依赖

A→B→C→A依赖链:

graph LR
A-->B
B-->C
C-->A

处理流程

  1. C获取A时触发三级缓存
  2. 返回A的早期引用
  3. C完成初始化
  4. B获得C的引用
  5. A最终获得B的引用

3. 无法解决的场景

场景 原因
构造器循环依赖 对象未实例化完成,无法暴露引用
原型(Prototype)作用域 Spring不缓存原型Bean
@Async方法 代理生成时机与标准AOP不同

六、性能优化建议

  1. 避免循环依赖:重构设计,引入事件机制

    // 使用事件解耦
    applicationContext.publishEvent(new UserCreatedEvent(user));
  2. 懒加载优化

    @Lazy
    @Autowired
    private HeavyService heavyService; // 延迟初始化
  3. 作用域控制

    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class RequestScopedBean {...}

结论

Spring的三级缓存机制通过以下创新设计解决循环依赖:

  1. 空间换时间:通过三级缓存状态管理打破创建顺序限制
  2. 延迟决策:在被依赖时才决定是否创建代理
  3. 状态保障:确保代理对象基于完整初始化状态
  4. 资源优化:避免不必要的对象创建

理解三级缓存不仅帮助解决循环依赖异常,更是深入掌握Spring框架设计思想的钥匙。正如Spring框架创始人Rod Johnson所说:"好的框架设计是在约束与灵活性之间找到完美平衡",三级缓存正是这种平衡的艺术体现。

【Spring三级缓存解密】如何优雅解决循环依赖难题的更多相关文章

  1. Spring 动态代理时是如何解决循环依赖的?为什么要使用三级缓存?

    前言 在研究 『 Spring 是如何解决循环依赖的 』 的时候,了解到 Spring 是借助三级缓存来解决循环依赖的. 同样在上一节留下了疑问: 循环依赖为什么要使用三级缓存?而不是使用二级缓存? ...

  2. Spring ioc(4)---如何解决循环依赖

    前面说到对象的创建,那么在创建的过程中Spring是怎么又是如何解决循环依赖的呢.前面提到有个三级缓存.就是利用这个来解决循环依赖.打个比方说实例化A的时候,先将A创建(早期对象)放入一个池子中.这个 ...

  3. Spring解决循环依赖,你真的懂了吗?

    导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...

  4. Spring如何解决循环依赖,你真的懂了?

    导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...

  5. 建议收藏!利用Spring解决循环依赖,深入源码给你讲明白!

    前置知识 只有单例模式下的bean会通过三级缓存提前暴露来解决循环依赖的问题.而非单例的bean每次获取都会重新创建,并不会放入三级缓存,所以多实例的bean循环依赖问题不能解决. 首先需要明白处于各 ...

  6. 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. Spring如何使用三级缓存解决循环依赖

    Spring如何使用三级缓存解决循环依赖 首先来了解一下什么是循环依赖 @Component public class A { @Autowired B b; } @Component public ...

  8. Spring三级缓存解决循环依赖

    前提知识 1.解决循环依赖的核心依据:实例化和初始化步骤是分开执行的 2.实现方式:三级缓存 3.lambda表达式的延迟执行特性 spring源码执行逻辑 核心方法refresh(), popula ...

  9. Spring如何解决循环依赖问题

    目录 1. 什么是循环依赖? 2. 怎么检测是否存在循环依赖 3. Spring怎么解决循环依赖 本文主要是分析Spring bean的循环依赖,以及Spring的解决方式. 通过这种解决方式,我们可 ...

  10. 面试官:连Spring三级缓存都答不好,自己走还是我送你?

    面试官:简历上写了精通Spring,那你回答一下Spring为什么用“三级缓存”去解决循环依赖? 我:.......应该有三个缓存的map结构 面试官:具体回答一下 我:平时没认真深入过 面试官:公司 ...

随机推荐

  1. 记一次 .NET某工控任务调度系统 卡死分析

    一:背景 1. 讲故事 前段时间有位朋友加我微信,来了就要进我的训练营,并且附带着纠结了他几个月的一个疑难杂症,让我帮忙看下怎么回事,问题描述截图如下: 由于这个定时任务是 furion 写的,刚好这 ...

  2. python初学之random()模块

    ##python小脚本 random()是不能直接访问的,需要导入 random 模块,然后通过 random 静态对象调用该方法. random.random()用于生成 一个指定范围内的随机符点数 ...

  3. PC端自动化测试实战教程-3-pywinauto 启动PC端应用程序 - 下篇(详细教程)

    1.简介 经过上一篇的学习.介绍和了解,pywinauto的强大,不言而喻吧!宏哥讲解和分享的是电脑自带和安装的应用程序.有些小伙伴或者童鞋们已经迫不及待地私信宏哥,如果在电脑中这个应用程序已经启用了 ...

  4. 消息验证码(MAC)的介绍

    目录 认证流程 MAC 的分类 认证加密 MAC的攻击手段 重发攻击 密钥推测攻击 消息认证码无法解决的问题 Reference 消息验证码(MAC)也可以称为消息认证码. 定义: 消息验证码(Mes ...

  5. 递归神经网络 RNN 原理(下)

    基于对 RNN 的初步认识, 还是先回顾一下它核心的步骤: (1) words / onehot vectors : \(x^{(t)} \in R^{|v|}\) **(2) word embedd ...

  6. RPC实战与核心原理之健康检测

    健康检测:这个节点都挂了,为啥还要疯狂发请求 回顾 超大规模集群"服务发现"的挑战,服务发现的作用就是实时感知集群 IP 的变化,实现接口跟服务集群节点 IP 的映射.在超大规模集 ...

  7. 一款基于 .NET 开源、可以拦截并修改 WinSock 封包的 Windows 软件

    前言 今天大姚给大家分享一款基于 .NET 开源(MIT license).可以拦截并修改 WinSock 封包的 Windows 软件:WinsockPacketEditor. 工具介绍 Winso ...

  8. css样式修改-悬浮数字

    代码实现 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...

  9. 如何在Mac系统上把U盘分成2个区?

    插入U盘后,使用命令行执行如下命令: diskutil partitionDisk /dev/disk8 GPT MS-DOS TESLAMUSIC 7% ExFAT TESLADRIVE 93% 第 ...

  10. C++学习思维导图

    C++思维导图 整个的思维导图大概的架构如下,Xmind.SVG.PDF格式的下载链接都在下面了,如有需要可自取 Xmind文件分享:https://cnblogs-img.oss-cn-hangzh ...