最近做項目遇到了一個(gè)很奇怪的問(wèn)題,大致的業(yè)務(wù)場(chǎng)景是這樣的:我們首先設定兩個(gè)事務(wù),事務(wù)parent和事務(wù)child,在Controller里邊同時(shí)調用這兩個(gè)方法,示例代碼如下:
1、場(chǎng)景A:
@RestController@RequestMapping(value = "/test")public class OrderController { @Autowired private TestService userService; @GetMapping public void test() { //同時(shí)調用parent和child userService.parent(); userService.child(); }}@Servicepublic 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í)是分別執行了兩個(gè)事物,執行的結果是兩個(gè)方法都可以插入數據!如下:
2、場(chǎng)景B:
修改上述代碼如下:
@RestController@RequestMapping(value = "/test")public class OrderController { @Autowired private TestService userService; @GetMapping public void test() { userService.parent(); }}@Servicepublic class TestServiceImpl implements TestService { @Autowired private UserMapper userMapper; @Override @Transactional public void parent() { User parent = new User("張大壯 Parent", "123456", 45); userMapper.insert(parent); //在parent里邊調用child child(); } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void child() { User child = new User("張大壯 Child", "654321", 25); userMapper.insert(child); }}Propagation.REQUIRES_NEW的含義表示:如果當前存在事務(wù),則掛起當前事務(wù)并且開(kāi)啟一個(gè)新事物繼續執行,新事物執行完畢之后,然后在緩刑之前掛起的事務(wù),如果當前不存在事務(wù)的話(huà),則開(kāi)啟一個(gè)新事物。
執行的結果是兩個(gè)方法都可以插入數據!執行結果如下:
場(chǎng)景A和場(chǎng)景B都是正常的執行,期間沒(méi)有發(fā)生任何的回滾,假如child()方法中出現了異常!
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...................."); }執行結果如下,會(huì )出現異常,并且數據都沒(méi)有插入進(jìn)去:
疑問(wèn)1:場(chǎng)景C中child()拋出了異常,但是parent()沒(méi)有拋出異常,按道理是不是應該parent()提交成功而child()回滾?
可能有的小伙伴要說(shuō)了,child()拋出了異常在parent()沒(méi)有進(jìn)行捕獲,造成了parent()也是拋出了異常了的!所以他們兩個(gè)都會(huì )回滾!
4、場(chǎng)景D
按照上述小伙伴的疑問(wèn)這個(gè)時(shí)候,如果對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...................."); }然后再次執行,結果是兩個(gè)都插入了數據庫:

看到這里很多小伙伴都可能會(huì )問(wèn),按照我們的邏輯來(lái)想的話(huà)child()中拋出了異常,parent()沒(méi)有拋出并且捕獲了child()拋出了異常!執行的結果應該是child()回滾,parent()提交成功的??!
疑問(wèn)2:場(chǎng)景D為什么不是child()回滾和parent()提交成功哪?
上述的場(chǎng)景C和場(chǎng)景D似乎融為了一題,要么都成功要么都失??!和我們預期的效果一點(diǎn)都不一樣!看到這里這就是我們今天要探討的主題《JDK動(dòng)態(tài)代理給Spring事務(wù)埋下的坑!》接下來(lái)我們就分析一下Spring事物在該特定場(chǎng)景下不能回滾的深層次原因!
我們知道Spring事務(wù)管理是通過(guò)JDK動(dòng)態(tài)代理的方式進(jìn)行實(shí)現的(另一種是使用CGLib動(dòng)態(tài)代理實(shí)現的),也正是因為動(dòng)態(tài)代理的特性造成了上述parent()方法調用child()方法的時(shí)候造成了child()方法中的事務(wù)失效!簡(jiǎn)單的來(lái)說(shuō),在場(chǎng)景D中parent()方法調用child()方法的時(shí)候,child()方法的事務(wù)是不起作用的,此時(shí)的child()方法像一個(gè)沒(méi)有加事務(wù)的普通方法,其本質(zhì)上就相當于下邊的代碼:
場(chǎng)景C本質(zhì):

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

正如上述的代碼,我們可以很輕松的解釋疑問(wèn)1和疑問(wèn)2,因為動(dòng)態(tài)代理的特性造成了場(chǎng)景C和場(chǎng)景D的本質(zhì)如上述代碼。在場(chǎng)景C中,child()拋出異常沒(méi)有捕獲,相當于parent事務(wù)中拋出了異常,造成parent()一起回滾,因為他們本質(zhì)是同一個(gè)方法;在場(chǎng)景D中,child()拋出異常并進(jìn)行了捕獲,parent事務(wù)中沒(méi)有拋出異常,parent()和child()同時(shí)在一個(gè)事務(wù)里邊,所以他們都成功了;
看到這里,那么動(dòng)態(tài)代理的這個(gè)特性到底是什么才會(huì )造成Spring事務(wù)失效那?
首先我們看一下一個(gè)簡(jiǎn)單的動(dòng)態(tài)代理實(shí)現方式:

//接口public interface OrderService { void test1(); void test2();}//接口實(shí)現類(lèi)public class OrderServiceImpl implements OrderService { @Override public void test1() { System.out.println("--執行test1--"); } @Override public void test2() { System.out.println("--執行test2--"); }}//代理類(lèi)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è)標志來(lái)識別是否使用代理還是使用方法本體 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(); }}此時(shí)我們執行以下測試方法,注意了此時(shí)是同時(shí)調用了test1()和test2()的,執行結果如下:

可以看出,在OrderServiceImpl 類(lèi)中由于test1()沒(méi)有調用test2(),他們方法的執行都是使用了代理的,也就是說(shuō)test1和test2都是通過(guò)代理對象調用的invoke()方法,這和我們場(chǎng)景A和B類(lèi)似。
加入我們模擬一下場(chǎng)景C和場(chǎng)景D在test1()中調用test2(),那么代碼修改為如下:


執行結果如下:

這里可以很清楚的看出來(lái)test1()走的是代理,而test2()走的是普通的方法,沒(méi)有經(jīng)過(guò)代理!看到這里你是否已經(jīng)恍然大明白了呢?
這個(gè)應該可以很好的理解為什么是這樣子!這是因為在Java中test1()中調用test2()中的方法,本質(zhì)上就相當于把test2()的方法體放入到test1()中,也就是內部方法,同樣的不管你嵌套了多少層,只有代理對象proxy 直接調用的那一個(gè)方法才是真正的走代理的,如下:

測試方法和上邊的測試方法一樣,執行結果如下:

記?。褐挥写韺ο髉roxy直接調用的那個(gè)方法才是真正的走代理的!
上文的分析中我們已經(jīng)了解了為什么在該特定場(chǎng)景下使用Spring事務(wù)的時(shí)候造成事務(wù)無(wú)法回滾的問(wèn)題,下邊我們談一下幾種解決的方法:
1、我們可以選擇逃避這個(gè)問(wèn)題!我們可以不使用以上這種事務(wù)嵌套的方式來(lái)解決問(wèn)題,最簡(jiǎn)單的方法就是把問(wèn)題提到Service或者是更靠前的邏輯中去解決,使用service.xxxtransaction是不會(huì )出現這種問(wèn)題的。
2、通過(guò)AopProxy上下文獲取代理對象:
(1)SpringBoot配置方式:注解開(kāi)啟 exposeProxy = true,暴露代理對象 (否則AopContext.currentProxy()) 會(huì )拋出異常。
添加依賴(lài):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>添加注解:

修改原有代碼的執行方式為:

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

可見(jiàn),child方法由于異常已經(jīng)回滾了,而parent可以正確的提交,這才是我們想要的結果!注意的是在parent調用child的時(shí)候是通過(guò)try/catch捕獲了異常的!
(2)傳統Spring XML配置文件只需要添加依賴(lài)個(gè)設置如下配置即可,使用方式一樣:
<aop:aspectj-autoproxy expose-proxy="true"/>3、通過(guò)ApplicationContext上下文進(jìn)行解決:
@Servicepublic class TestServiceImpl implements TestService { @Autowired private UserMapper userMapper; /** * Spring應用上下文 */ @Autowired private ApplicationContext context; private TestService proxy; @PostConstruct public void init() { //從Spring上下文中獲取AOP代理對象 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...................."); }}執行結果符合我們的預期:

到此為止,我們簡(jiǎn)單的介紹了一下Spring事務(wù)管理中如果業(yè)務(wù)中有像場(chǎng)景C或者場(chǎng)景D的情況時(shí),如果不清楚JDK動(dòng)態(tài)代理造成Spring事務(wù)無(wú)法回滾的問(wèn)題的話(huà)就可能是一個(gè)開(kāi)發(fā)事故了,說(shuō)不定是要扣工資的!
上文中簡(jiǎn)述了幾種場(chǎng)景的事務(wù)使用和造成事務(wù)無(wú)法回滾的根本問(wèn)題,當然講述的還是表面的現象,并沒(méi)有深入原理去分析,盡管如此,如果你在面試的時(shí)候能夠對這個(gè)問(wèn)題說(shuō)一下自己的了解,也是一個(gè)加分項!
聯(lián)系客服