1 什么是Spring循环依赖

// A依赖了B,B是A对象中的一个属性
class A{public B b;
}// B依赖了A
class B{public A a;
}

在普通的代码中,对象之间有依赖是很正常的,但是在Spring中,Bean对象的创建是要经过一系列的生命周期的。其中,有些循环依赖,Spring可以帮我们解决,但是有些就只能靠我们程序员手动解决。

在了解循环依赖之前,我们需要先熟悉Bean的生命周期,即:创建过程。

2 Bean的生命周期

首先,被Spring所管理的对象就叫做Bean。
Bean的主要生命周期(创建过程)如下:
①Spring扫描加了特殊注解(@Bean、@Component…)的类,得到对应的BeanDefinition

BeanDefinition包含了我们对bean的配置,就类似于XML配置中的<bean>标签,其中包含:bean的全限定类名、scope等一些系列配置

②根据BeanDefinition生成对应bean
③根据class判断类的构造方法
④根据推断出来的构造方法,通过反射,得到一个对象(原始对象【未进行依赖注入、AOP等操作】)
⑤填充原始对象中的属性(依赖注入)【加了@AutoWired】
⑥如果原始对象中的某个方法被AOP(增强)了,则需要根据原始对象生成一个代理对象
⑦把最终生成的代理对象放入单例池(singletonObjects),下次我们需要获取bean中,就可以直接从单例池中获取。【类比数据库连接池】
那么在第④步通过构造方法反射时,就容易出现问题:比如上文说的A,A类中存在一个B类的b属性。
1、在A类生成了一个原始对象时,就会给b属性赋值,此时就需要B类的对象,那么Spring就会根据b属性的类型和属性名去BeanFactory获取B类对应的单例Bean。
2、如果BeanFactory中存在B类对应的bean,则直接拿来赋值,那么就不会产生循环依赖。但是如果BeanFactory不存在B类对应的bean(B类的一个对象),那么就需要生成B类的对象,就会经历B的生命周期,但是在创建B的bean过程中,B又依赖A,此时又需要获取A类对应的bean,而此时A类的bean还在创建过程中,于是就出现了循环依赖。

主要过程:
A(bean)创建-》依赖B-》创建B(bean)-》B依赖A(A的bean还在创建过程中)

3 Spring解决循环依赖方法(三级缓存)

Spring采用三级缓存机制,帮助我们解决了部分的循环依赖问题

3、1 三级缓存概念

三级缓存:
一级缓存:singletonObjects
二级缓存:earlySingletonObjects
三级缓存:singletonFactories

  • singletonObjects中缓存的是已经经历了完整生命周期的bean对象。【完整生命周期】
  • earlySingletonObjects存入的是还没经历完整个生命周期的bean对象【部分生命周期】
  • singletonFactories缓存的是ObjectFactory对象工厂(用来创建某个对象的)

3、2 解决循环依赖思路

产生循环依赖原因是:
A创建时— —>需要B— —>去创建B— —>需要A,于是产生了循环
此时,我们通过加入缓存(中间人)来破循环,如下图所示:


A的bean在创建过程中,在进行依赖注入之前,就先把A的原始对象放入缓存earlySingletonObjects(提早暴露,方便其他Bean获取),在将A放入缓存后再进行依赖注入,此时A的bean依赖了B的bean,如果发现此时BeanFactory中没有B的bean,则去创建。在创建B的bean时,需要A的bean,此时去一级缓存(单例池)中未获取到,去二级缓存(earlySingletonObjects)中拿到了A的原始对象【此时是A的原始对象,不是最终的bean,没有走完全部生命周期】,B的原始对象依赖注入完之后,B的生命周期结束,A的生命周期也结束。

在整个过程中,都只有一个A的原始对象,对于B而言,就算注入的是A的原始对象也没有关系,因为A原始对象在后续的生命周期都在堆中没有发生变化。

这时,可能有人会想,只需要一个缓存就能解决循环依赖了,那么为什么Spring中还要又第三级缓存singletonFactories呢?

B依赖的A和最终的A不是同一个对象,因为在一个bean的生命周期最后,Spring提供了BeanPostProcess,可以对bean进行加工,这个加工不仅而可以修改bean的属性值,还可以替换掉当前的bean。

