Spring循环依赖代码演示及解决方案

 更新时间:2023年04月21日 11:38:54   作者:.番茄炒蛋  
这篇文章主要介绍了Spring循环依赖实现过程,Spring的解决循环依赖是有前置条件的,要解决循环依赖我们首先要了解Spring Bean对象的创建过程和依赖注入的方式

介绍

上图就是循环依赖的三种情况,虽然方式不同,但是循环依赖的本质是一样的,就A的完整创建要依赖与B,B的完整创建要依赖于A,相互依赖导致没办法完整创建造成失败.

循环依赖代码演示

public class Demo {
    public static void main(String[] args) {
        new Demo1();
    }
}
class Demo1{
    private Demo2 demo2 = new Demo2();
}
class Demo2 {
    private Demo1 demo1 = new Demo1();
}

上述代码就是最基本的循环依赖的场景,Demo1依赖Demo2,Demo2依赖Demo1,然后就报错了,而上面的这种设计情况是无解的.

分析问题

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

代码解决

public class Demo {
    /**
     * 保存提前暴露的对象,也就是半成品对象
     */
    private final static Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
    public static void main(String[] args) throws Exception {
        System.out.println(getBean(Demo1.class).getDemo2());
        System.out.println(getBean(Demo2.class).getDemo1());
    }
    private static <T> T getBean(Class<T> clazz) throws Exception {
        // 获取beanName
        String beanName = clazz.getName().toLowerCase();
        // 查找缓存中是否存在半成品对象
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        // 缓存中不存在半成品对象,反射进行实例化
        T res = clazz.newInstance();
        // 将实例化后的对象储存到缓存
        singletonObjects.put(beanName, res);
        // 获取所有属性
        Field[] fields = res.getClass().getDeclaredFields();
        // 循环进行属性填充
        for (Field field : fields) {
            // 针对private修饰
            field.setAccessible(Boolean.TRUE);
            // 获取属性类型
            Class<?> fieldClazz = field.getType();
            // 获取属性beanName
            String filedBeanName = fieldClazz.getName().toLowerCase();
            // 属性填充,查找缓存是否有对应属性,没有就递归调用
            field.set(res, singletonObjects.containsKey(filedBeanName) ? singletonObjects.get(filedBeanName) : getBean(fieldClazz));
        }
        return res;
    }
}
class Demo1 {
    private Demo2 demo2;
    public Demo2 getDemo2() {
        return demo2;
    }
    public void setDemo2(Demo2 demo2) {
        this.demo2 = demo2;
    }
}
class Demo2 {
    private Demo1 demo1;
    public Demo1 getDemo1() {
        return demo1;
    }
    public void setDemo1(Demo1 demo1) {
        this.demo1 = demo1;
    }
}

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

Spring循环依赖

针对Spring中Bean对象的各种场景,支持的方案不一样

单例

  • 构造注入:无解,避免栈溢出,需要检测是否存在循环依赖的情况,如果有直接抛异常
  • 设值注入:三级缓存–>提前暴露

原型

  • 构造注入:无解,避免栈溢出,需要检测是否存在循环依赖的情况,如果有直接抛异常
  • 设置注入:不支持循环依赖

Spring是如何解决循环依赖问题的?上述代码中对象的生命周期就两个:创建对象和属性填充,而Spring涉及到对象生命周期的方法就很多了,简单举例,如下图:

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

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

上面就是Spring的生命周期方法和循环依赖出现相关的流程了.下面就是放入三级缓存的源码:

/**
     * 添加对象到三级缓存
     *
     * @param beanName
     * @param singletonFactory
     */
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    // 确保singletonFactory不为null
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    // 使用singletonObjects进行加锁,保证线程安全
    synchronized (this.singletonObjects) {
        //如果singletonObjects缓存中没有该对象
        if (!this.singletonObjects.containsKey(beanName)) {
            // 将对象放置到singletonFactories(三级缓存)中
            this.singletonFactories.put(beanName, singletonFactory);
            // 从earlySingletonObjects(二级缓存)中移除该对象
            this.earlySingletonObjects.remove(beanName);
            // 将beanName添加到已经注册的单例集中
            this.registeredSingletons.add(beanName);
        }
    }
}

放入二级缓存的源码:

