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

原创 2018年01月12日 00:00:00

一、场景分析

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

1、场景A:

?wxfrom=5&wx_lazy=1

?wxfrom=5&wx_lazy=1

这里其实是分别执行了两个事物,执行的结果是两个方法都可以插入数据!如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziabBibHTCAZkefGKib7a6zibvq9rqb7td6p3ia4CJGBwMBzicX9l1oBiaSEaoA

2、场景B:

修改上述代码如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaSiaebdVROEibA5gnS7fnjV9rfdOS4xnnbEKpuo3qEH91d4zmicYlhxV9A

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaPNHCic0kJwyITjAyMnZdaSdOicNQJyrBBk78zlnT4VmmDbcPZ4mGA5LA

Propagation.REQUIRES_NEW的含义表示:如果当前存在事务,则挂起当前事务并且开启一个新事物继续执行,新事物执行完毕之后,然后在缓刑之前挂起的事务,如果当前不存在事务的话,则开启一个新事物。

执行的结果是两个方法都可以插入数据!执行结果如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaBHxL3rK5zMZv7znv7TkqxYsmB8SgViamozV1CpliclLULVTqs6fSFicmQ

场景A和场景B都是正常的执行,期间没有发生任何的回滚,假如child()方法中出现了异常!

3、场景C

修改child()的代码如下所示,其他代码和场景B一样:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia6AFvMNEemZ8oTZPaVQ2oialaRswhf9iaZrZJciciaXiasUBiaxn9rNBbOYUw

执行结果如下,会出现异常,并且数据都没有插入进去:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaug97mlo5fh3XcCofY48zRFricYnYR3GZY9Oftj21KIL6hqz4COxgJPQ

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziamicbnZFrw7ibQG4W3ic0yp6ibibb5osYVU8ia44WAS1NQdOibmib5trtJq87ibQ

疑问1:场景C中child()抛出了异常,但是parent()没有抛出异常,按道理是不是应该parent()提交成功而child()回滚?

可能有的小伙伴要说了,child()抛出了异常在parent()没有进行捕获,造成了parent()也是抛出了异常了的!所以他们两个都会回滚!

4、场景D

按照上述小伙伴的疑问这个时候,如果对parent()方法修改,捕获child()中抛出的异常,其他代码和场景C一样:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziasucDcAnDuxuibw2fGzy0nIHKpOsh8SPB0EDib44G6WJSvNFUx29xMR0w

然后再次执行,结果是两个都插入了数据库:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziau0YZA2Flg7A3Fz509mGftibuiag3jhADCGK0koGQXibt926zIIWaNTduA

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaTiaddmDgQHOj9DSm7xcAqW7LMdjuP0OWOicYWkQCyooaEKwBcW26HbAg

看到这里很多小伙伴都可能会问,按照我们的逻辑来想的话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本质:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia11nJqic1HAebf0KSz0D0IXxYYHM8TWLEHREUSYR8egJTjX9O4GUibSiaw

场景D本质:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaRTxbo3oYt0I4vmI21OnFkgG96Xt7ib70OqHWR3EjoTzVibibE39ThibDzQ

正如上述的代码,我们可以很轻松的解释疑问1和疑问2,因为动态代理的特性造成了场景C和场景D的本质如上述代码。在场景C中,child()抛出异常没有捕获,相当于parent事务中抛出了异常,造成parent()一起回滚,因为他们本质是同一个方法;在场景D中,child()抛出异常并进行了捕获,parent事务中没有抛出异常,parent()和child()同时在一个事务里边,所以他们都成功了;

看到这里,那么动态代理的这个特性到底是什么才会造成Spring事务失效那?

三、动态代理的这个特性到底是什么?

首先我们看一下一个简单的动态代理实现方式:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaP6C1WBp5jFZwXGvA7Qhm8dsnNhhpRLPBMV5tgvElUBj5IK0v1YOu2A

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziawZ8YmOFIUq2A3pW1icUq9KfDLI81richdBJBbNnnvicWfQzpicVazLqxNQ

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia5vghZqG5OajyjLLUXqXoFzicpEK93lwseyBia8U52sA8WguBlE5BPOaw

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia2Au7o1xVxia05Pojb0QgbsaJGpWNaaib8qR2ncrs9ibiaDcrNZ9Wb0hbBQ

