• 图解 Spring 解决循环依赖,学不会接着砍!


    SpringBean循环依赖

    循环依赖的三种情况

    首先,需要明确的是spring对循环依赖的处理有三种情况:

    1. 构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
    2. 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
    3. 非单例循环依赖:无法处理。

    解决单例模式下的setter循环依赖

    接下来,我们具体看看spring是如何处理第二种循环依赖的。

    Spring单例对象的初始化大略分为三步:

    1. createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象;
    2. populateBean:填充属性,这一步主要是多bean的依赖属性进行填充;
    3. initializeBean:调用spring xml中的init 方法。

    从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一步、第二步。也就是构造器循环依赖和field循环依赖。 Spring为了解决单例的循环依赖问题,使用了三级缓存。

    三级缓存的引入

    Spring的三个级别的缓存声明如下(DefaultSingletonBeanRegistry类中):

    /** Cache of singleton objects: bean name to bean instance. */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    /** Cache of singleton factories: bean name to ObjectFactory. */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    /** Cache of early singleton objects: bean name to bean instance. */
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这三级缓存的作用分别是:

    缓存作用
    singletonFactories进入实例化阶段的单例对象工厂的cache (三级缓存);
    earlySingletonObjects完成实例化但是尚未初始化的,提前暴光的单例对象的Cache (二级缓存);
    singletonObjects完成初始化的单例对象的cache(一级缓存)。

    在执行到doGetBean方法中,会调用getSingleton()方法来检查缓存中是否有这个bean单例,
    在这里插入图片描述

    我们进入getSingleton()方法,追踪源码发现,我们在创建bean的时候,会首先从cache中获取这个bean,这个缓存就是sigletonObjects(一级缓存)

    主要的调用方法是:

    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    	Object singletonObject = this.singletonObjects.get(beanName);
        // isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中
    	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    		synchronized (this.singletonObjects) {
    			singletonObject = this.earlySingletonObjects.get(beanName);
                // allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象
    			if (singletonObject == null && allowEarlyReference) {
    				ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
    				if (singletonFactory != null) {
    					singletonObject = singletonFactory.getObject();
                        // 从singletonFactories中移除,并放入earlySingletonObjects中。 
                        // 其实也就是从三级缓存移动到了二级缓存
    					this.earlySingletonObjects.put(beanName, singletonObject);
    					this.singletonFactories.remove(beanName);
    				}
    			}
    		}
    	}
    	return singletonObject;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。

    这个cache的类型是ObjectFactory,定义如下:

    public interface ObjectFactory<T> { 
    	T getObject() throws BeansException; 
    }
    
    • 1
    • 2
    • 3

    这个接口在AbstractBeanFactory里实现,并在核心方法doCreateBean()引用下面的方法:

    image-20220824162858937

    addSingletonFactory方法内部实现如下:

    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(singletonFactory, "Singleton factory must not be null");
        synchronized (this.singletonObjects) {
            if (!this.singletonObjects.containsKey(beanName)) {
                this.singletonFactories.put(beanName, singletonFactory);
                this.earlySingletonObjects.remove(beanName);
                this.registeredSingletons.add(beanName);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那么到这我们就能大概摸清三级缓存的作用了,进行三级缓存的这段代码发生在createBeanInstance之后,populateBean() 之前,也就是说单例对象此时已经被创建出来(调用了构造器)。

    关键来了,也就是说,创建bean之后,初始化bean之前,这个bean对象(半成品)已经被生产出来了放在三级缓存里了,此时将这个对象提前曝光出来,让大家使用

    这样做有什么好处呢?下面我们带入解决一下现实中的循环依赖问题,就能更好的理解了。

    解决循环依赖案例

    让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。

    出现这种循环依赖的代码如下所示:

    class A {        
        private B b;
        
        @Autowired
        public void setB(B b) {
            this.b = b;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    class B {        
        private A a;
        
        @Autowired
        public void setA(A a) {
            this.a = a;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们按照三级缓存的设置看看能不能解决循环依赖的问题:

    A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories(三级缓存)中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在拿到的的A对象就是完成了初始化的A对象。

    上述分析的简化流程图如下所示:
    在这里插入图片描述

    深入探究原理

    虽然我们解读了三级缓存的简单作用,但是里面大有深奥,一起来从一级缓存探究一下吧

    一级缓存作用

    首先我们模拟一级缓存作用,它保证单例对象仅被创建一次
    在这里插入图片描述

    • 第一次走 getBean("a") 流程后,最后会将成品 a 放入 singletonObjects 一级缓存
    • 后续再走 getBean("a") 流程时,先从一级缓存中找,这时已经有成品 a,就无需再次创建

    一级缓存发生循环依赖

    但是一级缓存无法解决循环依赖问题,分析如下
    在这里插入图片描述

    • 无论是获取 bean a 还是获取 bean b,走的方法都是同一个 getBean 方法,假设先走 getBean("a")
    • 当 a 的实例对象创建,接下来执行 a.setB() 时,需要走 getBean("b") 流程,红色箭头 1
    • 当 b 的实例对象创建,接下来执行 b.setA() 时,又回到了 getBean("a") 的流程,红色箭头 2
    • 但此时 singletonObjects 一级缓存内没有成品的 a,陷入了死循环

    加一个二级缓存尝试解决循环依赖

    上面一级缓存根本无法解决循环依赖问题,那么我们再加一个缓存这次看看能不能解决。

    解决思路如下:

    • 再增加一个 singletonFactories 缓存

      在这次尝试里叫(第)二级缓存,在最后的Spring中,这个是三级缓存

    • 在依赖注入前,即 a.setB() 以及 b.setA() 将 a 及 b 的半成品对象(未完成依赖注入和初始化)放入此缓存

    • 执行依赖注入时,先看看 singletonFactories 缓存中是否有半成品的对象,如果有拿来注入,顺利走完流程

    在这里插入图片描述
    在这里插入图片描述

    对于上面的图

    • a = new A() 执行之后就会把这个半成品的 a 放入 singletonFactories 缓存,即 factories.put(a)
    • 接下来执行 a.setB(),走入 getBean("b") 流程,红色箭头 3
    • 这回再执行到 b.setA() 时,需要一个 a 对象,有没有呢?有!
    • factories.get() 在 singletonFactories 缓存中就可以找到,红色箭头 4 和 5
    • b 的流程能够顺利走完,将 b 成品放入 singletonObject 一级缓存,返回到 a 的依赖注入流程,红色箭头 6

    二级缓存无法处理创建代理的场景

    二级缓存无法正确处理循环依赖并且包含有代理创建的场景,分析如下

    • spring 默认要求,在 a.init 完成之后才能创建代理 pa = proxy(a)
    • 由于 a 的代理创建时机靠后,在执行 factories.put(a) 向 singletonFactories 中放入的还是原始对象
    • 接下来箭头 3、4、5 这几步 b 对象拿到和注入的都是原始对象
      在这里插入图片描述

    三级缓存解决上述所有问题

    上面有代理对象情况更加复杂,所以这时候,spring引入了三级缓存,增加了一个缓存singletonObjects(在最后spring中,这个叫二级缓存,这只是第三次引入的)

    简单分析上述场景的话,只需要将代理的创建时机放在依赖注入之前即可,但 spring 仍然希望代理的创建时机在 init 之后,只有出现循环依赖时,才会将代理的创建时机提前。所以解决思路稍显复杂:

    在这里插入图片描述

    • 图中 factories.put(fa) 放入的既不是原始对象,也不是代理对象而是工厂对象 fa
    • 当检查出发生循环依赖时,fa 的产品就是代理 pa,没有发生循环依赖,fa 的产品是原始对象 a
    • 假设出现了循环依赖,拿到了 singletonFactories 中的工厂对象,通过在依赖注入前获得了 pa,红色箭头 5
    • 这回 b.setA() 注入的就是代理对象,保证了正确性,红色箭头 7
    • 还需要把 pa 存入新加的 earlySingletonObjects 缓存,红色箭头 6
    • a.init 完成后,无需二次创建代理,从哪儿找到 pa 呢?earlySingletonObjects 已经缓存,蓝色箭头 9

    当成品对象产生,放入 singletonObject 后,singletonFactories 和 earlySingletonObjects 就中的对象就没有用处,清除即可

  • 相关阅读:
    Linux 中 WIFI 和热点的使用
    编辑距离算法
    Apache Linkis自定义变量介绍
    WebRTC实现一个网页在线录制视频
    Python类和对象创建过程分析与元类以及魔法函数
    浏览器安装vue调试工具
    Web应用开发 - 实训三 B Servlet基础
    【Linux系统编程十八】:(基础IO5)--动静态库共享/动静态加载问题(涉及地址空间)
    js数据结构[单项链表];
    redis 配置与优化
  • 原文地址:https://blog.csdn.net/weixin_45525272/article/details/126506804