Spring如何利用三级缓存解密解决循环依赖难题

 更新时间:2025年07月23日 09:43:11   作者:佛祖让我来巡山  
在Spring框架的日常开发中,循环依赖问题如同一个幽灵,时不时困扰着开发者,本文将深入剖析Spring如何通过三级缓存机制破解这一难题,希望对大家有所帮助

引言

在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通过三级缓存架构破解循环依赖:

DefaultSingletonBeanRegistry

-singletonObjects: Map<String, Object> // 一级缓存:成品Bean

-earlySingletonObjects: Map<String, Object> // 二级缓存:半成品(早期引用)

-singletonFactories: Map<String, ObjectFactory> // 三级缓存:对象工厂

各级缓存的核心职责

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

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

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

关键步骤解析

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

// 实例化后立即注册
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean));

早期引用生成(步骤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;
}

缓存状态转移(步骤12/16/20):

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

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

1. 双重延迟决策机制

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

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

2. 状态完整性保障

当创建代理时,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依赖链:

处理流程

  • C获取A时触发三级缓存
  • 返回A的早期引用
  • C完成初始化
  • B获得C的引用
  • A最终获得B的引用

3. 无法解决的场景

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

六、性能优化建议

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

// 使用事件解耦
applicationContext.publishEvent(new UserCreatedEvent(user));

懒加载优化

@Lazy
@Autowired
private HeavyService heavyService; // 延迟初始化

作用域控制

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

结论

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

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

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

到此这篇关于Spring如何利用三级缓存解密解决循环依赖难题的文章就介绍到这了,更多相关Spring三级缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java界面编程实现界面跳转

    Java界面编程实现界面跳转

    这篇文章主要为大家详细介绍了Java界面编程实现界面跳转,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • 解决程序启动报错org.springframework.context.ApplicationContextException: Unable to start web server问题

    解决程序启动报错org.springframework.context.ApplicationContextExcept

    文章描述了一个Spring Boot项目在不同环境下启动时出现差异的问题,通过分析报错信息,发现是由于导入`spring-boot-starter-tomcat`依赖时定义的scope导致的配置问题,调整依赖导入配置后,解决了启动错误
    2024-11-11
  • Mybatis 查询语句条件为枚举类型时报错的解决

    Mybatis 查询语句条件为枚举类型时报错的解决

    这篇文章主要介绍了Mybatis 查询语句条件为枚举类型时报错的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • 利用Java实现天气预报播报功能

    利用Java实现天气预报播报功能

    这篇文章主要为大家介绍了如何利用Java语言实现天气预报播报功能,文中的示例代码讲解详细,对我们学习Java有一定的帮助,需要的可以参考一下
    2022-06-06
  • MybatisPlus分页失效不起作用的解决

    MybatisPlus分页失效不起作用的解决

    在使用MybatisPlus的selectPage时发现分页不起作用,每次返回的都是全部的数据,本文就来介绍一下MybatisPlus分页失效不起作用的解决,感兴趣的可以了解一下
    2024-03-03
  • Mybatis查询方法如何实现没有返回值

    Mybatis查询方法如何实现没有返回值

    这篇文章主要介绍了Mybatis查询方法如何实现没有返回值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • JUC之CountdownLatch使用详解

    JUC之CountdownLatch使用详解

    这篇文章主要介绍了JUC之CountdownLatch使用详解,CountdownLatch 用来进行线程同步协作,等待所有线程完成倒计时,
    其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一,需要的朋友可以参考下
    2023-12-12
  • 详解Spring Cloud Zuul中路由配置细节

    详解Spring Cloud Zuul中路由配置细节

    本篇文章主要介绍了详解Spring Cloud Zuul中路由配置细节,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • 图文详解Java中的字节输入与输出流

    图文详解Java中的字节输入与输出流

    在Java中所有数据都是使用流读写的,流是一组有序的数据序列,将数据从一个地方带到另一个地方,这篇文章主要给大家介绍了关于Java中字节输入与输出流的相关资料,需要的朋友可以参考下
    2021-08-08
  • 手把手教你使用IDEA创建多模块(maven)项目

    手把手教你使用IDEA创建多模块(maven)项目

    这篇文章主要给大家介绍了关于如何使用IDEA创建多模块(maven)项目的相关资料,文中通过图文以及实例代码介绍的非常详细,需要的朋友可以参考下
    2023-07-07

最新评论