找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1583

积分

0

好友

228

主题
发表于 前天 00:25 | 查看: 10| 回复: 0

图片

文章目录:

图片

一. 基础知识

1.1 什么是循环依赖?

一个或多个对象之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用,有以下3种方式。

图片

我们看一个简单的Demo,对标“情况2”。

@Service
public class Model1 {
    @Autowired
    private Model2 model2;
    public void test1() {
    }
}

@Service
public class Model2 {
    @Autowired
    private Model1 model1;
    public void test2() {
    }
}

这是一个经典的循环依赖,它能正常运行,后面我们会通过源码的角度,解读整体的执行流程。

1.2 三级缓存

解读源码流程之前,Spring框架内部的三级缓存逻辑必须了解,要不然后面看代码会蒙圈。

图片

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

图片

这是最核心的逻辑,我们直接看其抽象执行流程:

图片

  1. 先从“第一级缓存”找对象,有就返回,没有就找“二级缓存”;
  2. 找“二级缓存”,有就返回,没有就找“三级缓存”;
  3. 找“三级缓存”,找到了,就获取对象,放到“二级缓存”,并从“三级缓存”移除。

图片

1.3 原理执行流程

以“情况2”为例,其执行流程可以分解为以下3步:

图片

整个执行逻辑如下:

图片

  1. 第一层中,先去获取A的Bean,发现没有就准备去创建一个,然后将A的代理工厂放入“三级缓存”(此时的A是一个半成品,尚未进行属性注入)。由于A依赖B,必须先去创建B。
  2. 第二层中,准备创建B,发现B又依赖A,需要再去创建A。
  3. 第三层中,去创建A,因为第一层已经创建了A的代理工厂,此时直接从“三级缓存”中拿到A的代理工厂,获取A的代理对象,放入“二级缓存”,并清除“三级缓存”。
  4. 回到第二层,现在有了A的代理对象,B的依赖得以解决(注入了半成品的A),B初始化成功。
  5. 回到第一层,现在B初始化成功,完成A对象的属性注入,然后继续填充A的其它属性,并执行A的后续初始化步骤(包括AOP),最终完成对A完整Bean的初始化。
  6. 将完整的A放入“一级缓存”。

图片

为什么要用三级缓存?我们先跟踪源码执行流程,后面会给出详细解答。

二. 源码解读

图片

注意:本文基于Spring 5.2.15.RELEASE版本,不同版本代码可能存在差异。

图片

下面我们将通过Debug的方式,深入代码流程。

2.1 代码入口

图片

图片

调试时需要跳过前面的beanName,直接关注Model1的创建过程。

图片

图片

2.2 第一层

图片

进入doGetBean()方法,从getSingleton()没有找到对象,进入创建Bean的逻辑。

图片

图片

进入doCreateBean()后,调用addSingletonFactory()方法。

图片

此操作向三级缓存singletonFactories中放入model1的工厂对象。

图片

图片

图片

进入populateBean(),执行postProcessProperties(),这里是一个策略模式,找到对应的策略对象。

图片

正式进入该策略对应的注入方法。

图片

下面的流程是为了获取model1的依赖对象(model2)并进行注入。

图片

图片

图片

图片

进入doResolveDependency(),找到model1依赖的对象名model2。

图片

这里需要获取model2的bean,调用的是AbstractBeanFactory的方法。

图片

正式发起获取model2 bean的请求。

图片

至此,第一层逻辑因依赖model2而暂停,进入第二层。

2.3 第二层

图片

获取model2的bean,从doGetBean()doResolveDependency(),逻辑与第一层完全一致,最终找到model2依赖的对象名model1。

前面的流程省略,直接到doResolveDependency()

图片

正式发起获取model1 bean的请求。

图片

至此,第二层逻辑因依赖model1而暂停,进入第三层。

2.4 第三层

图片

再次获取model1的bean。在第一层和第二层中,每次都会从getSingleton()获取,但由于之前model1和model2的三级缓存皆为空,故获取对象为空。

图片

关键点在此!
到达第三层时,由于三级缓存中已存在model1的工厂,这里会使用该工厂为model2创建一个代理对象,并将其放入二级缓存。

图片

此时,model2成功获得了model1的代理对象,其依赖关系得以解决,流程返回到第二层。