//此时有一个User类
@Component
public class User {}
@Component
public class TestBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {// 注意这里,生成了一个新的User对象if (beanName.equals("user")) {System.out.println(bean);User user = new User();//生成新的user对象return user;}return bean;}
}
public class Test {public static void main(String[] args) {AnnotationConfigApplicationContext context =new AnnotationConfigApplicationContext(AppConfig.class);User user = context.getBean("user", User.class);System.out.println(user);}
}

最终得到的user对象就是不同的:

com.test.service.User@5e025e34
com.test.service.User@1b0375c6

因此,BeanPostProcessor完全可以替换掉某个beanName对应的bean对象
而BeanPostProcessor的执行时间在bean的属性注入之后,循环依赖是发生在属性注入过程中的,所以就有可能导致,注入给B对象的A对象和经历过完整生命周期的A对象不是同一个对象,这样就产生了问题。【因此,这种情况下的循环依赖,Spring是无法解决的,因为在对bean的属性注入时,Spring也不知道A对象后续会经过哪些BeanPostProcessor以及会对A对象进行什么处理。】

  • 某个beanName对应的最终对象和原始对象不是一个对象=》AOP

AOP就是通过一个BeanPostProcessor来实现的,Spring中的AOP分为两种:JDK动态代理、CGLib的动态代理,一个使用的是接口,一个使用的是继承;所以如果给一个类的某个方法设置了切面,那么这个类最终就需要生成一个代理对象。

  • 过程:A类 -> 生成一个普通A-> 属性注入 ->基于切面生成代理对象->把代理对象放入singletonObjects单例池

那么Spring是如何解决对象不一致的情况呢,这里就需要用到第三级缓存singletonFactories
首先,singletonFactories存的是某个beanName对应的ObjectFactory,在bean的生命周期中,生成完原始对象之后,就会构造一个ObjectFactory放入singletonFactoies中。【ObjectFactory是一个函数式接口,所以支持Lambda表达式:()->getEarlyReference(beanName, mbd, bean)

ObjectFactory(上文的Lambda表达式),中间有getEarlyBeanReference方法【获取代理对象】,注意此时我们存入singletonFactories时不会执行Lambda表达式,也就是不会执行getEarlyBeanReference方法。

getEarlyBeanReference():①得到cachekey(beanName) ②将beanName和bean(原始对象)存入earlyProxyReferences ③调用wraplfNecessary进行AOP,得到代理对象

1、从singletonFactories根据beanName得到ObjectFactory,然后执行其getEarlyBeanReference方法,此时会得到A经过AOP后的代理对象,然后将其放入earlySingletonObjects中,此时并没有将代理对象放入singletonObjects中
2、earlySingletonObjects作用:此时,我们只得到了A的代理对象,这个对象还不完整,因为还未进行属性注入,所以此时只能将A的代理对象放入earlySingletonObjects(singletonObjects单例池中放入的是全部生命周期后的bean),这样就能保证其他依赖了A对象的类获取到的就是同一个代理对象了。
3、在B创建完之后,A继续进行生命周期,在A完成属性注入后,会按照本来的逻辑进行AOP,而此时A的原始对象已经经历过了AOP,所以对于A本身而言就不会再去进行AOP了。

如何判断是否已经进行AOP?
会利用上文提到的earlyProxyReferences,在AbstractAutoProxyCreator的postProcessAfterInitialization方法中,会去判断档期按beanName是否存在earlyProxyReferences,如果存在则表示已经提前进行过AOP,无需再次进行。

4、对于A而言,进行AOP判断以及BeanPostProcessor(AOP实现)执行后,需要将A对应的对象放入singletonObjects中,但是,此时应该是从earlySingletonObjects中得到A的代理对象,然后放入singletonObjects(单例池)中

至此,整个循环依赖解决完毕。

4 总结

  1. singletonObjects:缓存某个经历了完整生命周期的bean
  2. earlySingletonObjects:缓存提前拿到原始对象并进行了AOP之后的代理对象;未进行属性注入的以及后续的BeanPostProcessor【AOP】等生命周期的bean
  3. singletonFactories:缓存的是一个ObjectFactory,用来生成进行了AOP之后的代理对象(在每个Bean的生成过程中都会提前暴露一个工厂),这个工厂可能用得到,也可能用不到
    4.earlyProxyReferences:记录对象是否进行了AOP

