(转)面试必备技能:JDK动态代理给Spring事务埋下的坑!
一、场景分析
最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在Controller里边同时调用这两个方法,示例代码如下:
1、场景A:
@RestController
@RequestMapping(value = "/test")
public class OrderController {@Autowiredprivate TestService userService;@GetMappingpublic void test() {//同时调用parent和childuserService.parent();userService.child();}
}
@Service
public class TestServiceImpl implements TestService {@Autowiredprivate UserMapper userMapper;@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);}@Override@Transactionalpublic void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);}
}
这里其实是分别执行了两个事务,执行的结果是两个方法都可以插入数据!如下:
2、场景B:
修改上述代码如下:
@RestController
@RequestMapping(value = "/test")
public class OrderController {@Autowiredprivate TestService userService;@GetMappingpublic void test() {userService.parent();}
}
@Service
public class TestServiceImpl implements TestService {@Autowiredprivate UserMapper userMapper;@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);//在parent里边调用childchild();}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);}
}
Propagation.REQUIRES_NEW的含义表示:如果当前存在事务,则挂起当前事务并且开启一个新事务继续执行,新事务执行完毕之后,然后在缓刑之前挂起的事务,如果当前不存在事务的话,则开启一个新事务。
执行的结果是两个方法都可以插入数据!执行结果如下:
场景A和场景B都是正常的执行,期间没有发生任何的回滚,假如child()方法中出现了异常!
3、场景C
修改child()的代码如下所示,其他代码和场景B一样:
@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);child();}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child= new User("张大壮 Child", "654321", 25);userMapper.insert(child);throw new RuntimeException("child Exception....................");}
执行结果如下,会出现异常,并且数据都没有插入进去:
疑问1:场景C中child()抛出了异常,但是parent()没有抛出异常,按道理是不是应该parent()提交成功而child()回滚?
可能有的小伙伴要说了,child()抛出了异常在parent()没有进行捕获,造成了parent()也是抛出了异常了的!所以他们两个都会回滚!
4、场景D
按照上述小伙伴的疑问这个时候,如果对parent()方法修改,捕获child()中抛出的异常,其他代码和场景C一样:
@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);try {child();} catch (Exception e) {e.printStackTrace();}}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);throw new RuntimeException("child Exception....................");}
然后再次执行,结果是两个都插入了数据库:
看到这里很多小伙伴都可能会问,按照我们的逻辑来想的话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事务失效那?
三、动态代理的这个特性到底是什么?
首先我们看一下一个简单的动态代理实现方式:
//接口
public interface OrderService {void test1();void test2();
}//接口实现类
public class OrderServiceImpl implements OrderService {@Overridepublic void test1() {System.out.println("--执行test1--");}@Overridepublic void test2() {System.out.println("--执行test2--");}
}
//代理类
public class OrderProxy implements InvocationHandler {private static final String METHOD_PREFIX = "test";private Object target;public OrderProxy(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//我们使用这个标志来识别是否使用代理还是使用方法本体if (method.getName().startsWith(METHOD_PREFIX)) {System.out.println("========分隔符========");}return method.invoke(target, args);}public Object getProxy() {return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),target.getClass().getInterfaces(), this);}
}
//测试方法
public class ProxyDemo {public static void main(String[] args) {OrderService orderService = new OrderServiceImpl();OrderProxy proxy = new OrderProxy(orderService);orderService = (OrderService) proxy.getProxy();orderService.test1();orderService.test2();}
}
此时我们执行以下测试方法,注意了此时是同时调用了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()) 会抛出异常。
添加依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
添加注解:
修改原有代码的执行方式为:
此时的执行结果为:
可见,child方法由于异常已经回滚了,而parent可以正确的提交,这才是我们想要的结果!注意的是在parent调用child的时候是通过try/catch捕获了异常的!
(2)传统Spring XML配置文件只需要添加依赖个设置如下配置即可,使用方式一样:
<aop:aspectj-autoproxy expose-proxy="true"/>
3、通过ApplicationContext上下文进行解决:
@Service
public class TestServiceImpl implements TestService {@Autowiredprivate UserMapper userMapper;/*** Spring应用上下文*/@Autowiredprivate ApplicationContext context;private TestService proxy;@PostConstructpublic void init() {//从Spring上下文中获取AOP代理对象proxy = context.getBean(TestService.class);}@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);try {proxy.child();} catch (Exception e) {e.printStackTrace();}}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);throw new RuntimeException("child Exception....................");}
}
执行结果符合我们的预期:
五、总结
到此为止,我们简单的介绍了一下Spring事务管理中如果业务中有像场景C或者场景D的情况时,如果不清楚JDK动态代理造成Spring事务无法回滚的问题的话就可能是一个开发事故了,说不定是要扣工资的!
上文中简述了几种场景的事务使用和造成事务无法回滚的根本问题,当然讲述的还是表面的现象,并没有深入原理去分析,尽管如此,如果你在面试的时候能够对这个问题说一下自己的了解,也是一个加分项!
转自:https://zhuanlan.zhihu.com/p/35483036
转载于:https://www.cnblogs.com/zhangmingcheng/p/9967364.html
(转)面试必备技能:JDK动态代理给Spring事务埋下的坑!相关推荐
- 分析动态代理给Spring事务埋下的坑
前言 Spring的声明式事务让我们不在编写获得连接.关闭连接.开启事务.提交事务.回滚事务等代码,通过一个简单的@Transactional注解,就让我们轻松进行事务处理.我们知道Spring事务基 ...
- spring 动态代理_分析动态代理给 Spring 事务埋下的坑
前言 Spring的声明式事务让我们不在编写获得连接.关闭连接.开启事务.提交事务.回滚事务等代码,通过一个简单的@Transactional注解,就让我们轻松进行事务处理.我们知道Spring事务基 ...
- 接住喽????,送你个装逼的技能: JDK动态代理
今天讲一个比较深层的知识点:JDK动态代理,这是个可以让小白在大咖面前装逼的神器,顺便送你一个代理模式的温习机会. 代理模式场景 为了引出动态代理的用法,我们先看看代理设计模式,这能让你了解JDK动态 ...
- AOP jdk动态代理
一: jdk动态代理是Spring AOP默认的代理方法.要求 被代理类要实现接口,只有接口里的方法才能被代理,主要步骤是先创建接口,接口里创建要被代理的方法,然后定义一个实现类实现该接口,接着将被代 ...
- 面试造火箭系列,栽在了cglib和jdk动态代理
代理模式 关于代理模式,查阅比较专业的资料是这么定义的:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用. 主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上 ...
- JDK 动态代理与 CGLIB 动态代理,它俩真的不一样
摘要:一文带你搞懂JDK 动态代理与 CGLIB 动态代理 本文分享自华为云社区<一文带你搞懂JDK 动态代理与 CGLIB 动态代理>,作者: Code皮皮虾 . 两者有何区别 1.Jd ...
- java代理(静态代理和jdk动态代理以及cglib代理)
说到代理,脑袋中浮现一大堆代理相关的名词,代理模式,静态代理,jdk代理,cglib代理等等. 记忆特别深刻的是,一次面试中,一位面试官问我,spring的AOP核心采用的设计模式是什么什么模式,阅读 ...
- 谁与争锋,JDK动态代理大战CGLib动态代理
文章目录 一.前言 二.基本概念 三.JDK 和 CGLib动态代理区别 3.1 JDK动态代理具体实现原理 3.2 CGLib动态代理 3.3 两者对比 3.4 使用注意 四.JDK 和 CGLib ...
- Java 面试必考题:动态代理
前天下午 2 点,坐标西二旗,某一线互联网大厂,面试官问:请讲讲什么是代理?什么是动态代理?什么是 ASM?面对连珠炮的问题,我闯了两关,但没能通关,只能回去等通知了. 回去不吃饭不睡觉,不断总结 J ...
最新文章
- Pycharm + Anaconda 安装遇到的问题以及自己的理解
- 独家 | 带你入门比Python更高效的Numpy(附代码)
- [RabbitMQ]RabbitMQ深入理解(一)进阶/管理/配置
- java多线程-阻塞队列BlockingQueue
- 清华校长送给毕业生的五句话,值得一看!
- android旋转动画开源库,android 围绕中心旋转动画
- Unity 全面理解加载和内存管理
- java 控制路由器_停用角度路由器链路
- 快乐大本营中测试声音年龄的软件_海天味极鲜酱油极限挑战宝藏行 终极试炼,极限成员们勇登珠峰大本营...
- Java开发笔记(一百四十)JavaFX的选择框
- Java回调函数实现案例
- OBLOG4.5 商业SQL版 漏洞解析
- 谷歌(chrome)恐龙小游戏外挂
- MongoDB三分钟插入100万数据
- vscode 脑图插件mindmap
- 141个关于人工智能的统计数据
- 诺奖背后的一位女性:伯莎·冯·苏特娜
- JS如何判断是否为ie浏览器的方法(包括IE10、IE11在内)
- 【monkey】monkey测试入门
- OSPF 理论一(热爱自己 热爱生活)
热门文章
- mysql查询字段大小写结果相同,mysql大小写查询不敏感,mysql5.7查询不区分大小写解决方案。
- 2021-2027全球与中国跨临界二氧化碳系统市场现状及未来发展趋势报告
- leetcode Longest Substring with At Most Two Distinct Characters 滑动窗口法
- train_test_split 数据集划分,样本划分
- GOF23设计模式(结构型模式)代理模式~
- Linux实现ffmpeg H.265视频编码
- 自动机器学习(AutoML)
- MegEngine推理性能优化
- MindArmour差分隐私
- 面向汽车应用的硬件推理芯片