Spring使用三级缓存解决循环依赖问题

前言:

Spring中使用了三级缓存的设计,来解决单例模式下的属性循环依赖问题

这句话有两点需要注意

  1. 解决问题的方法是「三级缓存的设计」
  2. 解决的只是单例模式下的 Bean 属性循环依赖问题,对于多例 Bean 和 Prototype 作用域的 Bean的循环依赖问题,并不能使用三级缓存设计解决。

那么前言谈到了三级缓存解决单例模式下的属性循环依赖问题,什么是循环依赖呢?

一.循环依赖

java 复制代码
public class A {
    @Autowired
    private B b;
}

public class B {
    @Autowired
    private A a;
}

如上代码所示,即 A 里面注入 B,B 里面又注入 A。此时,就发生了「循环依赖」。

二.三级缓存

如果你曾经看过Spring解决循环依赖的博客,应该知道它其中有好几个Map,一个Map放的是最完整的对象,称为singletonObjects,一个Map放的是提前暴露出来的对象,称为earlySingletonObjects。

Spring中,单例Bean在被创建后会被放入IoC容器的缓存池中,并触发Spring对该Bean的生命周期管理

单例模式下,在第一次使用 Bean 时,会创建一个 Bean 对象,并放入 IoC 容器的缓存池中。后续再使用该 Bean 对象时,会直接从缓存池中获取。

保存单例模式 Bean 的缓存池,采用了三级缓存设计,如下代码所示。

java 复制代码
/** Cache of singleton objects: bean name --> bean instance */
/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
 
/** Cache of early singleton objects: bean name --> bean instance */
/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

/** Cache of singleton factories: bean name --> ObjectFactory */
/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
  • singletonFactories:这是三级缓存。在一级缓存和二级缓存中,缓存的 key 是 beanName,缓存的 value 则是一个 Bean 对象,但是在三级缓存中,缓存的 value 是一个 Lambda 表达式,通过这个 Lambda 表达式可以创建出来目标对象的一个代理对象。

2.1 使用三级缓存解决依赖问题

AbstractBeanFactory.doGetBean() 方法中,获取单例 Bean 的主要流程是在第一次调用时尝试从缓存中获取已存在的实例getSingleton(),而此方法就使用到了三级缓存

java 复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // Spring首先从singletonObjects(一级缓存)中尝试获取
  Object singletonObject = this.singletonObjects.get(beanName);
  // 若是获取不到而且对象在建立中,则尝试从earlySingletonObjects(二级缓存)中获取
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    synchronized (this.singletonObjects) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
          ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
          if (singletonFactory != null) {
            //若是仍是获取不到而且允许从singletonFactories经过getObject获取,则经过singletonFactory.getObject()(三级缓存)获取
              singletonObject = singletonFactory.getObject();
              //若是获取到了则将singletonObject放入到earlySingletonObjects,也就是将三级缓存提高到二级缓存中
              this.earlySingletonObjects.put(beanName, singletonObject);
              this.singletonFactories.remove(beanName);
          }
        }
    }
  }
  return (singletonObject != NULL_OBJECT ? singletonObject : null);
}


}

分析getSingleton()的整个过程,可以知道三级缓存的使用过程如下:

  1. Spring会先从一级缓存**singletonObjects**中尝试获取 Bean
  2. 若是获取不到,而且对象正在建立中,就会尝试从二级缓存**earlySingletonObjects**中获取 Bean。
  3. 若还是获取不到,且允许从三级缓存**singletonFactories**中经过 singletonFactorygetObject() 方法获取 Bean 对象,就会尝试从三级缓存 singletonFactories 中获取 Bean。
  4. 若是在三级缓存中获取到了 Bean,会将该 Bean 存放到二级缓存中。

2.2第三级缓存为何能解决循环依赖

Spring解决循环依赖的诀窍在于singletonFactories这个三级缓存。

