一篇文章讲清楚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使用三级缓存解决循环依赖
相关推荐
捂月15 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
FIN技术铺3 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小码的头发丝、3 小时前
Spring Boot 注解
java·spring boot
午觉千万别睡过3 小时前
RuoYI分页不准确问题解决
spring boot
2301_811274313 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
编程重生之路4 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
politeboy4 小时前
k8s启动springboot容器的时候,显示找不到application.yml文件
java·spring boot·kubernetes
世间万物皆对象11 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
qq_174482857512 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
代码小鑫14 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计