从一个Spring动态代理Bug聊到循环依赖
文章目录
- Bug复现
- 结论
- @PostConstruct的在Bean的生命周期的哪一步
- 一般代理类的生成时机在生命周期的哪一步
- 解决办法
- 两个思路
- 1.不生成代理类
- 2.在生成代理类之后再进行数据的初始化
- 解决方法的原理
- 早期代理Bean是什么时候生成的
- 循环依赖
- 结语
喜欢省流太长不看的同学们
东老师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 omittedDisconnected 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 的主要作用有如下几点
- 如果实现了Aware接口,回调相关Aware接口的逻辑 例如 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware等接口
- 执行BeaPostProcessor的前置回调方法
- 执行初始化方法
- 执行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在属性填充的时候,是怎么生成这个代理对象的就可以了。
断点打在AbstractAutowireCapableBeanFactory 的populateBean() 填充属性的方法上。
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的填充属性及初始化之前,我们将这个懒加载的函数放进了三级缓存中。
然后就是我们上面讨论的流程
- 填充属性
- 依赖注入
- 从三级缓存中获取要注入Bean的实例(实际是这个懒加载函数)
- 执行懒加载的函数:AbstractAutoProxyCreator创建代理类后
- 注入获取到的代理类
最终在当前Bean初始化阶段initializeBean()执行int方法时
可以拿到注入的代理类,从而可以顺利执行
((TestPostConstructServiceImpl)AopContext.currentProxy()).save();
结语
东老师心花怒放:浪啊你真棒
我:滚,送你张图吧 东老师 好好学习,小心脱下长衫,黄袍加身,美团小哥欢迎你
如果你看完了这篇文章,恭喜你又get了一些没什么diao用的知识点
从一个Spring动态代理Bug聊到循环依赖相关推荐
- 【Spring AOP】静态代理设计模式、Spring 动态代理开发详解、切入点详解(切入点表达式、切入点函数)
AOP 编程 静态代理设计模式 1. 为什么需要代理设计模式 2. 代理设计模式 名词解释 代理开发的核心要素 静态代理编码 静态代理存在的问题 Spring 动态代理开发 搭建开发环境 Spring ...
- 【Spring源码三千问】Spring动态代理:什么时候使用的 cglib,什么时候使用的是 jdk proxy?
Spring动态代理:什么时候使用的 cglib,什么时候使用的是 jdk proxy? 前言 版本约定 正文 例子测试 结论分析 proxyTargetClass 标识的校正 哪些接口不是 Reas ...
- 实现一个基于动态代理的 AOP
实现一个基于动态代理的 AOP Intro 上次看基于动态代理的 AOP 框架实现,立了一个 Flag, 自己写一个简单的 AOP 实现示例,今天过来填坑了 目前的实现是基于 Emit 来做的,后面有 ...
- 京东一面:Spring 为何需要三级缓存解决循环依赖,而不是二级缓存?我懵了。。...
欢迎关注方志朋的博客,回复"666"获面试宝典 来源:cnblogs.com/semi-sub/p/13548479.html 前言 bean生命周期 三级缓存解决循环依赖 总结 ...
- Spring IOC 容器源码分析 - 循环依赖的解决办法
1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的.在本篇文章中,我会首先向大家介绍一下什么是循环依赖.然后,进入源码分析阶段.为了更好的说明 Spring 解决循环依赖的办法,我 ...
- Spring 通过 @Lazy 注解解决构造方法循环依赖问题
什么是循环依赖? 先定义两个类 Apple.Orange,如下所示: @Component public class Apple{@Autowiredprivate Orange orange; }@ ...
- 【干货】JDK动态代理的实现原理以及如何手写一个JDK动态代理
动态代理 代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中,更是有举足轻重的地位.代理模式从类型上来说,可以分为静态代理和动态代理两种类型. 在解 ...
- spring 动态代理_分析动态代理给 Spring 事务埋下的坑
前言 Spring的声明式事务让我们不在编写获得连接.关闭连接.开启事务.提交事务.回滚事务等代码,通过一个简单的@Transactional注解,就让我们轻松进行事务处理.我们知道Spring事务基 ...
- Spring动态代理机制理解
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
最新文章
- python怎么导入包-python模块之导入包及模块发布
- 压缩base 64字符串_ftp下载多个文件,ftp下载多个文件打包成一个压缩包
- Effective Java之谨慎地实现Serializable(七十四)
- dc持久内存与mysql_Calypso Systems推出测试软件和服务器测试傲腾数据中心级持久内存...
- 谷歌核心算法大更新,如何趋利避害对电商网站排名影响?
- 支援日本/厄瓜多尔震区 Skype推免费通话
- python语法学习_python语法学习笔记
- 手机怎么快速把jpg图片中的文字提取出来
- Glide4 高效加载图片的配置【转】
- LINUX设置终端窗口显示内容的滚动缓冲行数
- ong拼音汉字_拼音ong的正确发音
- 工作台式计算机配置单,台式电脑配置清单.doc
- 云计算是什么?3分钟了解云计算技术
- 死神来了~~~~~~~~
- python的scapy,Scapy在Python脚本中
- 微信小程序扫描二维码、小程序码进入的开发测试
- IOS- 时间格式转换问题(12小时和24小时的区别)
- 享受高清,索尼笔记本电脑
- ege寻宝挑战游戏(C、C++)(大一上游戏项目)
- 查看 CPU架构类型
热门文章
- 分段函数是不是一定初等函数_不可化为一个表达式的分段函数是不是初等函数?,分段函数是不是初等函数,那这个呢?...
- python列表处理函数map
- 拼多多 标题 html,拼多多商家运营时如何绑定关联第三方店铺展示总销量的技巧教程...
- PHP网站分类目录管理系统源码优客365导航源码
- idea配置公司的私服地址
- WebStorm设置px转换rem插件
- Tensorflow 使用cpu和gpu的区别
- 爱奇艺MySQL高可用方案概述
- 哀悼日一位老师在教室里的讲话
- swit - 毛玻璃效果