从一个Spring动态代理Bug聊到循环依赖

喜欢省流太长不看的同学们
东老师vme50解决bug
bug是由于postconstuct注解使用时代理类未生效的问题
由于东老师的胡搅蛮缠,讲了讲bug的原因
没有深入讲源码,因为东老师给的钱不够
顺便讲了讲解决思路及原理
解决方案1.自己注入自己 2.使用编程式事务
动态代理及循环依赖的大体逻辑

上周四需要对一个老功能进行迭代,功能开发完毕后,有数个表增加了些字段,需要对相关表模型老数据进行数据清洗。
负责这个功能的好兄弟东老师在预发环境遇到了一个问题,抛给我看看是咋回事,demo代码如下
这个场景很常见

需求改了之前的逻辑,历史数据需要清洗
如果线上和预发环境是一个库(我们就是),为了方便大多数会在预发环境对线上的数据进行清洗
在这里插入图片描述

东老师:浪啊帮我看看这段代码有问题吗
东老师是我的hxd,所以对于这样的诚恳请求
我很有礼貌:滚 没时间
东东老师发出了一段神秘的代码:kfcVYou50
我:不好意思,东老师,刚才怪我我声音太大,咱们互帮互助,我义不容辞,哪个分支我看下,对了,v50到我微信
接着check out,pull
我心想凭借这么多年写bug的经验 ,还有什么风浪没见过
果然这bug只是个别致的小东西罢了
只是东老师像个刚被我抛弃的怨妇,非要我深入讲讲
为了这个神秘的代码kfcvme50,我也只好由着东老师的性子来谈谈这段代码的逻辑
我告诉东老师这次我只在旁边蹭蹭不进去,不深入聊源码
东老师还是大怒,我给了钱的
我说你想吃三碗的粉,只给一碗的钱,想讲源码,得加钱
当然了,我也不会告诉东老师,源码太多,我也不太会,只能说没时间

Bug复现

粗略看了下代码,没啥问题呀,d老师莫不是耍我。
先启动下看看,嚯,还真报错了

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'testPostConstructServiceImpl': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:160) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:415) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) ~[spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405) ~[spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-2.2.13.RELEASE.jar:2.2.13.RELEASE]
	at com.zhanghl.first.FirstApplication.main(FirstApplication.java:26) [classes/:na]
Caused by: java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
	at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69) ~[spring-aop-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at com.zhanghl.first.controller.TestPostConstructServiceImpl.addProduct(TestPostConstructServiceImpl.java:33) ~[classes/:na]
	at com.zhanghl.first.controller.TestPostConstructServiceImpl.init(TestPostConstructServiceImpl.java:59) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:389) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:333) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:157) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	... 16 common frames omitted

Disconnected from the target VM, address: '127.0.0.1:60861', transport: 'socket'

报错找不到当前的代理对象

Cannot find current proxy: Set ‘exposeProxy’ property on Advised to ‘true’ to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

很容易定位到引起这个报错的代码

((TestPostConstructServiceImpl)AopContext.currentProxy()).save();
在这里插入图片描述

说明一下这个地方为什么要用AopContext.currentProxy(),大伙熟悉**@Transactional**注解的应该都很清楚,有如下几种情况导致事务注解会失效

1.非public修复的方法
2.同一个类普通方法调用事务方法
3.rollobackFor属性异常类不对
4.异常被catch
5.数据库本身不支持事务等

这个demo代码中符合上面的第二种情形,东老师使用的没毛病。
当然了,也可以使用编程式事务的方式可以代替注解的使用,从而不需要使用AopContext.currentProxy(),也就能避免这种情况的发生,但本文就这种情况进行讨论。


首先我们来注释掉@PostConstruct正常调用下接口看看,返回正常
在这里插入图片描述
在这里插入图片描述
再看下添加了@PostConstruct之后的报错提示

Cannot find current proxy

结论

回想一下之前看过的Spring源码,背过的八股文,Bean的生命周期,Spring动态代理,Bean的循环依赖。
得出结论:@PostConstruct起作用时,TestPostConstructServiceImpl的代理类并没有生成。

东老师:怎么得出来的这个结论
我:你这属于技术咨询,得加钱
东老师大怒:歪日 你这黑心商人,v你50 你忘了,我举报你
我一看东老师宁为玉碎,不为瓦全的铁骨铮铮模样,决定大发慈悲,拯救下无知的办公室老年人了


@PostConstruct的在Bean的生命周期的哪一步

根据异常信息,我们很容易就可以定位到报错的那行代码。
AbstractAutowireCapableBeanFactory 的initializeBean() Bean的初始化方法,在Bean populateBean() 填充属性之后
initializeBean 的主要作用有如下几点

  1. 如果实现了Aware接口,回调相关Aware接口的逻辑 例如 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware等接口
  2. 执行BeaPostProcessor的前置回调方法
  3. 执行初始化方法
  4. 执行BeanPostProcessor的后置回调方法

