一文帮你彻底打通循环依赖的任督二脉

Posted 波波烤鸭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文帮你彻底打通循环依赖的任督二脉相关的知识,希望对你有一定的参考价值。

面试的重点,大厂必问之一:

循环依赖

1. 什么是循环依赖

看下图

  上图是循环依赖的三种情况,虽然方式有点不一样,但是循环依赖的本质是一样的,就你的完整创建要依赖与我,我的完整创建也依赖于你。相互依赖从而没法完整创建造成失败。

2. 代码演示

  我们再通过代码的方式来演示下循环依赖的效果

public class CircularTest 

    public static void main(String[] args) 
        new CircularTest1();
    

class CircularTest1
    private CircularTest2 circularTest2 = new CircularTest2();


class CircularTest2
    private CircularTest1 circularTest1 = new CircularTest1();

执行后出现了 StackOverflowError 错误

  上面的就是最基本的循环依赖的场景,你需要我,我需要你,然后就报错了。而且上面的这种设计情况我们是没有办法解决的。那么针对这种场景我们应该要怎么设计呢?这个是关键!

3. 分析问题

  首先我们要明确一点就是如果这个对象A还没创建成功,在创建的过程中要依赖另一个对象B,而另一个对象B也是在创建中要依赖对象A,这种肯定是无解的,这时我们就要转换思路,我们先把A创建出来,但是还没有完成初始化操作,也就是这是一个半成品的对象,然后在赋值的时候先把A暴露出来,然后创建B,让B创建完成后找到暴露的A完成整体的实例化,这时再把B交给A完成A的后续操作,从而揭开了循环依赖的密码。也就是如下图:

4. 自己解决

  明白了上面的本质后,我们可以自己来尝试解决下:

先来把上面的案例改为set/get来依赖关联

public class CircularTest 

    public static void main(String[] args) throws Exception
        System.out.println(getBean(CircularTest1.class).getCircularTest2());
        System.out.println(getBean(CircularTest2.class).getCircularTest1());
    

    private static <T> T getBean(Class<T> beanClass) throws Exception
        // 1.获取 实例对象
        Object obj = beanClass.newInstance();
        // 2.完成属性填充
        Field[] declaredFields = obj.getClass().getDeclaredFields();
        // 遍历处理
        for (Field field : declaredFields) 
            field.setAccessible(true); // 针对private修饰
            // 获取成员变量 对应的类对象
            Class<?> fieldClass = field.getType();
            // 获取对应的 beanName
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            // 给成员变量赋值 如果 singletonObjects 中有半成品就获取,否则创建对象
            field.set(obj,getBean(fieldClass));
        
        return (T) obj;
    


class CircularTest1
    private CircularTest2 circularTest2;

    public CircularTest2 getCircularTest2() 
        return circularTest2;
    

    public void setCircularTest2(CircularTest2 circularTest2) 
        this.circularTest2 = circularTest2;
    


class CircularTest2
    private CircularTest1 circularTest1;

    public CircularTest1 getCircularTest1() 
        return circularTest1;
    

    public void setCircularTest1(CircularTest1 circularTest1) 
        this.circularTest1 = circularTest1;
    

然后我们再通过把对象实例化和成员变量赋值拆解开来处理。从而解决循环依赖的问题

public class CircularTest 
    // 保存提前暴露的对象,也就是半成品的对象
    private final static Map<String,Object> singletonObjects = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception
        System.out.println(getBean(CircularTest1.class).getCircularTest2());
        System.out.println(getBean(CircularTest2.class).getCircularTest1());
    

    private static <T> T getBean(Class<T> beanClass) throws Exception
        //1.获取类对象对应的名称
        String beanName = beanClass.getSimpleName().toLowerCase();
        // 2.根据名称去 singletonObjects 中查看是否有半成品的对象
        if(singletonObjects.containsKey(beanName))
            return (T) singletonObjects.get(beanName);
        
        // 3. singletonObjects 没有半成品的对象,那么就反射实例化对象
        Object obj = beanClass.newInstance();
        // 还没有完整的创建完这个对象就把这个对象存储在了 singletonObjects中
        singletonObjects.put(beanName,obj);
        // 属性填充来补全对象
        Field[] declaredFields = obj.getClass().getDeclaredFields();
        // 遍历处理
        for (Field field : declaredFields) 
            field.setAccessible(true); // 针对private修饰
            // 获取成员变量 对应的类对象
            Class<?> fieldClass = field.getType();
            // 获取对应的 beanName
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            // 给成员变量赋值 如果 singletonObjects 中有半成品就获取,否则创建对象
            field.set(obj,singletonObjects.containsKey(fieldBeanName)?
                    singletonObjects.get(fieldBeanName):getBean(fieldClass));
        
        return (T) obj;
    


