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)!