/**
     * 返回在给定名称下注册的(原始)单例对象.检查已经实例化的单例,并允许对当前创建的单例进行早期引用(解决循环引用)
     *
     * @param beanName
     * @param allowEarlyReference
     * @return
     */
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 不需要完全获取单例锁的情况下快速检查现有实例
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果单例对象为空,并且当前单例正在创建中,则尝试获取早期单例对象
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        // 如果早期单例对象为空,并且允许早期引用,则再完全获取单力所的情况下创建早期单例对象
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 检查早期单例对象是否存在
                singletonObject = this.singletonObjects.get(beanName);
                // 如果早期对象仍然为空则创建单例对象
                if (singletonObject == null) {
                    // 从二级缓存获取
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 获取不到对象从三级缓存中获取
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            // 获取到添加到二级缓存并从三级缓存中移除该对象
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

放入一级缓存中的源码:

/**
     * 将单例对象添加到一级缓存
     *
     * @param beanName
     * @param singletonObject
     */
protected void addSingleton(String beanName, Object singletonObject) {
    // 使用singletonObjects进行加锁,保证线程安全
    synchronized (this.singletonObjects) {
        // 将映射关系添加到一级缓存
        this.singletonObjects.put(beanName, singletonObject);
        // 从三级缓存;二级缓存中移除该对象
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        // 将beanName添加到已经注册的单例集中
        this.registeredSingletons.add(beanName);
    }
}

总结

三级缓存分别有什么作用

  • singletonObjects:缓存经过了完整生命周期的bean
  • earlySingletonObjects:缓存未经过完整生命周期的bean,如果某个bean出现了循环依赖,就会提前把这个暂时未经过完整生命周期的bean放入earlySingletonObjects中,如果这个bean要经过AOP,那么就会把代理对象放入到earlySingletonObjects中,否则就是把原始对象放入earlySingletonObjects,但是不管怎么样就是代理对象,代理对象所代理的原始对象也是没有经过完整生命周期的,所以放入earlySingletonObjects我们就可以统一认为是未经过完整生命周期的bean
  • singletonFactories:缓存的是一个ObjectFactory,也就是一个Lambda表达式,在每个bean的生成过程中,经过实例化得到一个原始对象后,都会提前基于原始对象暴露一个Lambda表达式,并保存到三级缓存中,这个Lambda表达式可能用到,也可能用不到, 如果当前bean没有出现循环依赖,那么这个Lambda表达式就没有用,当前bean按照自己的生命周期正常执行,执行完直接把当前bean放入singletonObjects中,如果当前bean在依赖注入时出现了循环依赖,则从三级缓存中拿到Lambda表达式,并执行Lambda表达式得到一个对象,并把得到的对象放入到二级缓存(如果当前bean需要AOP,那么执行Lambda表达式,得到的就是对应的代理对象,如果无需AOP,则直接得到一个原始对象)
  • 其实还要一个缓存,用来记录某个原始对象是否进行过AOP了

为什么需要三级缓存

如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的bean对象应该是AOP之后的代理对象,而B的a属性对应的不是AOP之后的代理对象,这就产生了冲突,B依赖的A和最终的A不是同一个对象,三级缓存主要处理的是AOP的代理对象,存储的是一个ObjectFactory

到此这篇关于Spring循环依赖代码演示及解决方案的文章就介绍到这了,更多相关Spring循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java 8中如何获取参数名称的方法示例

    Java 8中如何获取参数名称的方法示例

    这篇文章主要给大家介绍了在Java 8中如何获取参数名称的方法,文中给出了详细的介绍和方法示例,相信对大家的理解和学习具有一定的参考借鉴价值,有需要的朋友可以参考学习,下面来一起看看吧。
    2017-01-01
  • 带你全面认识Java中的异常处理

    带你全面认识Java中的异常处理

    在你所写过的代码中,你已经接触过一些异常了,我们可以通过一些简单的代码让我们理解一些简单的异常,下面这篇文章主要给大家介绍了关于Java中异常处理的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2022-12-12
  • SpringBoot2.0 ZipKin示例代码

    SpringBoot2.0 ZipKin示例代码

    这篇文章主要介绍了SpringBoot2.0 ZipKin示例代码,详细的介绍了什么是ZipKin以及SpringBoot2.0 ZipKin示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-11-11
  • springboot整合微信支付sdk过程解析

    springboot整合微信支付sdk过程解析

    这篇文章主要介绍了springboot整合微信支付sdk过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08
  • 浅谈SpringBoot之开启数据库迁移的FlyWay使用

    浅谈SpringBoot之开启数据库迁移的FlyWay使用

    这篇文章主要介绍了浅谈SpringBoot之开启数据库迁移的FlyWay使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • java内部类的最详细详解

    java内部类的最详细详解

    内部类是指在一个外部类的内部再定义一个类,下面这篇文章主要给大家介绍了关于java内部类的最详细详解,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2022-06-06
  • SpringBoot前后端json数据交互的全过程记录

    SpringBoot前后端json数据交互的全过程记录

    现在大多数互联网项目都是采用前后端分离的方式开发,下面这篇文章主要给大家介绍了关于SpringBoot前后端json数据交互的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-03-03
  • 如何重写hashcode和equals方法

    如何重写hashcode和equals方法

    这篇文章主要介绍了如何重写hashcode和equals方法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • PrintStream和PrintWriter的区别简介

    PrintStream和PrintWriter的区别简介

    这篇文章主要介绍了PrintStream和PrintWriter的区别简介,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Java 中校验时间格式的常见方法

    Java 中校验时间格式的常见方法

    在实际项目开发中,跟时间参数打交道是必不可少的,为了保证程序的安全性、健壮性,一般都会对参数进行校验,其他类型的参数校验很好实现,那你知道时间参数的是怎么校验的吗,下面给大家分享Java 中校验时间格式的方法,感兴趣的朋友跟随小编一起看看吧
    2024-08-08

最新评论