在这里插入图片描述

其中@PostConstruct执行的地方在第二步applyBeanPostProcessorsBeforeInitialization()前置回调,在将断点打在这里,进去看看堆栈信息,和执行过程。
在这里插入图片描述
这里我们快速定位到CommonAnnotationBeanPostProcessor,通过名字我们就知道它的作用了,通用的注解后置处理器
在执行CommonAnnotationBeanPostProcessor的前置回调时,看到了我们遇到的报错信息,没有找到代理类。
在这里插入图片描述

一般代理类的生成时机在生命周期的哪一步

一般,注意我们这里说的一般代理类,Spring中代理类的生成时机不止一个地方,后面我们的解决方案也会再讲一下。
一般代理类生成时机是在我们上面说的第四步

执行BeanPostProcessor的后置回调方法

我们去掉@PostConstruct注解再次Debug看下
AnnotationAwareAspectJAutoProxyCreator Spring代理实现核心AbstractAutoProxyCreator的子类
AnnotationAwareAspectJAutoProxyCreator 在后置处理方法中对原有目标类进行代理,生成了代理类。
在这里插入图片描述
Spring的动态代理核心逻辑就在这里,很重要,建议自己debug瞅瞅
AbstractAutoProxyCreator 的wrapIfNecessary()方法
AbstractAutoProxyCreator 的wrapIfNecessary()方法
AbstractAutoProxyCreator 的wrapIfNecessary()方法
它实现了 BeanPostProcessor、SmartInstantiationAwareBeanPostProcessor 和 InstantiationAwareBeanPostProcessor 三个接口,那么这个类就是 Spring AOP 的入口,在这里将 Advice 织入我们的 Bean 中,创建代理对象。
在这里插入图片描述
这块后置逻辑处理完,可以看到我们收获了当前类的代理类
在这里插入图片描述

东老师:你小子不戳哦,那我该怎么解决呢


解决办法
两个思路
1.不生成代理类

这种很好理解,这种思路是不通过切面代理的方式对方法进行事务处理
比如说将事务方法单独抽出一个类
当然更好的方式是使用编程式事务,此处不再赘述
或者在应用程序启动完毕之后,通过调用接口的方式对数据进行初始化

2.在生成代理类之后再进行数据的初始化

东老师:既然上面的方法可以了,你可以告退了
我意犹未尽:东老师,难道你不想做我就是我不一样的花火吗,难道你就想和外面的妖艳贱货一样吗,难道你不想进去看看吗
东老师挣扎了下,想起来自己还v我50不能亏决定继续听我bb。

正如我上面所说一般代理类生成时机是在Bean的初始化之后,
也就是上面说的AbstractAutowireCapableBeanFactory 的initializeBean()第四步,这种场景下我们无法实现。
基于上述的业务场景,Spring作为当前Java生态里最流行的框架,当然也提供了解决方法。
自己注入自己
在这里插入图片描述

我们可以看下执行结果
在这里插入图片描述


解决方法的原理

带着疑问来看看为什么Bean自己注入自己的方式能生效
我们只需要专注以下两个个问题

1.是否生成了代理类
2.如果是,那么代理类是在什么时候生成的

和上文分析一样,我们还是使用之前的断点,看下@PostConstruct注解被执行的时候上下文,也就是initializeBean()方法中执行BeanPostProcessor接口前置回调方法时候,其中CommonAnnotationBeanPostProcessor的前置回调方法,会处理@PostConstruct注解。
看下CommonAnnotationBeanPostProcessor的类注释
在这里插入图片描述
执行初始化的方法
在这里插入图片描述
invokeInitMethods(Object target, String beanName)
Target就是我们的当前类,看下图的堆栈信息当前Target是一个普通对象,这个对象包含两个属性
1.productMapper不重要不谈
2.testPostConstructService 重点

这不是我们当前Bean注入自己的属性嘛
可以看到这个属性是个代理类,解答了我们的第一个问题 --‘是否生成了代理类’
这个属性可以看到它本身的另外两个属性是空的,说明这个代理类是个不成熟的Bean(早期Bean,说到这是不是想到了什么)
注意哈 此时Spring中对于TestPostConstructServiceImpl demo类存在两个Bean,一个 当前Bean,一个早期代理Bean

在这里插入图片描述
当前Target,也就是TestPostConstructServiceImpl要执行的init方法。
在这里插入图片描述


从上述分析可知,在Bean的初始化完成之前,Bean自己注入自己,会产生一个早期代理对象来当作属性。
接下来我们的问题是
早期代理Bean是什么时候生成的
以及
为什么要生成早期代理Bean


早期代理Bean是什么时候生成的

当前场景下,TestPostConstructServiceImpl自己注入自己,注入的这个Bean是个早期代理对象
在这里插入图片描述
那么我们只需要看Bean在属性填充的时候,是怎么生成这个代理对象的就可以了。

