SpringBoot顯式事務

參考:https://www.jianshu.com/p/f5fc14bde8a0java

後續測試代碼的完整項目:https://files.cnblogs.com/files/hellohello/demo2.rarspring

後續說的事務註解都是指 import javax.transaction.Transactional;事務註解若是修飾在類上,則等價與做用在這個類的全部方法上,若是僅修飾在函數上,則僅僅做用在這個函數上,對其餘函數沒有效果。數據庫

  1. 只要加了事務註解,不論是加到bean上,仍是加到bean中的函數上,spring纔會生成一個代理對象與對應的bean,共兩個對象。
  2. 注入到容器中的是代理對象,而不是被代理的bean對象。
  3. 代理對象其實是被代理對象的子類,經過CGLib動態生成的。
  4. 其餘bean中注入的雖然是代理對象,可是對於開發人員來講,就跟直接調用實際的bean對象同樣,是透明的,由於代理對象內部會調用被代理對象的對應函數。
  5. 代理對象調用被代理對象函數時,只有拋出了uncheck exception(RuntimeException或Error)時【或配置其餘異常】,而且拋出異常的函數處於事務註解的做用範圍內時,事務纔會回滾。

測試一、二、3點:網絡

服務類代碼,成員函數上存在事務註解,因此會生成代理對象函數

@Service
public class ConfSystemService {
    // 用於記錄被代理對象的引用
    public static ConfSystemService ins ;

    public ConfSystemService(){
        ins = this;
    }
    
    @Transactional
    public void saveWithOk(int id){
        // ...
    }
}

 

 

測試類中進行測試單元測試

@Autowired
ConfSystemService confSystemService;

@Test
public void test3(){
    Assert.isTrue(!confSystemService.equals(ConfSystemService.ins),"判斷是否屬於同一個對象");
    Assert.isTrue(confSystemService.getClass().getSuperclass().equals(ConfSystemService.class),"判斷是不是父類子類關係");
    Assert.isTrue(!ConfSystemService.ins.getClass().getSuperclass().equals(ConfSystemService.class),"判斷是不是父類子類關係");
}

 

若是把服務類中的事務註解去掉,則不會生成代理對象,那上面例子中的靜態屬性保存的對象,跟其餘地方註冊的服務對象,就是同一個對象了。測試

測試四、5點:this

在服務類中添加以下代碼:spa

/**
 * dao.
 */
@Autowired
ConfSystemRepository confSystemRepository;

/**
 * 使用dao保存數據後,報錯.
 * @param id .
 */
@Transactional
public void saveWithErr(int id){
    ConfSystem confSystem = new ConfSystem();
    confSystem.setConfigurename("123");
    confSystem.setConfigurevalue("456");
    confSystem.setConfsystemid(id);

    confSystemRepository.save(confSystem);
    throw new RuntimeException("模擬錯誤");
}

/**
 * 正常保存數據.
 * @param id .
 */
@Transactional
public void saveWithOk(int id){
    ConfSystem confSystem = new ConfSystem();
    confSystem.setConfigurename("123");
    confSystem.setConfigurevalue("456");
    confSystem.setConfsystemid(id);

    confSystemRepository.save(confSystem);
}

/**
 * 判斷數據是否成功插入到了數據庫中.
 * @param id .
 * @return .
 */
public boolean checkExist(int id){
    return confSystemRepository.findById(id).isPresent();
}

 

執行單元測試代理

@Test
public void test1() {
    try{
        confSystemService.saveWithErr(888);
    }catch (Exception e){
        e.printStackTrace();
    }
    Assert.isTrue(!confSystemService.checkExist(888));
}

@Test
public void test2() {
    confSystemService.saveWithOk(999);
    Assert.isTrue(confSystemService.checkExist(999));
}

 

