JDK动态代理给Spring事务埋下的坑!
一、场景分析
最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在controller里边同时调用这两个方法,示例代码如下:
1、场景a:
这里其实是分别执行了两个事物,执行的结果是两个方法都可以插入数据!如下:
2、场景b:
修改上述代码如下:
propagation.requires_new的含义表示:如果当前存在事务,则挂起当前事务并且开启一个新事物继续执行,新事物执行完毕之后,然后在缓刑之前挂起的事务,如果当前不存在事务的话,则开启一个新事物。
执行的结果是两个方法都可以插入数据!执行结果如下:
场景a和场景b都是正常的执行,期间没有发生任何的回滚,假如child()方法中出现了异常!
3、场景c
修改child()的代码如下所示,其他代码和场景b一样:
执行结果如下,会出现异常,并且数据都没有插入进去:
疑问1:场景c中child()抛出了异常,但是parent()没有抛出异常,按道理是不是应该parent()提交成功而child()回滚?
可能有的小伙伴要说了,child()抛出了异常在parent()没有进行捕获,造成了parent()也是抛出了异常了的!所以他们两个都会回滚!
4、场景d
按照上述小伙伴的疑问这个时候,如果对parent()方法修改,捕获child()中抛出的异常,其他代码和场景c一样:
然后再次执行,结果是两个都插入了数据库:
看到这里很多小伙伴都可能会问,按照我们的逻辑来想的话child()中抛出了异常,parent()没有抛出并且捕获了child()抛出了异常!执行的结果应该是child()回滚,parent()提交成功的啊!
疑问2:场景d为什么不是child()回滚和parent()提交成功哪?
上述的场景c和场景d似乎融为了一题,要么都成功要么都失败!和我们预期的效果一点都不一样!看到这里这就是我们今天要探讨的主题《jdk动态代理给spring事务埋下的坑!》接下来我们就分析一下spring事物在该特定场景下不能回滚的深层次原因!
二、问题本质所在
我们知道spring事务管理是通过jdk动态代理的方式进行实现的(另一种是使用cglib动态代理实现的),也正是因为动态代理的特性造成了上述parent()方法调用child()方法的时候造成了child()方法中的事务失效!简单的来说,在场景d中parent()方法调用child()方法的时候,child()方法的事务是不起作用的,此时的child()方法像一个没有加事务的普通方法,其本质上就相当于下边的代码:
场景c本质:
场景d本质:
正如上述的代码,我们可以很轻松的解释疑问1和疑问2,因为动态代理的特性造成了场景c和场景d的本质如上述代码。在场景c中,child()抛出异常没有捕获,相当于parent事务中抛出了异常,造成parent()一起回滚,因为他们本质是同一个方法;在场景d中,child()抛出异常并进行了捕获,parent事务中没有抛出异常,parent()和child()同时在一个事务里边,所以他们都成功了;
看到这里,那么动态代理的这个特性到底是什么才会造成spring事务失效那?
三、动态代理的这个特性到底是什么?
首先我们看一下一个简单的动态代理实现方式:
此时我们执行以下测试方法,注意了此时是同时调用了test1()和test2()的,执行结果如下:
可以看出,在orderserviceimpl 类中由于test1()没有调用test2(),他们方法的执行都是使用了代理的,也就是说test1和test2都是通过代理对象调用的invoke()方法,这和我们场景a和b类似。
加入我们模拟一下场景c和场景d在test1()中调用test2(),那么代码修改为如下:
执行结果如下:
这里可以很清楚的看出来test1()走的是代理,而test2()走的是普通的方法,没有经过代理!看到这里你是否已经恍然大明白了呢?
这个应该可以很好的理解为什么是这样子!这是因为在java中test1()中调用test2()中的方法,本质上就相当于把test2()的方法体放入到test1()中,也就是内部方法,同样的不管你嵌套了多少层,只有代理对象proxy 直接调用的那一个方法才是真正的走代理的,如下:
测试方法和上边的测试方法一样,执行结果如下:
记住:只有代理对象proxy直接调用的那个方法才是真正的走代理的!
四、如何解决这个坑?
上文的分析中我们已经了解了为什么在该特定场景下使用spring事务的时候造成事务无法回滚的问题,下边我们谈一下几种解决的方法:
1、我们可以选择逃避这个问题!我们可以不使用以上这种事务嵌套的方式来解决问题,最简单的方法就是把问题提到service或者是更靠前的逻辑中去解决,使用service.xxxtransaction是不会出现这种问题的。
2、通过aopproxy上下文获取代理对象:
(1)springboot配置方式:注解开启 exposeproxy = true,暴露代理对象 (否则aopcontext.currentproxy()) 会抛出异常。
添加依赖:
添加注解:
修改原有代码的执行方式为:
此时的执行结果为:
可见,child方法由于异常已经回滚了,而parent可以正确的提交,这才是我们想要的结果!注意的是在parent调用child的时候是通过try/catch捕获了异常的!
(2)传统spring xml配置文件只需要添加依赖个设置如下配置即可,使用方式一样:
<aop:aspectj-autoproxy expose-proxy="true"/>
3、通过applicationcontext上下文进行解决:
执行结果符合我们的预期:
五、总结
到此为止,我们简单的介绍了一下spring事务管理中如果业务中有像场景c或者场景d的情况时,如果不清楚jdk动态代理造成spring事务无法回滚的问题的话就可能是一个开发事故了,说不定是要扣工资的!
---------------------
原文地址:https://blog.csdn.net/bntx2jsqfehy7/article/details/79040349
上一篇: 模板方法精讲
推荐阅读
-
详解Spring的两种代理方式:JDK动态代理和CGLIB动态代理
-
JDK动态代理给Spring事务埋下的坑!
-
jdk动态代理的实现流程(事务处理)
-
Spring AOP 代理实现的两种方式: JDK动态代理 和 Cglib框架动态代理
-
Spring AOP注解失效的坑及JDK动态代理
-
【Spring AOP源码】基于JDK动态代理和Cglib创建代理对象的原理分析
-
解析动态代理jdk的Proxy与spring的CGlib(包括区别介绍)
-
JDK动态代理给Spring事务埋下的坑!
-
Java--简单的Spring AOP配置以及AOP事物管理,JDK/GCLib动态代理
-
Java--简单的Spring AOP配置以及AOP事物管理,JDK/GCLib动态代理