【Spring依赖循环】提前曝光,直接曝光到二级缓存已经可以解决循环依赖问题了,为什么一定要三级缓存?

前言

问:什么是循环依赖?

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

问:Spring 如何解决循环依赖?

答:Spring 通过提前曝光机制,利用三级缓存解决循环依赖(这原理还是挺简单的,参考:三级缓存、图解循环依赖原理)

再问:Spring 通过提前曝光,直接曝光到二级缓存已经可以解决循环依赖问题了,为什么一定要三级缓存?

下面我们就来看看


首先先熟悉一下三级缓存分别是:

singletonObject:一级缓存,该缓存 key = beanName, value = bean;这里的 bean 是已经创建完成的,该 bean 经历过实例化->属性填充->初始化以及各类的后置处理。因此,一旦需要获取 bean 时,我们第一时间就会寻找一级缓存。

earlySingletonObjects:二级缓存,该缓存 key = beanName, value = bean;这里跟一级缓存的区别在于,该缓存所获取到的 bean 是提前曝光出来的,是还没创建完成的。

也就是说获取到的 bean 只能确保已经进行了实例化,但是属性填充跟初始化肯定还没有做完,因此该 bean 还没创建完成,仅仅能作为指针提前曝光,被其他 bean 所引用。

singletonFactories:三级缓存,该缓存 key = beanName, value = beanFactory;在 bean 实例化完之后,属性填充以及初始化之前,如果允许提前曝光,spring 会将实例化后的 bean 提前曝光,也就是把该 bean 转换成 beanFactory 并加入到三级缓存。

说人话

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

@Service
public class TestService1 {
 
    @Autowired
    private TestService2 testService2;
 
    public void test1() {
    }
}
@Service
public class TestService2 {
 
    @Autowired
    private TestService1 testService1;
 
    public void test2() {
    }
}

这是一个经典的循环依赖,但是它能正常运行,得益于spring的内部机制,让我们根本无法感知它有问题,因为spring默默帮我们解决了。

下面用一张图告诉你,spring是如何解决循环依赖的:在这里插入图片描述
细心的朋友可能会发现在这种场景中第二级缓存作用不大。

那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class TestService1 {
 
    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;
 
    public void test1() {
    }
}
@Service
public class TestService2 {
 
    @Autowired
    private TestService1 testService1;
 
    public void test2() {
    }
}
@Service
public class TestService3 {
 
    @Autowired
    private TestService1 testService1;
 
    public void test3() {
    }
}

TestService1依赖于TestService2和TestService3,而TestService2依赖于TestService1,同时TestService3也依赖于TestService1

按照上图的流程可以把TestService1注入到TestService2,并且TestService1的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1注入到TestService3的流程如图:

在这里插入图片描述
TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的

这样不是有问题?

为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。

在这里插入图片描述

### Spring三级缓存解决循环依赖原理 #### Bean 实例化过程中的三个阶段 在 Spring 容器中,Bean 的生命周期分为多个阶段,在这些阶段中会利用到不同的缓存来管理 Bean 的状态。具体来说: - **单实例 (Singleton) Bean** 在创建过程中会被放置在一个名为 `singletonObjects` 的一级缓存 Map 中[^1]。 - 当某个 Bean 正处于创建但尚未完全初始化的状态时,则将其加入到 `earlySingletonObjects` 这个二级缓存中作为提前暴露的对象引用。 - 如果一个 Bean 已经完成了属性填充但是还未执行初始化方法,则该对象会被存储于 `registeredSingletons` 集合内,这是第三级缓存的一部分。 这种机制允许容器能够在某些情况下处理 AOP 或者其他场景下的循环依赖问题而不至于陷入死锁或无限递归的情况。 #### 如何通过三级缓存解决问题 当遇到循环依赖情况时,假设存在两个 Singleton 类型的 Bean A 和 B 形成相互依赖关系: - 创建 Bean A 时发现其有对 Bean B 的依赖; - 尝试获取 Bean B 发现它也正在被创建并同样依赖于 Bean A; 如果没有特别措施的话这将导致错误的发生。然而借助上述提到的一二三级缓存结构可以有效规避此类风险: 一旦检测到即将发生循环依赖(即当前请求的目标 Bean 已存在于早期曝光列表),则直接返回已经部分构建好的版本给调用方继续使用而不是等待整个流程结束再提供服务[^2]。 ```java // 简化的伪代码展示如何判断是否存在可重入访问以及相应处理逻辑 if (!this.singletonObjects.containsKey(beanName)) { Object sharedInstance = getEarlyBeanReference(factory, beanName); if (sharedInstance != null && !StringUtils.isEmpty(beanName)) { addSingletonFactory(beanName, () -> getObjectForBeanInstance(sharedInstance, name)); return; } } ``` 此段简化后的 Java 代码片段展示了 Spring 框架内部是如何检查是否有可用的部分构造完毕的对象,并据此决定是否应该立即返回这个临时占位符而非阻塞直到全部完成为止。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Romeo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值