断点打在AbstractAutowireCapableBeanFactorypopulateBean() 填充属性的方法上。
在这里插入图片描述
populateBean()方法内部,逻辑很多,如下所示,InstantiationAwareBeanPostProcessor这个后置处理器就是我们寻找了好久的小宝贝,
这个接口InstantiationAwareBeanPostProcessor在SpringBean的生命周期中占据着核心位置。
在这里插入图片描述AutowiredAnnotationBeanPostProcessor实现了InstantiationAwareBeanPostProcessor,其中
postProcessProperties()中
inject()方法也是我们通常所说的依赖注入的由来
看下类注释
此处将@Autowired 修饰的属性注入到当前Bean
在这里插入图片描述

循环依赖

1.对于普通的Bean注入这块逻辑很简单,再去获取需要注入Bean的实例,走一遍创建的逻辑,将需要注入进来的Bean实例化给当前Bean即可
2.对于我们这个场景,你想到了什么,自己注入自己,自己依赖自己,这不就妥妥的循环依赖嘛
来看下第二点,这种循环依赖是怎么解决的
继续跟着inject,往里面探究,是怎么给当前Bean注入自己的。
在这里插入图片描述
注意看上图在inject()的执行过程中,又执行到了==getBean()==方法,开始了套娃之旅,注意和左边的堆栈信息对比,那么当前Bean还会执行创建一次吗。
下图,跟着执行可以看到注入的这个Bean getSingleton()又开始执行了
但是从三级缓存里拿到的对象工厂并不为空
但是从三级缓存里拿到的对象工厂并不为空
但是从三级缓存里拿到的对象工厂并不为空
这个是和当前Bean的最大区别,当前Bean第一次创建过程中,这个地方为null。
在这里插入图片描述
再来看一下,这个ObjectFactory对象工厂偷偷摸摸的干了啥,一个函数式接口
在这里插入图片描述
在这里插入图片描述
执行进入getObject()方法
在这里插入图片描述

嚯,这不就是懒加载放进去了个早期Bean引用吗
那么 :
1.这个引用是个啥,
2.什么时候放进去的呢

先看看这个引用是什么
画黑板,敲重点,如下方法注释,为了解决循环依赖
在这里插入图片描述这样的逻辑我们应该看到了很多次,遍历所有的BeanPostProcessor(),在Bean生命周期的不同阶段处理不同的回调逻辑,Spring通过这种方式在Bean生命周期的过程中,有着很强的拓展性。
这次需要处理的是SmartInstantiationAwareBeanPostProcessor ==getEarlyBeanReference()方法
老规矩 先看注释
核心大意是 :让Bean提前暴露出去,以便尽早访问指定的bean,通常用于解析循环引用
在这里插入图片描述
在SmartInstantiationAwareBeanPostProcessor的实现类AnnotationAwareAspectJAutoProxyCreator
中调用了AbstractAutoProxyCreator 的getEarlyBeanReference();
来跟我念一下
AbstractAutoProxyCreator,wrapIfNecessary()
AbstractAutoProxyCreator,wrapIfNecessary()
== AbstractAutoProxyCreator,wrapIfNecessary()

这不就是我们强调的Spring动态代理的核心逻辑吗
也就是在这里我们获得了当前Bean的Cglib代理类

东老师:哦吼,详细说说这块逻辑
我:滚,再v50
东老师:当我没说

wrapIfNecessary这里的逻辑本文不过多讨论,因为我写不完。
在这里插入图片描述
这个引用就是通过Spring的AbstractAutoProxyCreator给当前Bean创建一个动态代理
那么回到第二个问题
这个代理什么时候放进去的呢

东老师听到这,瞬间羞涩:你好坏,什么进去不进去的
我大怒:滚

我们回到当前Bean的创建过程中,注意我说的不是作为属性需要注入的Bean,
而是我们Bean第一次初始化的过程中 AbstractAutowireCapableBeanFactory 的doCreateBean()方法

在这里插入图片描述
如上图,在当前Bean的填充属性及初始化之前,我们将这个懒加载的函数放进了三级缓存中。
然后就是我们上面讨论的流程

  1. 填充属性
  2. 依赖注入
  3. 从三级缓存中获取要注入Bean的实例(实际是这个懒加载函数)
  4. 执行懒加载的函数:AbstractAutoProxyCreator创建代理类后
  5. 注入获取到的代理类

最终在当前Bean初始化阶段initializeBean()执行int方法时
可以拿到注入的代理类,从而可以顺利执行

((TestPostConstructServiceImpl)AopContext.currentProxy()).save();
结语

东老师心花怒放:浪啊你真棒
我:滚,送你张图吧 东老师 好好学习,小心脱下长衫,黄袍加身,美团小哥欢迎你

在这里插入图片描述

如果你看完了这篇文章,恭喜你又get了一些没什么diao用的知识点

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值