1. 程式人生 > >(轉)面試必備技能:JDK動態代理給Spring事務埋下的坑!

(轉)面試必備技能:JDK動態代理給Spring事務埋下的坑!

一、場景分析

最近做專案遇到了一個很奇怪的問題,大致的業務場景是這樣的:我們首先設定兩個事務,事務parent和事務child,在Controller裡邊同時呼叫這兩個方法,示例程式碼如下:

1、場景A:

@RestController
@RequestMapping(value = "/test")
public class OrderController {

    @Autowired
    private TestService userService;

    @GetMapping
    public void test() {
        //同時呼叫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);
    }
}

這裡其實是分別執行了兩個事務,執行的結果是兩個方法都可以插入資料!如下:

2、場景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裡邊呼叫child
        child();
    }

    @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
    @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....................");
    }

執行結果如下,會出現異常,並且資料都沒有插入進去:

疑問1:場景C中child()丟擲了異常,但是parent()沒有丟擲異常,按道理是不是應該parent()提交成功而child()回滾?

可能有的小夥伴要說了,child()丟擲了異常在parent()沒有進行捕獲,造成了parent()也是丟擲了異常了的!所以他們兩個都會回滾!

4、場景D

按照上述小夥伴的疑問這個時候,如果對parent()方法修改,捕獲child()中丟擲的異常,其他程式碼和場景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....................");
    }

然後再次執行,結果是兩個都插入了資料庫:

看到這裡很多小夥伴都可能會問,按照我們的邏輯來想的話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 {

    @Override
    public void test1() {
        System.out.println("--執行test1--");
    }

    @Override
    public 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;
    }

    @Override
    public 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 {

    @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....................");
    }
}

執行結果符合我們的預期:

五、總結

到此為止,我們簡單的介紹了一下Spring事務管理中如果業務中有像場景C或者場景D的情況時,如果不清楚JDK動態代理造成Spring事務無法回滾的問題的話就可能是一個開發事故了,說不定是要扣工資的!

上文中簡述了幾種場景的事務使用和造成事務無法回滾的根本問題,當然講述的還是表面的現象,並沒有深入原理去分析,儘管如此,如果你在面試的時候能夠對這個問題說一下自己的瞭解,也是一個加分項!

轉自:https://zhuanlan.zhihu.com/p/35483036