面试——Spring中的循环依赖相关推荐

  1. 面试:讲一讲Spring中的循环依赖

    前言 Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃 ...

  2. 面试必杀技,讲一讲Spring中的循环依赖

    本系列文章: 听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configu ...

  3. Spring中的循环依赖

    目录 一.什么是循环依赖? 二.Bean的生命周期 2.1 Spring Bean 的生命周期 2.2 Bean 的生成步骤 三.三级缓存 3.1三个缓存分别有什么作用 四.思路分析 4.1 为什么 ...

  4. 一起来踩踩 Spring 中这个循环依赖的坑!

    作者:Mythsman blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/ 1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什 ...

  5. Spring中的循环依赖问题

    Spring的的循环依赖问题 文章目录 Spring的的循环依赖问题 一. 简介 1.什么是循环依赖问题? 2.循环依赖有什么影响? 二. 循环依赖复现 三. 解决方案 1. 重新设计 2 使用 @L ...

  6. Spring中的循环依赖及解决,2021Java精选面试实战总结整理

    那么在创建B类的Bean的过程中,如果B类中存在一个A类的a属性,那么在创建B的Bean的过程中就需要A类对应的Bean,但是,触发B类Bean的创建的条件是A类Bean在创建过程中的依赖注入,所以这 ...

  7. Spring中的循环依赖解决详解

    目录 1 什么是循环依赖? 1.1 构造器循环依赖 1.2 field属性注入循环依赖 1.3 field属性注入循环依赖(prototype) 2 循环依赖处理 2.1 构造器循环依赖(无法解决) ...

  8. 闷棍暴打面试官 Spring源码系列: (一) Spring 如何解决循环依赖

    前言 初夏时节, 时间: AM 7.30分左右, 空无一人的健身房里,一个硕大的身体在跑步机上扭动着, 不一会头上便挥汗如雨, 他嘴上还不时嘀咕着 "循环依赖,单例模式,Bean的定位加载注 ...

  9. what?spring已经解决循环依赖了,为啥还报循环依赖错误?

    前言 spring中的循环依赖及三层map解决方案,八股文中重灾区,强如小伙伴们或许一字一句倒背如流了,当年的我也是如此,然而现实狠狠给了我一巴掌,报错虽迟但到 报错一: The dependenci ...

最新文章

  1. HP officejet、PageWide打印机任意代码执行漏洞cve-2017-2741 Tenable发布漏洞检测插件...
  2. 使用阿里云CentOS安装LAMP时,安装PHP扩展需要注意的事情
  3. VTK:Points之PowercrustExtractSurface
  4. java shp求相交面积_shp文件自相交处理的方法
  5. How does JdkRegexpMethodPointcut work
  6. jzoj5123-diyiti【统计,容斥】
  7. 位说法的由来_南方土地庙有榕树的原因,为何会有榕树不容人的说法?
  8. 【WPS表格】快速填充数据的多种方法
  9. Ubuntu20.04环境下samba无法被小米摄像头搜索到的解决方案
  10. 民办三本,我从3K到15K的一年
  11. 基于JSP+SSH的在线租车汽车租赁系统
  12. ovs 支持的full offload action
  13. ubuntu22.04图文安装流程
  14. spring boot集成阿里云短信服务
  15. 头发为什么会从中间断掉_头发总是从中间断怎么办 几个妙招让你告别脱发问题...
  16. Pytorch lstm中batch_first 参数理解使用
  17. C++ 主流报表框架
  18. windows如何显示文件扩展名称?
  19. 简述维特比算法(Viterbi Algorithm)
  20. PID(比例积分微分)介绍

热门文章

  1. 微信不用绑定银行卡、没零钱也一样可以支付,特别适合孩子
  2. ScheduledExecutorService定时任务停止问题
  3. JAVA类加载与初始化顺序
  4. JavaScript 间歇函数在点击事件内的多次生成bug解决方法
  5. 大数据战略:从数据大国到数据强国
  6. 海思芯片-Proc调试信息查看-视频输入输出信息-电压信息
  7. ObjectARX_多重引线MLeader
  8. SR-TE Policy(思科)----PCEP实验
  9. 流媒体学习之路(WebRTC)——GCC分析(1)
  10. oracle tnsping 下载,tnsping命令对tnsname.ora文件的使用