面試必備技能:JDK動(dòng)態(tài)代理給Spring事務(wù)埋下的坑

https://blog.csdn.net/xlgen157387/article/details/79026285


一、場(chǎng)景分析

最近做項(xiàng)目遇到了一個(gè)很奇怪的問題皿桑,大致的業(yè)務(wù)場(chǎng)景是這樣的:我們首先設(shè)定兩個(gè)事務(wù)下面,事務(wù)parent和事務(wù)child枚碗,在Controller里邊同時(shí)調(diào)用這兩個(gè)方法舟扎,示例代碼如下:

1粮呢、場(chǎng)景A:

@RestController

@RequestMapping(value = "/test")

public class OrderController {

? ? @Autowired

? ? private TestService userService;

? ? @GetMapping

? ? public void test() {

? ? ? ? //同時(shí)調(diào)用parent和child

? ? ? ? userService.parent();

? ? ? ? userService.child();

? ? }

}

@Service

public class TestServiceImpl implements TestService {

? ? @Autowired

? ? private UserMapper userMapper;

? ? @Override

? ? @Transactional

? ? public void parent() {

? ? ? ? User parent = new User("張大壯 Parent", "123456", 45);

? ? ? ? userMapper.insert(parent);

? ? }

? ? @Override

? ? @Transactional

? ? public void child() {

? ? ? ? User child = new User("張大壯 Child", "654321", 25);

? ? ? ? userMapper.insert(child);

? ? }

}

這里其實(shí)是分別執(zhí)行了兩個(gè)事務(wù)伍玖,執(zhí)行的結(jié)果是兩個(gè)方法都可以插入數(shù)據(jù)嫩痰!如下:

2、場(chǎng)景B:

修改上述代碼如下:

@RestController

@RequestMapping(value = "/test")

public class OrderController {

? ? @Autowired

? ? private TestService userService;

? ? @GetMapping

? ? public void test() {

? ? ? ? userService.parent();

? ? }

}

@Service

public class TestServiceImpl implements TestService {

? ? @Autowired

? ? private UserMapper userMapper;

? ? @Override

? ? @Transactional

? ? public void parent() {

? ? ? ? User parent = new User("張大壯 Parent", "123456", 45);

? ? ? ? userMapper.insert(parent);

? ? ? ? //在parent里邊調(diào)用child

? ? ? ? child();

? ? }

? ? @Override

? ? @Transactional(propagation = Propagation.REQUIRES_NEW)

? ? public void child() {

? ? ? ? User child = new User("張大壯 Child", "654321", 25);

? ? ? ? userMapper.insert(child);

? ? }

}

Propagation.REQUIRES_NEW的含義表示:如果當(dāng)前存在事務(wù)窍箍,則掛起當(dāng)前事務(wù)并且開啟一個(gè)新事務(wù)繼續(xù)執(zhí)行串纺,新事務(wù)執(zhí)行完畢之后,然后在緩刑之前掛起的事務(wù)椰棘,如果當(dāng)前不存在事務(wù)的話纺棺,則開啟一個(gè)新事務(wù)。

執(zhí)行的結(jié)果是兩個(gè)方法都可以插入數(shù)據(jù)邪狞!執(zhí)行結(jié)果如下:

場(chǎng)景A和場(chǎng)景B都是正常的執(zhí)行祷蝌,期間沒有發(fā)生任何的回滾,假如child()方法中出現(xiàn)了異常帆卓!

3巨朦、場(chǎng)景C

修改child()的代碼如下所示,其他代碼和場(chǎng)景B一樣:

? ? @Override

? ? @Transactional

? ? public 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....................");

? ? }

執(zhí)行結(jié)果如下剑令,會(huì)出現(xiàn)異常糊啡,并且數(shù)據(jù)都沒有插入進(jìn)去:

疑問1:場(chǎng)景C中child()拋出了異常,但是parent()沒有拋出異常尚洽,按道理是不是應(yīng)該parent()提交成功而child()回滾悔橄?

可能有的小伙伴要說了,child()拋出了異常在parent()沒有進(jìn)行捕獲,造成了parent()也是拋出了異常了的癣疟!所以他們兩個(gè)都會(huì)回滾挣柬!

4、場(chǎng)景D

按照上述小伙伴的疑問這個(gè)時(shí)候睛挚,如果對(duì)parent()方法修改邪蛔,捕獲child()中拋出的異常,其他代碼和場(chǎng)景C一樣:

? ? @Override

? ? @Transactional

? ? public 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....................");

? ? }

然后再次執(zhí)行扎狱,結(jié)果是兩個(gè)都插入了數(shù)據(jù)庫:

看到這里很多小伙伴都可能會(huì)問侧到,按照我們的邏輯來想的話child()中拋出了異常,parent()沒有拋出并且捕獲了child()拋出了異常淤击!執(zhí)行的結(jié)果應(yīng)該是child()回滾匠抗,parent()提交成功的啊污抬!

疑問2:場(chǎng)景D為什么不是child()回滾和parent()提交成功哪汞贸?

上述的場(chǎng)景C和場(chǎng)景D似乎融為了一題,要么都成功要么都失斢』矢腻!和我們預(yù)期的效果一點(diǎn)都不一樣!看到這里這就是我們今天要探討的主題《JDK動(dòng)態(tài)代理給Spring事務(wù)埋下的坑射赛!》接下來我們就分析一下Spring事務(wù)在該特定場(chǎng)景下不能回滾的深層次原因多柑!

二、問題本質(zhì)所在

我們知道Spring事務(wù)管理是通過JDK動(dòng)態(tài)代理的方式進(jìn)行實(shí)現(xiàn)的(另一種是使用CGLib動(dòng)態(tài)代理實(shí)現(xiàn)的)楣责,也正是因?yàn)閯?dòng)態(tài)代理的特性造成了上述parent()方法調(diào)用child()方法的時(shí)候造成了child()方法中的事務(wù)失效竣灌!簡單的來說,在場(chǎng)景D中parent()方法調(diào)用child()方法的時(shí)候秆麸,child()方法的事務(wù)是不起作用的帐偎,此時(shí)的child()方法像一個(gè)沒有加事務(wù)的普通方法,其本質(zhì)上就相當(dāng)于下邊的代碼:

場(chǎng)景C本質(zhì):

場(chǎng)景D本質(zhì):

正如上述的代碼蛔屹,我們可以很輕松的解釋疑問1和疑問2,因?yàn)閯?dòng)態(tài)代理的特性造成了場(chǎng)景C和場(chǎng)景D的本質(zhì)如上述代碼豁生。在場(chǎng)景C中兔毒,child()拋出異常沒有捕獲,相當(dāng)于parent事務(wù)中拋出了異常甸箱,造成parent()一起回滾育叁,因?yàn)樗麄儽举|(zhì)是同一個(gè)方法;在場(chǎng)景D中芍殖,child()拋出異常并進(jìn)行了捕獲豪嗽,parent事務(wù)中沒有拋出異常,parent()和child()同時(shí)在一個(gè)事務(wù)里邊,所以他們都成功了龟梦;

看到這里隐锭,那么動(dòng)態(tài)代理的這個(gè)特性到底是什么才會(huì)造成Spring事務(wù)失效那?

三计贰、動(dòng)態(tài)代理的這個(gè)特性到底是什么钦睡?

首先我們看一下一個(gè)簡單的動(dòng)態(tài)代理實(shí)現(xiàn)方式:

//接口

public interface OrderService {

? ? void test1();

? ? void test2();

}

//接口實(shí)現(xiàn)類

