• Spring中的循环依赖问题


    Spring的的循环依赖问题


    一. 简介

    1.什么是循环依赖问题?

    类A依赖类B,类B也依赖类A,这种情况就会出现循环依赖。 Bean A → Bean B → Bean A

    在这里插入图片描述

    2.循环依赖有什么影响?

    循环依赖会导致内存溢出

    public class AService {
      private BService bService = new BService();
    }
    
    public class BService {
      private AService aService = new AService();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当你通过 new AService() 创建一个对象时你会获得一个栈溢出的错误。 如果你了解 Java的初始化顺序就应该知道为什么会出现这样的问题。

    因为调用 new AService() 时会先去执行属性 bService 的初始化, 而 bService 的初始化又会去执行AService 的初始化, 这样就形成了一个循环调用,最终导致调用栈内存溢出。

    二. 循环依赖复现

    StudentA 依赖StudentB,同时 StudentB也依赖StudentA

    @Component
    public class StudentA {
    
        private String nameA;
    
        @Autowired
        private StudentB studentB;
    
        public StudentA( StudentB studentB) {
            this.studentB = studentB;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    @Component
    public class StudentB {
    
        private String nameB;
    
        @Autowired
        private StudentA studentA;
    
        public StudentB( StudentA studengA) {
            this.studentA = studentA;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    启动工程,我们会看到如下报错,这就是循环依赖导致的程序运行问题

    Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
    2022-11-07 13:47:13.714 ERROR 12744 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 
    
    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    The dependencies of some of the beans in the application context form a cycle:
    
    ┌─────┐
    |  studentA defined in file [D:\prometheus\student-server\target\classes\com\student\studentserver\entity\xunhaunyilai\StudentA.class]
    ↑     ↓
    |  studentB defined in file [D:\prometheus\student-server\target\classes\com\student\studentserver\entity\xunhaunyilai\StudentB.class]
    └─────┘
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    三. 解决方案

    1. 重新设计

    当出现这种循环依赖时,很可能是在设计方面存在问题,没有把每个类的职责很好的区分开。应该尝试正确重新设计组件,以便其层次结构设计良好,并且不需要循环依赖项。‎如果无法重新设计,那么可以考虑其他解决办法。

    2 使用 @Lazy

    解决Spring循环依赖的一个简单方法就是对一个Bean使用延时加载。也就是说:这个Bean并没有完全的初始化完,实际上他注入的是一个代理,只有当他首次被使用的时候才会被完全的初始化。

    @Component
    public class StudentA {
    
        private String nameA;
    
        @Autowired
        private StudentB studentB;
    
        public StudentA(@Lazy StudentB studentB) {
            this.studentB = studentB;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    @Component
    public class StudentA {
    
        private String nameA;
    
        @Autowired
        private StudentB studentB;
    
        public StudentA(@Lazy StudentB studengB) {
            this.studentB = studengB;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    加上@Lazy以后,启动工程后就不报错了

    3. 使用setter注入
    @Component
    public class StudentA {
    
    
        @Autowired
        private StudentB studentB;
    
        public void testA(){
            System.out.println("student1");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    @Component
    public class StudentB {
    
        @Autowired
        private StudentA studengA;
    
        public void testB(){
            System.out.println("studentB");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    四. 三级缓存

    三级缓存其实它更像是Spring容器工厂的内的术语,采用三级缓存模式来解决循环依赖问题,这三级缓存分别指

    • singletonObjects(一级缓存):用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用
    • earlySingletonObjects(二级缓存):提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
    • singletonFactories(三级缓存):单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖(提前暴露)
    public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
        ...
        // 从上至下 分表代表这“三级缓存”
        private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一级缓存
        private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存
        private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
        ...
        
        /** Names of beans that are currently in creation. */
        // 这个缓存也十分重要:它表示bean创建过程中都会在里面呆着~
        // 它在Bean开始创建时放值,创建完成时会将其移出~
        private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
    
        /** Names of beans that have already been created at least once. */
        // 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复
        // 至少被创建了一次的  都会放进这里~~~~
        private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    先从一级缓存singletonObjects中去获取。(如果获取到就直接return)
    如果获取不到或者对象正在创建中(isSingletonCurrentlyInCreation()),那就再从二级缓存earlySingletonObjects中获取。(如果获取到就直接return)
    如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过getObject()获取。就从三级缓存singletonFactory.getObject()获取。(如果获取到了就从singletonFactories中移除,并且放进earlySingletonObjects。其实也就是从三级缓存移动(是剪切、不是复制哦~)到了二级缓存)
    加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决

    getSingleton()从缓存里获取单例对象步骤分析可知,Spring解决循环依赖的诀窍:就在于singletonFactories这个三级缓存。这个Cache里面都是ObjectFactory,它是解决问题的关键。

    流程梳理

    1. 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。
    2. 为 A 创建一个 Bean工厂,并放入到 singletonFactories 中。
    3. 发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B。
    4. 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
    5. 为 B 创建一个 Bean工厂,并放入到 singletonFactories 中。
    6. 发现 B 需要注入 A 对象,此时在一级、二级未发现对象A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A 放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A还是一个半成品,并没有完成属性填充和执行初始化方法)
    7. 将对象 A 注入到对象 B 中。
    8. 对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)
    9. 对象 A 得到对象B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B)
    10. 对象 A完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A

    流程图总结:在这里插入图片描述

    参考链接:
    spring循环依赖与三级缓存
    循环依赖及解决方法

    五. 总结

    整个从创建bean到解决循环依赖的过程:

    context.getBean(A.class)->实例化->放入缓存->依赖注入B->getBean(B)->实例化B并放入缓存->B依赖注入A->getBean(A)获取到了缓存中的值并正常返回->B初始化成功->A初始化成功

  • 相关阅读:
    领夹直播麦克风常规的使用方法及方案说明
    使用comicai绘制漫画
    Android Studio新版本New UI及相关设置丨遥遥领先版
    Codeforces Round #836 (Div. 2) A-D
    Scanner类用法(学习笔记)
    java-数组
    【目标检测经典算法】R-CNN、Fast R-CNN和Faster R-CNN详解系列一:R-CNN图文详解
    热门Java开发工具IDEA入门指南——创建新的Java应用程序(下)
    java计算机毕业设计网上求职招聘系统源码+系统+数据库+lw文档+mybatis+运行部署
    JDK7多线程并发环境HashMap死循环infinite loop,CPU拉满100%,Java
  • 原文地址:https://blog.csdn.net/qq_44936392/article/details/127729486