Spring事務管理失效的原因

個人認爲, spring的聲明式事務是spring讓人感覺用的最爽的功能之一. 可是在有些時候, 我們使用spring的聲明式事務時卻並沒有效果. 是spring的問題嗎? 下面我們先大致說明一下spring聲明式事務的原理, 然後再分析在什麼情況下, spring的聲明式事務會失效.

代理模式

我們知道, spring的聲明式事務是基於代理模式的. 那麼說事務之前我們還是大致的介紹一下代理模式吧. 其實代理模式相當簡單, 就是將另一個類包裹在我們的類外面, 在調用我們創建的方法之前, 先經過外面的方法, 進行一些處理, 返回之前, 再進行一些操作. 比如:

public class UserService{
    ...
    public User getUserByName(String name) {
       return userDao.getUserByName(name);
    }
    ...
}

那麼如果配置了事務, 就相當於又創建了一個類:

public class UserServiceProxy extends UserService{
    private UserService userService;
    ...
    public User getUserByName(String name){
        User user = null;
        try{
            // 在這裏開啓事務
            user = userService.getUserByName(name);
            // 在這裏提交事務
        }
        catch(Exception e){
            // 在這裏回滾事務

            // 這塊應該需要向外拋異常, 否則我們就無法獲取異常信息了. 
            // 至於方法聲明沒有添加異常聲明, 是因爲覆寫方法, 異常必須和父類聲明的異常"兼容". 
            // 這塊應該是利用的java虛擬機並不區分普通異常和運行時異常的特點.
            throw e;
        }
        return user;
    }
    ...
}

然後我們使用的是 UserServiceProxy 類, 所以就可以"免費"得到事務的支持:

@Autowired
private UserService userService;    // 這裏spring注入的實際上是UserServiceProxy的對象

private void test(){
    // 由於userService是UserServiceProxy的對象, 所以擁有了事務管理的能力
    userService.getUserByName("aa");
}

哪些情況下spring的事務管理會失效

private 方法, final 方法 和 static 方法不能添加事務

上面的東西並不難. 那麼我們可以從上面知道些什麼呢? 首先, 由於java繼承時, 不能重寫 private , final , static 修飾的方法. 所以, 所有的 private 方法, final 方法 和 static 方法 都無法 直接 添加spring的事務管理功能. 比如下面的代碼(完整代碼點擊這裏下載):

/**
 * 保存兩個user對象. 添加了spring事務註解
 */
@Transactional
public final void saveErrorFinal(User user1, User user2) {
    UserDao userDao = getUserDao(); // 此處需要使用getUserDao方法. 不能直接使用userDao
    userDao.save(user1);
    System.out.println(10 / 0); // 引發異常
    userDao.save(user2);
}

/**
 * 靜態方法. 添加了spring事務註解
 */
@Transactional
public static void saveErrorStatic(UserDao userDao, User user1, User user2) {
    userDao.save(user1);
    System.out.println(10 / 0); // 引發異常
    userDao.save(user2);
}

/**
 * 私有方法保存方法. 添加了spring事務註解
 */
@Transactional
private void saveErrorPrivate(User user1, User user2) {
    userDao.save(user1);
    System.out.println(10 / 0); // 引發異常
    userDao.save(user2);
}

測試代碼如下:

/**
 * 檢驗user1是否插入成功, 並嘗試刪除數據. 如果沒有插入, 則報錯, 並退出.
 */
private void validateInsertSuccess() {
    User user = userService.getUserByName(user1.getName());
    userService.deleteByName(user1.getName()); // 刪除用戶數據
    assertNotEquals("插入失敗!", -1, user.getId().intValue());
}

/**
 * 檢驗user1是否插入失敗. 如果插入成功, 則刪除數據, 並報錯.
 */
private void validateInsertFail() {
    User user = userService.getUserByName(user1.getName());
    userService.deleteByName(user1.getName()); // 刪除用戶數據
    assertEquals("插入成功, 事務沒有生效!" + user, -1, user.getId().intValue());
}

@Test
public void testSaveErrorFinal() {
    try {
        userService.saveErrorFinal(user1, user2);
    }
    catch (ArithmeticException e) {}    // 除零異常, 直接忽略
    validateInsertFail(); // 我們假設有事務支持, user1添加失敗
}

@Test
public void testSaveErrorStatic() {
    try {
        UserService.saveErrorStatic(userDao, user1, user2);
    }
    catch (ArithmeticException e) {}
    validateInsertFail(); // 我們假設有事務支持, user1添加失敗
}

由於 saveErrorPrivate 方法外面是無法調用的, 就暫時不去討論了. 我們直接看 testSaveErrorFinal 和 testSaveErrorStatic 方法的運行結果:

很明顯, 事務並沒有生效. 也就是說 private 方法, final 方法 和 static 方法都沒有事務支持.

沒有通過代理對象調用添加事務的方法

仔細看看代理模式中的代碼, 就會發現不通過代理對象調用方法也會導致spring事務管理失效. 繞過代理對象最直接的方法就是自己 new 一個對象, 雖然這種可能性非常小:

new UserService().save(user);

當然, 前面也說了, 這種可能性非常小. 那麼我們看看第二種情況, 這種情況的可能性也不大:

/**
 * 保存兩個user對象, 中間產生異常. 驗證spring的事務是否可以正常工作
 */
@Transactional
public void saveError(User user1, User user2) {
    userDao.save(user1);
    System.out.println(10 / 0); // 引發異常
    userDao.save(user2);
}

/**
 * 通過{@link #saveError(User, User)}保存數據, 該方法本身並沒有添加事務註解. 
 * 而是通過{@link #saveError(User, User)}方法使用事務
 */
public void saveByCallMethod(User user1, User user2) {
    //saveErrorPrivate(user1, user2);  // 或者調用saveErrorPrivate方法
    saveError(user1, user2);
}

由於測試的代碼基本上和上面一樣, 所以這裏我們就不貼測試的代碼了. 再說一次, 點擊這裏下載完整代碼). 實際上, 上面的 saveByCallMethod 方法還是無法獲得spring的事務支持. 因爲它的調用堆棧如下圖所示(從下向上):最終結果就是spring的事務管理沒有生效. 這是或許你會想了, 那爲啥不直接給 saveByCallMethod 方法添加事務支持呢? 對啊, 所以我說這種情況的可能性也不大. 下面我們再看看事務管理和多線程纏在一起時的情況:

/**
 * 通過創建一個新的線程, 調用{@link #saveError(User, User)}方法來保存用戶, 該方法和
 * {@link #saveError(User, User)}方法都添加了事務註解
 */
@Transactional
public void saveByThread(User user1, User user2) {
    new Thread(() -> {
        try {
            Thread.sleep(1000); //耗時操作
            saveError(user1, user2);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("保存完成");
    }).start();
}

測試代碼請參見我提供的完整代碼. 這樣的代碼已經有可能了吧? 那麼事務管理會生效嗎? 我們再看看調用堆棧就知道了.結果和我們想的一樣, spring的事務管理並沒有生效.

總結

好了, 現在我們來回顧一下, 在那些情況下spring的事務管理會失效:

  • private 方法無法添加事務管理.
  • final 方法無法添加事務管理.
  • static 方法無法添加事務管理.
  • 當繞過代理對象, 直接調用添加事務管理的方法時, 事務管理將無法生效.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章