面试——Spring中的循环依赖
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 总结
- singletonObjects:缓存某个经历了完整生命周期的bean
- earlySingletonObjects:缓存提前拿到原始对象并进行了AOP之后的代理对象;未进行属性注入的以及后续的BeanPostProcessor【AOP】等生命周期的bean
- singletonFactories:缓存的是一个ObjectFactory,用来生成进行了AOP之后的代理对象(在每个Bean的生成过程中都会提前暴露一个工厂),这个工厂可能用得到,也可能用不到
4.earlyProxyReferences:记录对象是否进行了AOP
面试——Spring中的循环依赖相关推荐
- 面试:讲一讲Spring中的循环依赖
前言 Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃 ...
- 面试必杀技,讲一讲Spring中的循环依赖
本系列文章: 听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configu ...
- Spring中的循环依赖
目录 一.什么是循环依赖? 二.Bean的生命周期 2.1 Spring Bean 的生命周期 2.2 Bean 的生成步骤 三.三级缓存 3.1三个缓存分别有什么作用 四.思路分析 4.1 为什么 ...
- 一起来踩踩 Spring 中这个循环依赖的坑!
作者:Mythsman blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/ 1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什 ...
- Spring中的循环依赖问题
Spring的的循环依赖问题 文章目录 Spring的的循环依赖问题 一. 简介 1.什么是循环依赖问题? 2.循环依赖有什么影响? 二. 循环依赖复现 三. 解决方案 1. 重新设计 2 使用 @L ...
- Spring中的循环依赖及解决,2021Java精选面试实战总结整理
那么在创建B类的Bean的过程中,如果B类中存在一个A类的a属性,那么在创建B的Bean的过程中就需要A类对应的Bean,但是,触发B类Bean的创建的条件是A类Bean在创建过程中的依赖注入,所以这 ...
- Spring中的循环依赖解决详解
目录 1 什么是循环依赖? 1.1 构造器循环依赖 1.2 field属性注入循环依赖 1.3 field属性注入循环依赖(prototype) 2 循环依赖处理 2.1 构造器循环依赖(无法解决) ...
- 闷棍暴打面试官 Spring源码系列: (一) Spring 如何解决循环依赖
前言 初夏时节, 时间: AM 7.30分左右, 空无一人的健身房里,一个硕大的身体在跑步机上扭动着, 不一会头上便挥汗如雨, 他嘴上还不时嘀咕着 "循环依赖,单例模式,Bean的定位加载注 ...
- what?spring已经解决循环依赖了,为啥还报循环依赖错误?
前言 spring中的循环依赖及三层map解决方案,八股文中重灾区,强如小伙伴们或许一字一句倒背如流了,当年的我也是如此,然而现实狠狠给了我一巴掌,报错虽迟但到 报错一: The dependenci ...
最新文章
- HP officejet、PageWide打印机任意代码执行漏洞cve-2017-2741 Tenable发布漏洞检测插件...
- 使用阿里云CentOS安装LAMP时,安装PHP扩展需要注意的事情
- VTK:Points之PowercrustExtractSurface
- java shp求相交面积_shp文件自相交处理的方法
- How does JdkRegexpMethodPointcut work
- jzoj5123-diyiti【统计,容斥】
- 位说法的由来_南方土地庙有榕树的原因,为何会有榕树不容人的说法?
- 【WPS表格】快速填充数据的多种方法
- Ubuntu20.04环境下samba无法被小米摄像头搜索到的解决方案
- 民办三本,我从3K到15K的一年
- 基于JSP+SSH的在线租车汽车租赁系统
- ovs 支持的full offload action
- ubuntu22.04图文安装流程
- spring boot集成阿里云短信服务
- 头发为什么会从中间断掉_头发总是从中间断怎么办 几个妙招让你告别脱发问题...
- Pytorch lstm中batch_first 参数理解使用
- C++ 主流报表框架
- windows如何显示文件扩展名称?
- 简述维特比算法(Viterbi Algorithm)
- PID(比例积分微分)介绍
热门文章
- 微信不用绑定银行卡、没零钱也一样可以支付,特别适合孩子
- ScheduledExecutorService定时任务停止问题
- JAVA类加载与初始化顺序
- JavaScript 间歇函数在点击事件内的多次生成bug解决方法
- 大数据战略:从数据大国到数据强国
- 海思芯片-Proc调试信息查看-视频输入输出信息-电压信息
- ObjectARX_多重引线MLeader
- SR-TE Policy(思科)----PCEP实验
- 流媒体学习之路(WebRTC)——GCC分析(1)
- oracle tnsping 下载,tnsping命令对tnsname.ora文件的使用