一篇文章讲清楚Spring如何解决循环依赖,以及为什么需要三级缓存

这是笔者从两道面试题出发的思考,如果有不对的地方,还请指正,仅供参考

Q:讲一讲spring 的循环依赖

循环依赖(Circular Dependency)指的是在对象之间互相依赖的情况。例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环。Spring 框架中主要处理的是单例(singleton)作用域的循环依赖问题,而不能处理原型(prototype)作用域的循环依赖,也无法解决构造器注入的循环依赖。原因后面分析。

Spring 解决循环依赖的方法:三级缓存

一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化)。单例池,为"Spring 的单例属性"⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。 三级缓存(singletonFactories):存放ObjectFactory,ObjectFactory的getObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。 三级缓存如何解决循环依赖?

首先要清楚 Spring 创建 Bean 的流程:

先去 一级缓存 singletonObjects 中获取,存在就返回;Q:讲一讲spring 的循环依赖

循环依赖(Circular Dependency)指的是在对象之间互相依赖的情况。例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环。Spring 框架中主要处理的是单例(singleton)作用域的循环依赖问题,而不能处理原型(prototype)作用域的循环依赖,也无法解决构造器注入的循环依赖。原因后面分析。

Spring 解决循环依赖的方法:三级缓存

  1. 一级缓存(singletonObjects) :存放最终形态的 Bean(已经实例化、属性填充、初始化)。单例池,为"Spring 的单例属性"⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
  2. 二级缓存(earlySingletonObjects) :存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。
  3. 三级缓存(singletonFactories) :存放ObjectFactoryObjectFactorygetObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

三级缓存如何解决循环依赖?

首先要清楚 Spring 创建 Bean 的流程:

  1. 先去 一级缓存 singletonObjects 中获取,存在就返回;
  1. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
  1. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,并在获取成功之后将该对象加入到二级缓存中。

然后来看如何解决循环依赖:

当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;

在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;

那么此时就去三级缓存中调用 getObject() 方法去获取 A 的 前期暴露的对象 ,也就是调用上边加入的 getEarlyBeanReference() 方法,生成一个 A 的 前期暴露对象

然后就将这个 ObjectFactory 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期引用对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址)注入到依赖,来支持循环依赖。

然后B(或者,其代理对象)完成初始化后,进入一级缓存。

A再获取B,按照上述流程就会从一级缓存中获取初始化好的B,注入,完成A的初始化,进入一级缓存。

Q:为什么要有三级依赖?两级行不行?

三级缓存的引入主要用于处理AOP 动态代理对象的创建问题,即某些 Bean 在实例化之后可能需要生成一个代理对象。如果仅仅使用两级缓存,Spring 无法在实例化 Bean 后,动态地将代理对象暴露给其他依赖它的 Bean。

要理解这个问题,我们需要先明确两点:

  1. 代理对象的创建时机:在 Spring 中,某些 Bean 在创建后(即,实例化并初始化后)需要生成代理对象,比如通过 AOP 增强功能(如事务管理、方法拦截等)。

  2. Spring 创建(或者说,获取) Bean 的流程

    1. 先去 一级缓存 singletonObjects 中获取,存在就返回;
    1. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
    1. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,并在获取成功之后将该对象加入到二级缓存中。

下面是分析:

如果 A 和 B 只是普通的 Bean,没有涉及代理的问题(即不需要 AOP 之类的功能),那么只有两级缓存(即,第一和第三级缓存)是够用的:

  1. 由于最开始一级缓存没有A,所以创建A时候,会到第三级缓存拿到创建A的工厂,将A实例化,放入一级缓存
  2. 实例化A之后,Spring尝试初始化A。Spring在初始化阶段为A注入属性时,需要B,于是Spring开始创建B,同样是从三级缓存调用创建B的工厂方法,实例化B,放入一级缓存。然后Spring尝试初始化B,B也依赖于A,那么Spring会在初始化B的过程中去获取A。此时,会在一级缓存获取到A的未初始化的实例,完成B的初始化。
  3. B初始化完成,Spring继续完成A的初始化,最终一级缓存里的A和B都正常地完成了初始化。

两级缓存(保留第一、第三级缓存)就能解决循环依赖的问题。

但假设 B 是一个需要 AOP 代理的对象,比如带有事务功能的 @Transactional 注解。那么 Spring 还需要在 B 被创建之后生成它的代理对象(这个代理对象负责增强:拦截方法调用、实现事务等功能)。

那么,问题出现了:如果没有三级缓存机制(也就是,没有第二级缓存),当 A 依赖 B 时,Spring 可能在初始化过程中只能获取到 B 的原始对象,而不是代理对象,导致 A 没有正确拿到 B 的最终版本(即代理对象)。这时,就可能导致 AOP 功能失效。

这里二级缓存和三级缓存的核心区别在于:有三级缓存(第二级缓存)时,相当于多了一个中间层来处理代理对象,Spring 可以判断完是否需要创建代理对象再将实例(或该实例的代理对象)放入一级缓存。

有了三级缓存之后,需要代理的场景才能得到很好的解决:

  1. 由于最开始一二级缓存都没有A,所以创建A时候,会到第三级缓存拿到创建A的工厂,将A实例化,放入二级缓存。

  2. 实例化A之后,Spring尝试初始化A。Spring在初始化阶段为A注入属性时,需要B,于是Spring开始创建B,同样是从三级缓存调用创建B的工厂方法,实例化B,放入二级缓存。然后Spring尝试初始化B,B也依赖于A,那么Spring会在初始化B的过程中去获取A。此时,会在二级缓存获取到A的未初始化的实例,完成B的初始化。

  3. 到这里完成了B的实例化和初始化,接下来查看是否需要生成代理对象。

    • 如果B被标记为需要代理(例如,有@Transactional注解),Spring就会为B创建代理对象。原始的初始化好的B会被包装在代理对象中。这样,就完成了B的代理对象的实例化,初始化,然后B的代理对象被放入一级缓存;
    • 如果B没有被标记为需要代理,那么将初始化好的B放入一级缓存。
  4. 现在,初始化A所需的B(或者,B的代理对象)就已经被创建好了,A也初始化完成。检查A是否需要被代理,并将初始化好的A(或A的代理对象放入一级缓存)

参考

  1. JavaGuide框架篇
  2. Spring使用三级缓存解决循环依赖
相关推荐
往事随风去14 分钟前
虚拟线程在Spring Boot中的正确使用方式
spring boot
麦兜*24 分钟前
Spring Boot 应用 Docker 监控:Prometheus + Grafana 全方位监控
spring boot·后端·spring cloud·docker·prometheus
L.EscaRC1 小时前
Redisson在Spring Boot中的高并发应用解析
java·spring boot·后端
Naylor2 小时前
玩转kafka
spring boot·kafka
摇滚侠2 小时前
Spring Boot3零基础教程,StreamAPI 介绍,笔记98
java·spring boot·笔记
摇滚侠2 小时前
Spring Boot3零基础教程,StreamAPI 的基本用法,笔记99
java·spring boot·笔记
codingPower3 小时前
升级mybatis-plus导致项目启动报错: net.sf.jsqlparser.statement.select.SelectBody
java·spring boot·maven·mybatis
刘一说4 小时前
深入理解 Spring Boot Web 开发中的全局异常统一处理机制
前端·spring boot·后端
智_永无止境4 小时前
Spring Boot全局异常处理指南
java·spring boot
屹奕4 小时前
基于EasyExcel实现Excel导出功能
java·开发语言·spring boot·excel