当我们创建一个 AService 的时候,通过反射刚把原始的 AService 创建出来之后,先去判断当前一级缓存中是否存在当前 Bean,如果不存在

  1. 首先向三级缓存中添加一条记录,记录的 key 就是当前 Bean 的 beanName,value 则是一个 Lambda 表达式 ObjectFactory,通过执行这个 Lambda 可以给当前 AService 生成代理对象。
  2. 然后如果二级缓存中存在当前 AService Bean,则移除掉。

现在继续去给 AService 各个属性赋值,结果发现 AService 需要 BService,然后就去创建 BService,创建 BService 的时候,发现 BService 又需要用到 AService,于是就先去一级缓存中查找是否有 AService,如果有,就使用,如果没有,则去二级缓存中查找是否有 AService,如果有,就使用,如果没有,则去三级缓存中找出来那个 ObjectFactory,然后执行这里的 getObject 方法,这个方法在执行的过程中,会去判断是否需要生成一个代理对象,如果需要就生成代理对象返回,如果不需要生成代理对象,则将原始对象返回即可。最后,把拿到手的对象存入到二级缓存中以备下次使用,同时删除掉三级缓存中对应的数据。这样 AService 所依赖的 BService 就创建好了。

接下来继续去完善 AService,去执行各种后置的处理器,此时,有的后置处理器想给 AService 生成代理对象,发现 AService 已经是代理对象了,就不用生成了,直接用已有的代理对象去代替 AService 即可。

至此,AServiceBService 都搞定。

下述是ObjectFactory接口的定义:

java 复制代码
public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

在 Bean 建立过程当中,有两处比较重要的匿名内部类实现了该接口。一处是 Spring 利用其建立 Bean 的时候,另外一处就是在**addSingletonFactory**方法中,如下代码所示。

java 复制代码
addSingletonFactory(beanName, new ObjectFactory<Object>() {
   @Override   
   public Object getObject() throws BeansException {
       return getEarlyBeanReference(beanName, mbd, bean);
   }
});

此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance 以后

  1. 此时,单例 Bean 对象已经实例化(可以通过对象引用定位到堆中的对象),但尚未属性赋值和初始化。
  2. Spring 会将该状态下的 Bean 存放到三级缓存中,提早曝光给 IoC 容器("提早"指的是不必等对象完成属性赋值和初始化后再交给 IoC 容器)。也就是说,可以在三级缓存 singletonFactories 中找到该状态下的 Bean 对象。

本质上,singletonFactories 是把 AOP 的过程提前了。

2.3 解决循环依赖实例分析

java 复制代码
public class A {
    @Autowired
    private B b;
}

public class B {
    @Autowired
    private A a;
}
  1. 创建对象A,完成生命周期的第一步实例化再调用完createBeanInstance方法后,会调用addSingletonFactory方法,将已实例化但未属性赋值未初始化的对象A放入三级缓存singletonFactories中,即将对象A提早曝光给IoC容器。

  2. 继续,执行对象 A 生命周期的第二步,即属性赋值(Populate)。此时,发现对象 A 依赖对象,所以就会尝试去获取对象 B。

  3. 继续,发现 B 尚未创建,所以会执行创建对象 B 的过程。

  4. 在创建对象 B 的过程中,执行实例化(Instantiation)和属性赋值(Populate)操作。此时发现,对象 B 依赖对象 A。

  5. 继续,尝试在缓存中查找对象 A。先查找一级缓存,发现一级缓存中没有对象 A(因为对象 A 还未初始化完成);转而查找二级缓存,二级缓存中也没有对象 A(因为对象 A 还未属性赋值);转而查找三级缓存 singletonFactories,对象 B 可以通过 ObjectFactory.getObject 拿到对象 A。

  6. 继续,对象 B 在获取到对象 A 后,继续执行属性赋值(Populate)和初始化(Initialization)操作。对象 B 完成初始化操作后,会被存放到一级缓存中,而A会被放入二级缓存中以备使用。

  7. 继续,转到「对象 A 执行属性赋值过程并发现依赖了对象 B」的场景。此时,对象 A 可以从一级缓存中获取到对象 B,所以可以顺利执行属性赋值操作。

  8. 继续,对象 A 执行初始化(Initialization)操作,完成后,会被存放到一级缓存中。