此时我们执行以下测试方法,注意了此时是同时调用了test1()和test2()的,执行结果如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziazjdg2GglUicT9k2ynC4dR7XicSOk77wjoyfVORNM9c9mj5ngVo5hQayg

可以看出,在OrderServiceImpl 类中由于test1()没有调用test2(),他们方法的执行都是使用了代理的,也就是说test1和test2都是通过代理对象调用的invoke()方法,这和我们场景A和B类似。

加入我们模拟一下场景C和场景D在test1()中调用test2(),那么代码修改为如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziahr2nlic2FcrA2ibVB7RDxBeicZ8HQ32TguichZRcYiaSLGOIpdqsKXzgafA

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziacicSKU6L9ibtjVm3IWOuTKP0ItO4qcGNAzCw1vibWhP5UzzPoo1hQaElg

执行结果如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaoCzVVUQcFCIGyNhvHN9EPwnmbZ4zMReszHZ4wFPWd7B3Q9avric1HdQ

这里可以很清楚的看出来test1()走的是代理,而test2()走的是普通的方法,没有经过代理!看到这里你是否已经恍然大明白了呢?

这个应该可以很好的理解为什么是这样子!这是因为在Java中test1()中调用test2()中的方法,本质上就相当于把test2()的方法体放入到test1()中,也就是内部方法,同样的不管你嵌套了多少层,只有代理对象proxy 直接调用的那一个方法才是真正的走代理的,如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia8YP7kJFXzpiapUFhIJUNJcYqkG2YQsNFL3Wh7VqXBwKkd7zKIRXrvgQ

测试方法和上边的测试方法一样,执行结果如下:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaQtlicX9cSG8JpaD2qoh7fhcK9CRc9MgrzrR9wpH4aJDicsibCgiaCnFIgA

记住:只有代理对象proxy直接调用的那个方法才是真正的走代理的!

四、如何解决这个坑?

上文的分析中我们已经了解了为什么在该特定场景下使用Spring事务的时候造成事务无法回滚的问题,下边我们谈一下几种解决的方法:

1、我们可以选择逃避这个问题!我们可以不使用以上这种事务嵌套的方式来解决问题,最简单的方法就是把问题提到Service或者是更靠前的逻辑中去解决,使用service.xxxtransaction是不会出现这种问题的。

2、通过AopProxy上下文获取代理对象:

(1)SpringBoot配置方式:注解开启 exposeProxy = true,暴露代理对象 (否则AopContext.currentProxy()) 会抛出异常。

添加依赖:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaXD9RibvJQGOdn5HicibB4I6D3x6mnQDFkgt1KfsB2axLuCfWJPd6hboIw

添加注解:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia0icywzOMvAz2dxW8hibUic3n2rq3MJFTW14PUUBx9cWOZYA6O2wbsyOyw

修改原有代码的执行方式为:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaxiaM1UAEouESbqvkAncPfqEyBib2aiavIKNRrk52la0MOGfhcVXiag61aA

此时的执行结果为:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia6uJ47chI3z4JnGqsanuDMavmgTGmEnsEKAjzLq0f8BUmS9fVRvGlWw

可见,child方法由于异常已经回滚了,而parent可以正确的提交,这才是我们想要的结果!注意的是在parent调用child的时候是通过try/catch捕获了异常的!

(2)传统Spring XML配置文件只需要添加依赖个设置如下配置即可,使用方式一样:

<aop:aspectj-autoproxy expose-proxy="true"/>

3、通过ApplicationContext上下文进行解决:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaEcXryYumsIX7MaqIUayuu7QBiaqk9J3tv1TzeLFogqqHHshoSZb5t1g

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziazFGUXlWBplOUB5hzunSicReOicwarswadfogKUvb8uOeOZX2yAY82spg


执行结果符合我们的预期:

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziaPdV2rtUZtH26BrrVU3oykiaicclyTswDzhBPMNiaOJuaKYMuknHaco6DA

五、总结

到此为止,我们简单的介绍了一下Spring事务管理中如果业务中有像场景C或者场景D的情况时,如果不清楚JDK动态代理造成Spring事务无法回滚的问题的话就可能是一个开发事故了,说不定是要扣工资的!

上文中简述了几种场景的事务使用和造成事务无法回滚的根本问题,当然讲述的还是表面的现象,并没有深入原理去分析,尽管如此,如果你在面试的时候能够对这个问题说一下自己的了解,也是一个加分项!