class CircularTest1
    private CircularTest2 circularTest2;

    public CircularTest2 getCircularTest2() 
        return circularTest2;
    

    public void setCircularTest2(CircularTest2 circularTest2) 
        this.circularTest2 = circularTest2;
    


class CircularTest2
    private CircularTest1 circularTest1;

    public CircularTest1 getCircularTest1() 
        return circularTest1;
    

    public void setCircularTest1(CircularTest1 circularTest1) 
        this.circularTest1 = circularTest1;
    

运行程序你会发现问题完美的解决了

  在上面的方法中的核心是getBean方法,Test1 创建后填充属性时依赖Test2,那么就去创建 Test2,在创建 Test2 开始填充时发现依赖于 Test1,但此时 Test1 这个半成品对象已经存放在缓存到 singletonObjects 中了,所以Test2可以正常创建,在通过递归把 Test1 也创建完整了。

最后总结下该案例解决的本质:

5. Spring循环依赖

  然后我们再来看看Spring中是如何解决循环依赖问题的呢?刚刚上面的案例中的对象的生命周期的核心就两个

  而Spring创建Bean的生命周期中涉及到的方法就很多了。下面是简单列举了对应的方法

  基于前面案例的了解,我们知道肯定需要在调用构造方法方法创建完成后再暴露对象,在Spring中提供了三级缓存来处理这个事情,对应的处理节点如下图:

对应到源码中具体处理循环依赖的流程如下:

  上面就是在Spring的生命周期方法中和循环依赖出现相关的流程了。那么源码中的具体处理是怎么样的呢?我们继续往下面看。

首先在调用构造方法的后会放入到三级缓存中

下面就是放入三级缓存的逻辑

	protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) 
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		// 使用singletonObjects进行加锁,保证线程安全
		synchronized (this.singletonObjects) 
			// 如果单例对象的高速缓存【beam名称-bean实例】没有beanName的对象
			if (!this.singletonObjects.containsKey(beanName)) 
				// 将beanName,singletonFactory放到单例工厂的缓存【bean名称 - ObjectFactory】
				this.singletonFactories.put(beanName, singletonFactory);
				// 从早期单例对象的高速缓存【bean名称-bean实例】 移除beanName的相关缓存对象
				this.earlySingletonObjects.remove(beanName);
				// 将beanName添加已注册的单例集中
				this.registeredSingletons.add(beanName);
			
		
	

然后在填充属性的时候会存入二级缓存中

earlySingletonObjects.put(beanName,bean);
registeredSingletons.add(beanName);

最后把创建的对象保存在了一级缓存中

	protected void addSingleton(String beanName, Object singletonObject) 
		synchronized (this.singletonObjects) 
			// 将映射关系添加到单例对象的高速缓存中
			this.singletonObjects.put(beanName, singletonObject);
			// 移除beanName在单例工厂缓存中的数据
			this.singletonFactories.remove(beanName);
			// 移除beanName在早期单例对象的高速缓存的数据
			this.earlySingletonObjects.remove(beanName);
			// 将beanName添加到已注册的单例集中
			this.registeredSingletons.add(beanName);
		
	

6. 疑问点

这些疑问点也是面试官喜欢问的问题点

为什么需要三级缓存

三级缓存主要处理的是AOP的代理对象,存储的是一个ObjectFactory

三级缓存考虑的是带你对象,而二级缓存考虑的是性能-从三级缓存的工厂里创建出对象,再扔到二级缓存(这样就不用每次都要从工厂里拿)

没有三级环境能解决吗?

没有三级缓存是可以解决循环依赖问题的

三级缓存分别什么作用

一级缓存:正式对象

二级缓存:半成品对象

三级缓存:工厂

以上是关于一文帮你彻底打通循环依赖的任督二脉的主要内容,如果未能解决你的问题,请参考以下文章

一文彻底帮你打通SpringAOP的任督二脉,大厂高薪指日可待,建议收藏!!!

一文打通Seata源码的任督二脉

开篇词 | 打通“容器技术”的任督二脉

数据联袂AI:打通云原生的任督二脉

10段代码打通js学习的任督二脉

“智慧”交通打通城市的任督二脉