public class OrderServiceImpl implements OrderService {

? ? @Override

? ? public void test1() {

? ? ? ? System.out.println("--執(zhí)行test1--");

? ? }

? ? @Override

? ? public void test2() {

? ? ? ? System.out.println("--執(zhí)行test2--");

? ? }

}

//代理類

public class OrderProxy implements InvocationHandler {

? ? private static final String METHOD_PREFIX = "test";

? ? private Object target;

? ? public OrderProxy(Object target) {

? ? ? ? this.target = target;

? ? }

? ? @Override

? ? public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

? ? ? ? //我們使用這個(gè)標(biāo)志來識(shí)別是否使用代理還是使用方法本體

? ? ? ? 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);

? ? }

}

//測(cè)試方法

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();

? ? }

}

此時(shí)我們執(zhí)行以下測(cè)試方法,注意了此時(shí)是同時(shí)調(diào)用了test1()和test2()的躁倒,執(zhí)行結(jié)果如下:

可以看出荞怒,在OrderServiceImpl 類中由于test1()沒有調(diào)用test2(),他們方法的執(zhí)行都是使用了代理的秧秉,也就是說test1和test2都是通過代理對(duì)象調(diào)用的invoke()方法褐桌,這和我們場(chǎng)景A和B類似。

假如我們模擬一下場(chǎng)景C和場(chǎng)景D在test1()中調(diào)用test2()象迎,那么代碼修改為如下:

執(zhí)行結(jié)果如下:

這里可以很清楚的看出來test1()走的是代理荧嵌,而test2()走的是普通的方法,沒有經(jīng)過代理挖帘!看到這里你是否已經(jīng)恍然大明白了呢完丽?

這個(gè)應(yīng)該可以很好的理解為什么是這樣子!這是因?yàn)樵贘ava中test1()中調(diào)用test2()中的方法拇舀,本質(zhì)上就相當(dāng)于把test2()的方法體放入到test1()中逻族,也就是內(nèi)部方法,同樣的不管你嵌套了多少層骄崩,只有代理對(duì)象proxy 直接調(diào)用的那一個(gè)方法才是真正的走代理的聘鳞,如下:

測(cè)試方法和上邊的測(cè)試方法一樣,執(zhí)行結(jié)果如下:

記滓鳌:只有代理對(duì)象proxy直接調(diào)用的那個(gè)方法才是真正的走代理的抠璃!

四、如何解決這個(gè)坑脱惰?

上文的分析中我們已經(jīng)了解了為什么在該特定場(chǎng)景下使用Spring事務(wù)的時(shí)候造成事務(wù)無法回滾的問題搏嗡,下邊我們談一下幾種解決的方法:

1、我們可以選擇逃避這個(gè)問題拉一!我們可以不使用以上這種事務(wù)嵌套的方式來解決問題采盒,最簡單的方法就是把問題提到Service或者是更靠前的邏輯中去解決,使用service.xxxtransaction是不會(huì)出現(xiàn)這種問題的蔚润。

2磅氨、通過AopProxy上下文獲取代理對(duì)象:

(1)SpringBoot配置方式:注解開啟 exposeProxy = true,暴露代理對(duì)象 (否則AopContext.currentProxy()) 會(huì)拋出異常嫡纠。

添加依賴:

<dependency>

? ? <groupId>org.springframework.boot</groupId>

? ? <artifactId>spring-boot-starter-aop</artifactId>

</dependency>

添加注解:

修改原有代碼的執(zhí)行方式為:

此時(shí)的執(zhí)行結(jié)果為:

可見烦租,child方法由于異常已經(jīng)回滾了延赌,而parent可以正確的提交,這才是我們想要的結(jié)果叉橱!注意的是在parent調(diào)用child的時(shí)候是通過try/catch捕獲了異常的挫以!

(2)傳統(tǒng)Spring XML配置文件只需要添加依賴個(gè)設(shè)置如下配置即可,使用方式一樣:

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

3赏迟、通過ApplicationContext上下文進(jìn)行解決:

@Service