送给每一位用心看完的小伙伴!

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrzia7dWISfNfrX6iboxqZ6Nj3icjdFOic41e8SaY7ogF0kQST4iakTkyRe1RtQ

自己写的文章中说了很多次动态代理了,但时一直没时间写,今天特意送出两本书帮大家掌握一下,两本书中一本从留言区抽取,一本从“抽奖助手”中抽!

送书规则

1、本次活动还是在评论区随机抽取一名幸运吃瓜群众!

2、截止日期:2018年01月12日23时00分,幸运的小伙伴名单评论区留言会置顶,获奖的小伙伴请在一个工作日内在公众号界面联系我,发送手机号、姓名、收货地址,逾期则认为放弃处理;

UtWdDgynLdbhpq8PicAoLpGjsEbG2vrziafM31NFEFTksVsPFRkKonOA6Pch7G7187udhKr7tx2dcn4Koz0Ew5IA

重要的事情是:一共两本,一本从留言区选,一本从抽奖助手选!

最近热文阅读:

1、四张图带你了解Tomcat系统架构

2、三条路线告诉你如何掌握Spring IoC容器的核心原理

更多精彩,从关注《Java后端技术》公众号开始!

?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bntX2jSQfEHy7/article/details/79040349

SSH面试和笔试指导

-
  • 1970年01月01日 08:00

spring 如何决定使用jdk动态代理和cglib(网易面试题)

Spring1.2: 将事务代理工厂[TransactionProxyFactoryBean] 或 自动代理拦截器[BeanNameAutoProxyCreator] 的 proxyTar...
  • samjustin1
  • samjustin1
  • 2016-08-31 11:05:06
  • 4036

从一道面试题开始说起 枚举、动态代理的原理

本文已在我的公众号hongyangAndroid原创发布。 转载请标明出处: 本文出自:涨鸿洋的博客 前段时间在dota群,一哥们出去面试,回顾面试题的时候,说问到了枚举。 作为一名...
  • lmj623565791
  • lmj623565791
  • 2018-02-07 12:54:12
  • 5121

jdk动态代理引起的spring事务不起作用

原文地址: https://mp.weixin.qq.com/s/vCZP8sPrtnXWvg6IlcHQOg 一、场景分析 最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:...
  • lixiaoyi01
  • lixiaoyi01
  • 2018-01-15 13:34:48
  • 177

java动态代理原理及解析

java动态代理, jdk反射与代理模式
  • Scplove
  • Scplove
  • 2016-09-06 17:25:05
  • 54700

[转]Java面试近一个月的面试总结

本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/46753275前言      打算换个工作,近一个月面试了不...
  • u013129944
  • u013129944
  • 2017-01-20 14:17:15
  • 13140

Java动态代理的两种实现方法

1、定义接口和实现 package com.meituan.hyt.test3.service; /** * Created by heyutao on 15/11/9. */ public i...
  • HEYUTAO007
  • HEYUTAO007
  • 2015-11-09 15:24:04
  • 95601

Java JDK动态代理实现自己的事务管理器

spring aop介绍 spring提供了五种通知类型 Interception Around JointPoint前后调用,实现此类需要实现接口MethodInterceptor。 Befor...
  • qq_23126581
  • qq_23126581
  • 2017-11-06 11:25:17
  • 207

java面试总结,非常值得一看

第一阶段:三年 我认为三年对于程序员来说是第一个门槛,这个阶段将会淘汰掉一批不适合写代码的人。这一阶段,我们走出校园,迈入社会,成为一名程序员,正式从书本 上的内容迈向真正的企业级开发。我们知道...
  • miachen520
  • miachen520
  • 2016-06-21 19:39:04
  • 11795

【关于基础】——-1、玩好JDK,面试不用愁。

一日,LZ在群里发话,“招人啦。”   然某群友曰,“群主,俺想去。”   LZ回之,“你年几何?”   群友曰,“两年也。”   LZ憾言之,“惜了,三至五为佳。然如汝有扎实之基础,且附一技...
  • singit
  • singit
  • 2017-02-01 23:44:32
  • 2216
收藏助手
不良信息举报
您举报文章:面试必备技能:JDK动态代理给Spring事务埋下的坑!
举报原因:
原因补充:

(最多只允许输入30个字)