1. 程式人生 > >事務的傳播無效,required_new無效,動態代理給spring事務傳播留下的坑

事務的傳播無效,required_new無效,動態代理給spring事務傳播留下的坑

事務的傳播級別

七種傳播級別
propagation_requierd:如果當前沒有事務,就新建一個事務,如果已存在一個事務中,加入到這個事務中,這是最常見的選擇。(預設)
propagation_supports:支援當前事務,如果沒有當前事務,就以非事務方法執行。
propagation_mandatory:使用當前事務,如果沒有當前事務,就丟擲異常。
propagation_required_new:新建事務,如果當前存在事務,把當前事務掛起。
propagation_not_supported:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
propagation_never:以非事務方式執行操作,如果當前事務存在則丟擲異常。
propagation_nested:如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與propagation_required類似的操作

情況A

有事務的A方法呼叫沒事務的B方法,都能成功插入,
B有異常都回滾。
A使用REQUIRED,B會加入到A的事務當中。

@Transactional(propagation=Propagation.REQUIRED)
    public void A() {
        User user=new User();
        user.setUserName("張三");
        mapper.save(user);

        B();
    }

    public void B() {
        User user=new User();
        user.setUserName("李四"
); mapper.save(user); }

情況B

B單獨加上事務REQUIRES_NEW:建立一個新的事務,掛起原來的事務。
無異常時都能新增。
B有異常,都不能新增。

問題就來了,為什麼都不能新增呢?按道理應該B有異常B回滾,A正常新增才對。這裡就涉及了JDK動態代理和spring事務的一個坑點。在Spring事務中巢狀呼叫其他的事務方法,事務都不能生效。

@Transactional(propagation=Propagation.REQUIRED)
    public void A() {
        User user=new User();
        user.setUserName("張三"
); mapper.save(user); B(); } @Transactional(propagation=Propagation.REQUIRES_NEW) public void B() { User user=new User(); user.setUserName("李四"); mapper.save(user); int i=1/0; }

動態代理的特性1

首先我們看一下一個簡單的動態代理實現方式:
這裡寫圖片描述

介面

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 ObjectProxy implements InvocationHandler {
    private static final String METHOD_PREFIX = "test";
    private Object target;
    public ObjectProxy(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 Test {
    public static void main(String[] args) {
        OrderService orderService = new OrderServiceImpl();
        ObjectProxy proxy = new ObjectProxy(orderService);
        orderService = (OrderService) proxy.getProxy();
        orderService.test1();
        orderService.test2();
    }
}

獲取的代理分別代理執行test1方法和test2方法。

========分隔符========
–執行test1–
========分隔符========
–執行test2–

代理執行了兩次

修改一下實現類巢狀執行test2

public class OrderServiceImpl implements OrderService {
    @Override
    public void test1() {
         System.out.println("--執行test1--");
         test2();
    }
    @Override
    public void test2() {
         System.out.println("--執行test2--");
    }
}

測試類

public class Test {
    public static void main(String[] args) {
        OrderService orderService = new OrderServiceImpl();
        ObjectProxy proxy = new ObjectProxy(orderService);
        orderService = (OrderService) proxy.getProxy();
        orderService.test1();
//        orderService.test2();
    }
}

結果:

========分隔符========
–執行test1–
–執行test2–

代理只了一次

總結:

只有代理物件proxy直接呼叫的那個方法才是真正的走代理的,巢狀的方法實際上就是 直接把巢狀的程式碼移動到代理的方法裡面。 所以,巢狀的事務都不能生效。

解決方案:

  • 傳統mvc專案spring配置裡面增加:
<!-- 開啟暴露Aop代理到ThreadLocal支援,解決事務巢狀不生效問題。 -->
<aop:aspectj-autoproxy expose-proxy="true"/><!—註解風格支援-->  
<aop:config expose-proxy="true"><!—xml風格支援-->   

配置約束要加上以下配置, 同時加入aop類的jar。
這裡寫圖片描述
使用代理物件呼叫B方法。

    @Transactional(propagation=Propagation.REQUIRED)
    public void A() {
        User user=new User();
        user.setUserName("張三");
        mapper.save(user);
        try {
            ((UserService) AopContext.currentProxy()).B();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void B() {
        User user=new User();
        user.setUserName("李四");
        mapper.save(user);
        throw new RuntimeException("child Exception....................");
    }

注意:呼叫巢狀的方法需要用try-catch處理,不然異常繼續向上拋,導致A也異常回滾了。

  • SpringBoot配置方式

新增依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

添加註解:
這裡寫圖片描述
修改原有程式碼的執行方式為:
這裡寫圖片描述

  • 通過ApplicationContext上下文進行解決

獲取到當前類的例項物件直接呼叫。


    /**
     * Spring應用上下文
     */
    @Autowired
    private ApplicationContext context;
    private UserService proxy;
    @PostConstruct
    public void init() {
        //從Spring上下文中獲取AOP代理物件
        proxy = context.getBean(UserService.class);
    }

    @Transactional(propagation=Propagation.REQUIRED)
    public void A() {
        User user=new User();
        user.setUserName("張三");
        mapper.save(user);
        try {
            proxy.B();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void B() {
        User user=new User();
        user.setUserName("李四");
        mapper.save(user);
        throw new RuntimeException("child Exception....................");
    }

無論是 獲取代理物件呼叫內嵌方法,還是從上下文物件獲取物件呼叫。本質就是重新的呼叫一次內嵌的方法,避免了動態代理 只代理外面方法的坑。重新呼叫就重新產生事務了。