一.前言
「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
hello,everyone。
spring的循环依赖问题是面试的时候经常会碰到的问题。相信很多朋友都看相关spring使用三级缓存解决循环依赖的博文。面试官问你的时候除了想要了解你对spring框架的熟悉程度,还想要了解你对spring循环依赖的思考。
你上来直接说spring使用了三级缓存解决了循环依赖,那你就要回家等通知了。
前几天写需求的时候,整合了几个方法逻辑的时候,碰到了一个循环依赖的bug。
借着这个bug的排查思路给大家讲讲spring循环依赖中几个小坑。
本文重点并非spring循环依赖源码解读,默认你对spring循环依赖有过简单的了解。
我贴心点吧,贴一下大神A哥的blog:一文告诉你Spring是如何利用“三级缓存“巧妙解决Bean的循环依赖问题的
二.bug缘由
博主在进行一个需求开发的时候,需要调用几个现有接口的逻辑,但是它们原先的方法是私有的,并且好几个逻辑定义在controller层【历史原因,强烈谴责此种做法!】
。
为了方法复用,我将controller中对应通用逻辑进行剥离同步到对应的service中,并且注入了相关依赖的一些bean。
然后代码结构变成了,serviceA注入了serviceB,serviceB注入了serviceA
然后项目一启动就报错了
1 | dart复制代码Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'appManagerServiceImpl': Bean with name 'appManagerServiceImpl' has been injected into other beans [deviceManagerServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example. |
这个其实我当时还挺诧异的,我在不看绝对后悔的@Async深度解析【不仅仅是源码那么简单】这篇文章中4.3提到过使用@Async,互相注入bean会导致循环依赖。
但是我在这两个bean中全局搜索@Aysnc,也没有搜索到。
然后也没有看到构造器注入的场景。
so,看来还是只能调一下源码吧。
三.bug定位
看到这个bug,直接定位到堆栈报出来的错误行
报错比较简单直观,暴露对象与关联的bean不是同一个对象,在这里打一个条件断点:
exposedObject != bean
发现bean是原始对象,exposedObject是代理对象。
借用一下A哥的图
spring解决循环依赖时,beanB去获取beanA时,beanA如果切面处理,那么beanB关联beanA时,会调用
1 | ini复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { |
生成代理对象,将beanA从三级缓存中删除,生成代理对象放置到二级缓存中。
但是由于getEarlyBeanReference方法中仅对类型为SmartInstantiationAwareBeanPostProcessor
的后置处理器进行代理处理。如果是其他的类型的BeanPostProcessor
,将不会在此处做增强。
ok,我们再回过头看一下上面的流程图,bean加载最后的逻辑在
1 | ini复制代码exposedObject = initializeBean(beanName, exposedObject, mbd); |
这一行,最后处理bean的逻辑
1 | scss复制代码protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { |
分别打断点在这里
能够看到这两行的wrappedBean对象不一样,一个是原始对象,换一个是代理对象。
看到曙光,再进去看看
1 | ini复制代码@Override |
断点一打
逐个排查
发现bean被MethodValidationPostProcessor
增强处理了!!!!
点击这个类进去,被标注了@Validated
的类将被代理增强。
四.bug解决
bug解决就很简单了,发现在service上标注了 @Validated,为了校验方法入参。
我就简单粗暴,把校验逻辑写了一个方法单独处理。
五.总结
其实这篇文章的排查思路跟@Async那篇文章的排查思路是一模一样的,但是我在排查的时候有增加了不少的思考。
1.spring本身帮助我们解决了属性注入方式的循环依赖。但是如果循环依赖的bean,被除SmartInstantiationAwareBeanPostProcessor
的后置处理器代理到,那么还是会产生循环依赖的报错。
2.spring无法为我们解决构造器循环依赖,因为三级缓存的最开始操作就是要对bean实例化放入到三级缓存。
3.使用 @Lazy去解决类似本文的这种bug,是可行的。比如B希望依赖进来的是最终的代理对象进来,所以B加上即可,A上并不需要加。但是实际上,此种情况下B里持有A的引用和Spring容器里的A并不是同一个
。 【本强迫症患者看来,治标不治本】
4.其实二级缓存也能解决注入循环依赖,但是为什么要使用三级缓存?spring还是期望bean的声明周期是符合spring的设计规范的,类似于二级缓存的早期曝光提前生成代理的方式,是为了系统的健壮性考虑。
5.谨慎使用:allowRawInjectionDespiteWrapping,把这个置为true后会针对循环内的bean不进行校验,但是代理会失效了。
六.联系我
文中如有不正确之处,欢迎指正,写文不易,点个赞吧,么么哒~
钉钉:louyanfeng25
微信:baiyan_lou
公众号:柏炎大叔
本文转载自: 掘金