一、场景分析

最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务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

JDK动态代理给Spring事务埋下的坑!的更多相关文章

  1. (转)面试必备技能:JDK动态代理给Spring事务埋下的坑!

    一.场景分析 最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在Controller里边同时调用这两个方法,示例代码如下: 1.场景A ...

  2. 通过JDK动态代理实现 Spring AOP

    1.新建一个目标类 接口:public interface IUserService //切面编程 public void addUser(); public void updateUser( ); ...

  3. 有点深度的聊聊JDK动态代理

    在接触SpringAOP的时候,大家一定会被这神奇的功能所折服,想知道其中的奥秘,底层到底是如何实现的.于是,大家会通过搜索引擎,知道了一个陌生的名词:动态代理,慢慢的又知道了动态代理有多种实现方式, ...

  4. AOP jdk动态代理

    一: jdk动态代理是Spring AOP默认的代理方法.要求 被代理类要实现接口,只有接口里的方法才能被代理,主要步骤是先创建接口,接口里创建要被代理的方法,然后定义一个实现类实现该接口,接着将被代 ...

  5. Spring声明式事务的实现方式选择(JDK动态代理与cglib)

    1.简介 Spring声明式事务的具体实现方式是动态决定的,与具体配置.以及事务代理对象是否实现接口等有关. 2.使用JDK动态代理的情况 在满足下面两个条件时,Spring会选择JDK动态代理作为声 ...

  6. Spring AOP详解 、 JDK动态代理、CGLib动态代理

    AOP是Aspect Oriented Programing的简称,面向切面编程.AOP适合于那些具有横切逻辑的应用:如性能监测,访问控制,事务管理以及日志记录.AOP将这些分散在各个业务逻辑中的代码 ...

  7. jdk动态代理与cglib代理、spring aop代理实现原理

    原创声明:本博客来源与本人另一博客[http://blog.csdn.net/liaohaojian/article/details/63683317]原创作品,绝非他处摘取 代理(proxy)的定义 ...

  8. jdk动态代理与cglib代理、spring aop代理实现原理解析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

  9. 何为代理?jdk动态代理与cglib代理、spring Aop代理原理浅析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

随机推荐

  1. STM32-对芯片启动读保护,实现加密(详解)

    STM32可以对存储在flash上的程序进行读保护. 启动读保护后,用户就不能再读写程序了. 所以,在烧写程序之前,需要程序调用关闭读保护.关闭读保护后,会自动清空flash上的程序 头文件位于:#i ...

  2. trivial and nontrivial

    Trivial A solution or example that is ridiculously simple and of little interest. Often, solutions o ...

  3. 使用JSDoc自动生成代码文档

    译者按: 代码要有规范的注释,遵从jsDoc规则来注释可以生成有用的文档. 原文: Generate docs and host it with JSDoc and GitHub Pages 译者:  ...

  4. php生成xml数据

    1.php生成xml数据一般有2种方式, 一个是组装字符串,另一个就是使用php内置的系统类 2.使用php内置类生成xml数据 3.拼装字符串生成xml数据 public function stat ...

  5. Elasticsearch Query DSL

    Elasticsearch Query DSL By:授客 QQ:1033553122 1. match_all 1 2. match 2 3. match_phrase 5 4. match_phr ...

  6. Glide图片加载框架小bug

    如上一段加载图片的代码,本身是没问题的,后来测试发现有情况不显示url对应的图片,而一直显示加载超时的图片 修改如下: 将with()方法的上下文context改为图片的imageView.getCo ...

  7. 转载:python生成以及打开json、csv和txt文件

    原文地址:https://blog.csdn.net/weixin_42555131/article/details/82012642 生成txt文件: mesg = "hello worl ...

  8. git 入门教程之本地仓库

    本地仓库 背景 创建工作目录 平时工作时我们习惯对文档分门别类进行管理,.doc .txt 等文本类型的文件习惯存在 doc文件下,开发java js 等源代码文件存在在 src 目录下,这一点很好理 ...

  9. (其他)Thinkpad笔记本装系统

    电脑城装一次系统收你40元,不如自己装系统. 虽然百度上装系统的文章泛滥,但是还是自己尝试. 前3个小时thinkpad e570是不是坏掉了,怎么就进不去BIOS,这个时候直接搜索这个型号,问题输入 ...

  10. Android getprop 读取的属性哪里来的?

    Android  getprop 和  setprop 可以对系统属性进行读取和设置. 通过串口执行以下 geyprop    打印出来的属性让你一目了然. 属性出来了,但是在哪里设置的呢,这里有两个 ...