Spring 源码学习(五) 循环依赖(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 还记得上一篇笔记,在 bean 加载流程,在创建过程中,出现了依赖循环的监测,如果出现了这个循环依赖,而没有解决的话,代码中将会报错,然后 Spring 容器初始化失败。 由于感觉循环依赖是个比较独立的知识点,所以我将它的分析单独写一篇笔记,来看下什么是循环依赖和如何解决它。

结合关键代码梳理流程

创建原始 bean

  1. BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
  2. // 原始 bean
  3. finalObject bean = instanceWrapper.getWrappedInstance();

在这一步中,创建的是原始 bean,因为还没到最后一步属性解析,所以这个类里面没有属性值,可以将它想象成 newClassA,同时没有构造函数等赋值的操作,这个原始 bean 信息将会在下一步使用到。


addSingleFactory

  1. // 注释 5.2 解决循环依赖 第二个参数是回调接口,实现的功能是将切面动态织入 bean
  2. addSingletonFactory(beanName,()-> getEarlyBeanReference(beanName, mbd, bean));

前面也提到过这个方法,它会将需要提前曝光的单例加入到缓存中,将单例的 beanNamebeanFactory 加入到缓存,在之后需要用到的时候,直接从缓存中取出来。


populateBean 填充属性

刚才第一步时也说过了,一开始创建的只是初始 bean,没有属性值,所以在这一步会解析类的属性。在属性解析时,会判断属性的类型,如果判断到是 RuntimeBeanReference 类型,将会解析引用。

就像我们写的例子, CircleA 引用了 CircleB,在加载 CircleA时,发现 CircleB 依赖,于是乎就要去加载 CircleB

我们来看下代码中的具体流程吧:

  1. protectedvoid populateBean(String beanName,RootBeanDefinition mbd,@NullableBeanWrapper bw){
  2.    ...
  3.    if(pvs !=null){
  4.        // 将属性应用到 bean 中,使用深拷贝,将子类的属性一并拷贝
  5.        applyPropertyValues(beanName, mbd, bw, pvs);
  6.    }
  7. }

  8. protectedvoid applyPropertyValues(String beanName,BeanDefinition mbd,BeanWrapper bw,PropertyValues pvs){
  9.    ...
  10.    String propertyName = pv.getName();
  11.    Object originalValue = pv.getValue();
  12.    // 注释 5.5 解析参数,如果是引用对象,将会进行提前加载
  13.    Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
  14.    ...
  15. }

  16. publicObject resolveValueIfNecessary(Object argName,@NullableObject value){
  17.    // 我们必须检查每个值,看看它是否需要一个运行时引用,然后来解析另一个 bean
  18.    if(value instanceofRuntimeBeanReference){
  19.        // 注释 5.6 在这一步中,如果判断是引用类型,需要解析引用,加载另一个 bean
  20.        RuntimeBeanReference ref =(RuntimeBeanReference) value;
  21.        return resolveReference(argName, ref);
  22.    }
  23.    ...
  24. }

跟踪到这里,加载引用的流程比较清晰了,发现是引用类的话,最终会委派 org.springframework.beans.factory.support.BeanDefinitionValueResolver#resolveReference 进行引用处理,核心的两行代码如下:

  1. // 注释 5.7 在这里加载引用的 bean
  2. bean =this.beanFactory.getBean(refName);
  3. this.beanFactory.registerDependentBean(refName,this.beanName);

在这一步进行 CircleB 的加载,但是我们写的例子中, CircleB 依赖了 CircleA,那它是如何处理的呢,所以这时,我们刚才将 CircleA 放入到缓存中的信息就起到了作用。


getSingleton

还记得之前在类加载时学到的只是么,单例模式每次加载都是取同一个对象,如果在缓存中有,可以直接取出来,在缓存中没有的话才进行加载,所以再来熟悉一下取单例的方法:

  1. protectedObject getSingleton(String beanName,boolean allowEarlyReference){
  2.    Object singletonObject =this.singletonObjects.get(beanName);
  3.    // 检查缓存中是否存在实例
  4.    if(singletonObject ==null&& isSingletonCurrentlyInCreation(beanName)){
  5.        // 记住,公共变量都需要加锁操作,避免多线程并发修改
  6.        synchronized(this.singletonObjects){
  7.            // 如果此 bean 正在加载则不处理
  8.            singletonObject =this.earlySingletonObjects.get(beanName);
  9.            if(singletonObject ==null&& allowEarlyReference){
  10.                // 当某些方法需要提前初始化,调用 addSingletonFactory 方法将对应的
  11.                // objectFactory 初始化策略存储在 earlySingletonObjects,并且从 singletonFactories 移除
  12.                ObjectFactory<?> singletonFactory =this.singletonFactories.get(beanName);
  13.                if(singletonFactory !=null){
  14.                    singletonObject = singletonFactory.getObject();
  15.                    this.earlySingletonObjects.put(beanName, singletonObject);
  16.                    this.singletonFactories.remove(beanName);
  17.                }
  18.            }
  19.        }
  20.    }
  21.    return singletonObject;
  22. }

虽然 CircleB 引用了 CircleA,但在之前的方法 addSingletonFactory 时, CircleAbeanFactory 就提前暴露。

所以 CircleB 在获取单例 getSingleton() 时,能够拿到 CircleA 的信息,所以 CircleB 顺利加载完成,同时将自己的信息加入到缓存和注册表中,接着返回去继续加载 CircleA,由于它的依赖已经加载到缓存中,所以 CircleA 也能够顺利完成加载,最终整个加载操作完成~

结合解决场景的流程图和关键代码流程,比较完善的介绍了循环依赖处理方法,下面还有一个 debug 流程图,希望能加深你的理解~

53.jpg

总结

写这篇总结的目的是为了填坑,因为之前在解析类加载的文章中只是简单的过了一下循环依赖的概念,想要将在类加载中留下的坑填掉。

在分析循环依赖的过程中,发现之前对作用域 scope 的不了解,于是补充了一下这个知识点,接着又发现对循环依赖中使用到的缓存和详细处理不熟悉,于是查阅了相关资料,跟踪源码,一步一步进行分析,所以发现越写越多,解决了一个困惑,增加了几个疑问,所以在不断排查和了解中,加深了对 Spring 的理解。

同样,在工作中,经常会遇到与其它团队的合作,也会遇到同时需要对方的新接口支持,例如在 RPC 中遇到循环调用,那我建议还是换一种方案,例如通过消息解耦,避免循环调用,实在没办法要循环调用,要记得在方法中加上退出条件,避免无限循环(>_<)


由于个人技术有限,如果有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正

代码和注释都在里面,小伙伴们可以下载我上传的代码,亲测可运行~

Gitee 地址:https://gitee.com/vip-augus/spring-analysis-note.git

Github 地址:https://github.com/Vip-Augus/spring-analysis-note


参考资料

  1. Spring学习(十五)Spring Bean 的5种作用域介绍
  2. Spring IOC 容器源码分析 - 循环依赖的解决办法
  3. Spring 源码深度解析》- 郝佳
相关文章
|
1天前
|
JavaScript Java Maven
理解固化的Maven依赖:spring-boot-starter-parent 与 spring-boot-dependencies
理解固化的Maven依赖:spring-boot-starter-parent 与 spring-boot-dependencies
6 1
|
3天前
|
消息中间件 安全 Java
学习认识Spring Boot Starter
在SpringBoot项目中,经常能够在pom文件中看到以spring-boot-starter-xx或xx-spring-boot-starter命名的一些依赖。例如:spring-boot-starter-web、spring-boot-starter-security、spring-boot-starter-data-jpa、mybatis-spring-boot-starter等等。
17 4
|
4天前
|
XML 安全 Java
Spring 基础知识学习
Spring 基础知识学习
|
4天前
|
Java Spring 容器
Spring5系列学习文章分享---第六篇(框架新功能系列+整合日志+ @Nullable注解 + JUnit5整合)
Spring5系列学习文章分享---第六篇(框架新功能系列+整合日志+ @Nullable注解 + JUnit5整合)
5 0
|
4天前
|
XML Java 数据库
Spring5系列学习文章分享---第五篇(事务概念+特性+案例+注解声明式事务管理+参数详解 )
Spring5系列学习文章分享---第五篇(事务概念+特性+案例+注解声明式事务管理+参数详解 )
6 0
|
4天前
|
SQL Java 数据库连接
Spring5系列学习文章分享---第四篇(JdbcTemplate+概念配置+增删改查数据+批量操作 )
Spring5系列学习文章分享---第四篇(JdbcTemplate+概念配置+增删改查数据+批量操作 )
6 0
|
4天前
|
XML Java 数据格式
Spring5系列学习文章分享---第三篇(AOP概念+原理+动态代理+术语+Aspect+操作案例(注解与配置方式))
Spring5系列学习文章分享---第三篇(AOP概念+原理+动态代理+术语+Aspect+操作案例(注解与配置方式))
7 0
|
4天前
|
XML druid Java
Spring5系列学习文章分享---第二篇(IOC的bean管理factory+Bean作用域与生命周期+自动装配+基于注解管理+外部属性管理之druid)
Spring5系列学习文章分享---第二篇(IOC的bean管理factory+Bean作用域与生命周期+自动装配+基于注解管理+外部属性管理之druid)
8 0
|
4天前
|
XML Java 数据格式
Spring5系列学习文章分享---第一篇(概述+特点+IOC原理+IOC并操作之bean的XML管理操作)
Spring5系列学习文章分享---第一篇(概述+特点+IOC原理+IOC并操作之bean的XML管理操作)
13 1
|
4天前
|
Java 程序员 Spring
Spring 源码阅读 一
Spring 源码阅读 一