2.4 为何不能解决非单例Bean的循环依赖

**question1:**Spring 为什么不能解决构造器的循环依赖

answer: 对象的构造函数是在实例化阶段调用的**。**在对象已实例化后,才会将对象存入三级缓存中。
**question2:**Spring 为什么不能解决prototype作用域循环依赖

answer: Spring IoC 容器只会管理单例 Bean 的生命周期,并将单例 Bean 存放到缓存池中(三级缓存)。Spring 并不会管理 prototype 作用域的 Bean
**question3:**Spring 为什么不能解决多例的循环依赖

answer: 多实例 Bean 是每次调用 getBean 都会创建一个新的 Bean 对象,该 Bean 对象并不能缓存。而 Spring 中循环依赖的解决正是通过缓存来实现的。

非单例Bean的循环依赖如何解决

  • 对于构造器注入产生的循环依赖,可以使用 @Lazy 注解,延迟加载。
  • 对于多例 Bean 和 prototype 作用域产生的循环依赖,可以尝试改为单例 Bean。

2.5 为什么一定要三级缓存

为什么一定要三级缓存,使用二级缓存可以解决循环依赖吗?循环依赖问题解决不是只要加入一个缓存就可以了吗?那这样二级缓存应该够用了吧。来看下面这个图:

如上图所示,引入了一个缓存池。

当我们需要创建 AService 的实例的时候,会首先通过 Java 反射创建出来一个原始的 AService,这个原始 AService 可以简单理解为刚刚 new 出来(实际是刚刚通过反射创建出来)还没设置任何属性的 AService,此时,我们把这个 AService 先存入到一个缓存池中。

接下来我们就需要给 AService 的属性设置值了,同时还要处理 AService 的依赖,这时我们发 现 AService 依赖 BService,那么就去创建 BService 对象,结果创建 BService 的时候,发现 BService 依赖 AService,那么此时就先从缓存池中取出来 AService 先用着,然后继续 BService 创建的后续流程,直到 BService 创建完成后,将之赋值给 AService,此时 AServiceBService 就都创建完成了。

可能有人会说,BService 从缓存池中拿到的 AService 是一个半成品,并不是真正的最终的 AService,但你要知道,Java 是引用传递(也可以认为是值传递,只不过这个值是内存地址),BService 当时拿到的是 AService 的引用,说白了就是一块内存地址而已,根据这个地址找到的就是 AService,所以,后续如果 AService 创建完成后,BService 所拿到的 AService 就是完整的 AService 了。

那么上面提到的这个缓存池,在 Spring 容器中有一个专门的名字,earlySingletonObjects,这是 Spring 三级缓存中的二级缓存,这里保存的是刚刚通过反射创建出来的 Bean,这些 Bean 还没有经历过完整生命周期,Bean 的属性可能都还没有设置,Bean 需要的依赖都还没有注入进来。

那按照上述的介绍,有人可能会觉得奇怪,一级缓存和二级缓存就足以解决循环依赖了,为什么还冒出来一个三级缓存?那就需要考虑AOP的情况了。

说到这里,先了解一下Spring中的AOP创建流程

正常来说是首先通过反射获取到一个Bean的实例,然后就是给这个Bean填充属性,属性填充完毕之后,接下来就是执行各种 BeanPostProcessor 了,如果这个 Bean 中有需要代理的方法,那么系统就会自动配置对应的后置处理器。

举一个简单例子,假设有如下一个 Service

java 复制代码
@Service
public class UserService {