第一個單元測試中,模擬了一個報錯,事務就回滾了,因此檢查出數據不存在。其中實際調用了代理類的saveWithErr方法,而這個方法中調用了被代理對象的saveWithErr方法,而這個方法中拋出了一個RuntimeException,這個異常被代理類的saveWithErr方法檢測到,並且代理類發現當前方法處於事務註解的做用下,因此代理類就會將事務回滾,最後再將這個RuntimeException拋出去,讓調用者知道這個方法報錯了。

事務沒有回滾

以上提到了回滾的兩個必要條件:

  1. 當前方法處於事務註解的做用範圍內
  2. 方法得拋出RuntimeException或Error的子異常,或配置的指定異常

測試第1條,場景:服務類中未出於事務註解做用下的方法調用了,處於事務註解做用下的方法。服務類中添加以下代碼:

public void funcWithNoTrans(int id){
    saveWithErr(id);
}

 

測試類中添加以下測試用例(和以前的test1很相似,至不要調用的服務方法不同):

@Test
public void test5() {
    try{
        confSystemService.funcWithNoTrans(888);
    }catch (Exception e){
        e.printStackTrace();
    }
    Assert.isTrue(!confSystemService.checkExist(888));
}

 

 這個測試用例沒法經過。是由於代理類執行當前方法時,雖然執行的被代理對象的方法中拋出了異常,可是代理類發現當前方法(funcWithNoTrans)並非處於事務註解做用下,因此事務沒有回滾。

 測試第2條,場景:沒有拋出uncheck exception,而是拋出了自定義的異常,同時沒有作對應配置,在服務類中添加代碼:

// 自定義異常
public static class MyException extends Exception {
    // ...
}

// 沒有配置
@Transactional
public void saveWithCustomException(int id) throws MyException {
    ConfSystem confSystem = new ConfSystem();
    confSystem.setConfigurename("123");
    confSystem.setConfigurevalue("456");
    confSystem.setConfsystemid(id);

    confSystemRepository.save(confSystem);
    throw new MyException();
}

// 有配置
@Transactional(rollbackOn = MyException.class)
public void saveWithCustomCfgException(int id) throws MyException {
    ConfSystem confSystem = new ConfSystem();
    confSystem.setConfigurename("123");
    confSystem.setConfigurevalue("456");
    confSystem.setConfsystemid(id);

    confSystemRepository.save(confSystem);
    throw new MyException();
}

 

測試代碼:

    @Test
    public void test6() {
        try{
            confSystemService.saveWithCustomCfgException(111);
        }catch (Exception e){
            e.printStackTrace();
        }
        Assert.isTrue(!confSystemService.checkExist(111),"拋出已配置的自定義異常");
    }

    @Test
    public void test7() {
        try{
            confSystemService.saveWithCustomException(222);
        }catch (Exception e){
            e.printStackTrace();
        }
        Assert.isTrue(confSystemService.checkExist(222),"拋出未配置的自定義異常");
    }

 

兩個測試都能經過。

還有另一個場景:服務類內部出現異常了,可是內部try...catch處理掉了,致使代理類檢測不出來被代理對象內部其實出現了異常,最終事務也沒有回滾。

ps:測試發現,換用另外一個spring包下的Transactional註解,具備以上相同的效果,只不過配置註解那裏,得改爲 rollbackFor。這兩個註解功能上其實沒啥區別,只是配置的屬性名有點差別而已。

 

其餘

  被註解修飾的函數是運行在一個事務內,因此要保證這個函數運行的時間要儘量短(如不要穿插網絡請求,實在不行的話,則將網絡請求剝離到這個事務方法以外,這樣就不影響這個方法的執行時間了)。並且在這個函數內,該拋的運行時異常要拋出來,而不要trycatch住,不然致使事務沒法回滾

  事務註解,加到service方法上,service方法內調用不一樣的dao來操做數據。一個sevice方法內有兩個數據庫修改操做以上,才須要在這個service方法上加事務註解

相關文章
相關標籤/搜索