2.5 返回第二层

返回第二层后,model2完成初始化。那么,二级缓存的数据何时会晋升到一级呢?

答案在doGetBean()中。当通过createBean()成功创建model2的bean后,会执行getSingleton()方法对结果进行处理。

图片

进入getSingleton(),会看到以下方法。

图片

这里就是处理model2的一、二级缓存的逻辑:将其从二级缓存清除,并放入一级缓存。

图片

2.6 返回第一层

同理,model1初始化完毕后,也会通过相同的逻辑,将model1从二级缓存清除,并将完整的对象放入一级缓存。

图片

至此,整个Bean的创建过程与依赖解决流程全部结束,最终返回初始化完成的model1对象。

三. 原理深度解读

3.1 为什么要有三级缓存?

这是一个非常经典的面试题。前面已经介绍了详细的执行流程和源码,现在我们来探讨设计三级缓存的原因。

核心解析:

  • 一级缓存 singletonObjects:它是一个单例池,用于存放完全初始化好的Bean。它的存在是为了保证Spring容器的单例属性。如果没有它,无法确保全局唯一的单例。
  • 三级缓存 singletonFactories:用于存放创建Bean的工厂(ObjectFactory)。它的核心作用是打破循环依赖。在循环依赖场景中,Bean A可能只完成实例化,还未进行属性注入(是一个“半成品”),就需要被Bean B引用。此时,就可以通过三级缓存中A的工厂,提前暴露这个半成品对象的引用(可能是原始对象,也可能是代理对象),从而让B能够继续初始化。
  • 二级缓存 earlySingletonObjects:用于存放从三级缓存的工厂中获取到的对象。它的存在主要是为了支持AOP。如果没有二级缓存,考虑一个更复杂的场景:Bean A被Bean B和Bean C同时循环依赖,且A需要AOP代理。那么B和C在通过三级缓存获取A时,工厂每次都会生成一个新的代理对象(A1和A2),这就破坏了单例原则。二级缓存存储了第一次从工厂获取的对象,后续再需要时直接从此获取,保证了唯一性。

通过查看工厂方法的源码,可以更清晰地理解其作用。在getEarlyBeanReference()方法中:

图片

图片

该工厂的核心逻辑是:
图片

  1. 如果当前Bean有AOP切面,就为其创建一个代理对象返回。
  2. 如果当前Bean没有AOP切面,则直接返回原始对象。
    图片
3.2 能干掉第二级缓存吗?

考虑以下场景:

@Service
public class A {
    @Autowired private B b;
    @Autowired private C c;
    // A 需要AOP代理
}

@Service
public class B {
    @Autowired private A a;
}

@Service
public class C {
    @Autowired private A a;
}

如果只有一级和三级缓存:
图片

  1. B依赖A,通过三级缓存工厂生成代理对象A1。
  2. C依赖A,通过三级缓存工厂生成代理对象A2。
    图片
    此时,A1和A2是两个不同的代理对象,违反了单例原则。因此,二级缓存的目的就是为了避免因AOP创建多个代理对象,它存储的是半成品的AOP单例Bean。如果没有AOP,理论上只需要一级和三级缓存即可。

四. 总结

最后,我们回顾一下Spring三级缓存各自的核心使命:

图片

  1. 一级缓存 (singletonObjects):为“Spring的单例属性”而生,是完整的单例Bean的家。
  2. 二级缓存 (earlySingletonObjects):为“解决AOP代理对象的唯一性”而生,存放半成品的AOP单例Bean。
  3. 三级缓存 (singletonFactories):为“打破循环依赖”而生,存放用于生成半成品Bean的工厂方法。

图片

理解这三条,就意味着对Spring循环依赖的核心原理有了比较透彻的掌握。本文旨在梳理核心原理并引导读者通过Debug理解流程。阅读源码时建议:掌握基本设计模式、预先了解理论知识、善用英文注释,最重要的是在Debug时保持全局观,聚焦核心逻辑,避免陷入过多细节。




上一篇:嵌入式多线程开发实战指南:POSIX线程库同步原语与死锁预防
下一篇:HTML `<output>` 标签深度指南:解锁原生无障碍支持的语义化元素
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 22:54 , Processed in 0.197372 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表