    @Async
    public void hello() {
        System.out.println("hello>>>"+Thread.currentThread().getName());
    }
}

那么系统就会自动提供一个名为 AsyncAnnotationBeanPostProcessor 的处理器,在这个处理器中,系统会生成一个代理的 UserService 对象,并用这个对象代替原本的 UserService

那么要搞清楚的是,原本的 UserService 和新生成的代理的 UserService 是两个不同的对象,占两块不同的内存地址!!!

再来回顾下下面这张图:

如果 AService 最终是要生成一个代理对象的话,那么 AService 存到缓存池的其实还是原本的 AService,因为此时还没到处理 AOP 那一步**(要先给各个属性赋值,然后才是 AOP 处理)**,这就导致 BService 从缓存池里拿到的 AService 是原本的 AService,等到 BService 创建完毕之后,AService 的属性赋值才完成,接下来在 AService 后续的创建流程中,AService 会变成了一个代理对象了,不是缓存池里的 AService 了,最终就导致 BService 所依赖的 AService 和最终创建出来的 AService 不是同一个。

二级缓存:

  1. 创建Bean A实例(原始对象)

  2. ✅ 直接放入二级缓存:earlySingletonObjects.put("A", 原始对象A)

  3. Bean B需要注入A:从二级缓存拿到原始对象A

  4. 问题!Bean A实际上需要被代理,但B拿到的已经是原始对象了

  5. 后续给A创建代理对象时,B持有的还是旧的原始对象 → ❌ 不一致!

三级缓存

  1. 创建Bean A实例(原始对象)

  2. ✅ 放入三级缓存:singletonFactories.put("A", 工厂)

  3. Bean B需要注入A:调用三级缓存中的工厂

  4. 工厂执行:getEarlyBeanReference("A", mbd, bean)

  5. 检查A是否需要代理 → 需要! → 创建代理对象A'

  6. 将代理对象A'放入二级缓存

  7. Bean B拿到代理对象A'(正确的!)

  8. 后续A完成初始化,二级缓存中的代理对象A'被移到一级缓存

  9. ✅ Bean B和容器中的A保持一致性!

总的来说,Spring 解决循环依赖把握住两个关键点:

  • 提前暴露:刚刚创建好的对象还没有进行任何赋值的时候,将之暴露出来放到缓存中,供其他 Bean 提前引用(二级缓存)。
  • 提前 AOP:A 依赖 B 的时候,去检查是否发生了循环依赖(检查的方式就是将正在创建的 A 标记出来,然后 B 需要 A,B 去创建 A 的时候,发现 A 正在创建,就说明发生了循环依赖),如果发生了循环依赖,就提前进行 AOP 处理,处理完成后再使用(三级缓存)。

参考文章:

Spring使用三级缓存解决循环依赖

相关推荐
草字41 分钟前
uniapp 滚动到表单的某个位置,表单验证失败时。
前端·javascript·uni-app
p***s9142 分钟前
【SpringBoot】日志文件
java·spring boot·spring
CXH72843 分钟前
架构师的登山之路|第十二站:服务网格 Istio——未来的标配,还是复杂过头?
前端·javascript·istio
梁bk1 小时前
Redis 通信协议(RESP)
数据库·redis·缓存
脾气有点小暴1 小时前
详解 HTML Image 的 mode 属性:图像显示模式的灵活控制
前端·html·uniapp
e***19352 小时前
【mybatis】基本操作:详解Spring通过注解和XML的方式来操作mybatis
xml·spring·mybatis
编程修仙2 小时前
第七篇 java的注解以及使用反射实现自定义注解功能
xml·java·开发语言·spring
0思必得02 小时前
[Web自动化] 开发者工具性能(Performance)面板
运维·前端·自动化·web自动化·开发者工具
心灵的制造商2 小时前
el-tree左侧新增类别和删除类别实例代码
前端·javascript·vue.js