一篇文章讲清楚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使用三级缓存解决循环依赖
相关推荐
Yaml435 分钟前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~36 分钟前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong16168838 分钟前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
程序媛小果2 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot
AskHarries4 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端
2401_857622664 小时前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化
程序员阿龙4 小时前
基于SpringBoot的医疗陪护系统设计与实现(源码+定制+开发)
java·spring boot·后端·医疗陪护管理平台·患者护理服务平台·医疗信息管理系统·患者陪护服务平台
前 方5 小时前
若依入门案例
java·spring boot·maven
阿华的代码王国5 小时前
【Spring】——SpringBoot项目创建
java·spring boot·后端·启动类·target文件
转世成为计算机大神5 小时前
网关 Spring Cloud Gateway
java·网络·spring boot·1024程序员节