public class TestServiceImpl implements TestService {

? ? @Autowired

? ? private UserMapper userMapper;

? ? /**

? ? * Spring應(yīng)用上下文

? ? */

? ? @Autowired

? ? private ApplicationContext context;

? ? private TestService proxy;

? ? @PostConstruct

? ? public void init() {

? ? ? ? //從Spring上下文中獲取AOP代理對(duì)象

? ? ? ? proxy = context.getBean(TestService.class);

? ? }

? ? @Override

? ? @Transactional

? ? public 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....................");

? ? }

}

執(zhí)行結(jié)果符合我們的預(yù)期:

五屡贺、總結(jié)

到此為止,我們簡單的介紹了一下Spring事務(wù)管理中如果業(yè)務(wù)中有像場(chǎng)景C或者場(chǎng)景D的情況時(shí)锌杀,如果不清楚JDK動(dòng)態(tài)代理造成Spring事務(wù)無法回滾的問題的話就可能是一個(gè)開發(fā)事故了甩栈,說不定是要扣工資的!

上文中簡述了幾種場(chǎng)景的事務(wù)使用和造成事務(wù)無法回滾的根本問題糕再,當(dāng)然講述的還是表面的現(xiàn)象量没,并沒有深入原理去分析,盡管如此突想,如果你在面試的時(shí)候能夠?qū)@個(gè)問題說一下自己的了解殴蹄,也是一個(gè)加分項(xiàng)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猾担,一起剝皮案震驚了整個(gè)濱河市袭灯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绑嘹,老刑警劉巖稽荧,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異工腋,居然都是意外死亡姨丈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門擅腰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟋恬,“玉大人,你說我怎么就攤上這事趁冈〖哒” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵渗勘,是天一觀的道長矾飞。 經(jīng)常有香客問我,道長呀邢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任豹绪,我火速辦了婚禮价淌,結(jié)果婚禮上申眼,老公的妹妹穿的比我還像新娘。我一直安慰自己蝉衣,他們只是感情好括尸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著病毡,像睡著了一般濒翻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啦膜,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天有送,我揣著相機(jī)與錄音,去河邊找鬼僧家。 笑死雀摘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的八拱。 我是一名探鬼主播阵赠,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼肌稻!你這毒婦竟也來了清蚀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤爹谭,失蹤者是張志新(化名)和其女友劉穎枷邪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旦棉,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡齿风,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绑洛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片救斑。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖真屯,靈堂內(nèi)的尸體忽然破棺而出脸候,到底是詐尸還是另有隱情,我是刑警寧澤绑蔫,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布运沦,位于F島的核電站,受9級(jí)特大地震影響配深,放射性物質(zhì)發(fā)生泄漏携添。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一篓叶、第九天 我趴在偏房一處隱蔽的房頂上張望烈掠。 院中可真熱鬧羞秤,春花似錦、人聲如沸左敌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矫限。三九已至哺哼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叼风,已是汗流浹背取董。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咬扇,地道東北人甲葬。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像懈贺,于是被迫代替她去往敵國和親经窖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理梭灿,服務(wù)發(fā)現(xiàn)画侣,斷路器,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法堡妒,類相關(guān)的語法配乱,內(nèi)部類的語法,繼承相關(guān)的語法皮迟,異常的語法搬泥,線程的語...
    子非魚_t_閱讀 31,639評(píng)論 18 399
  • 1 Mybatis入門 1.1 單獨(dú)使用jdbc編程問題總結(jié) 1.1.1 jdbc程序 上邊使...
    哇哈哈E閱讀 3,307評(píng)論 0 38
  • 早上在沈家門半升洞碼頭搭乘高速船,10分鐘后即抵達(dá)普陀島伏尼。 上岸找到酒店來接的巴士忿檩,由于到太早,還不到入住時(shí)間爆阶,司...
    suzanneWH閱讀 483評(píng)論 0 2