参考:https://www.jianshu.com/p/f5fc14bde8a0
后续测试代码的完整项目:https://files.cnblogs.com/files/hellohello/demo2.rar
后续说的事务注解都是指 import javax.transaction.Transactional;事务注解如果修饰在类上,则等价与作用在这个类的所有方法上,如果仅修饰在函数上,则仅仅作用在这个函数上,对其他函数没有效果。
- 只要加了事务注解,不管是加到bean上,还是加到bean中的函数上,spring才会生成一个代理对象与对应的bean,共两个对象。
- 注入到容器中的是代理对象,而不是被代理的bean对象。
- 代理对象实际上是被代理对象的子类,通过CGLib动态生成的。
- 其他bean中注入的虽然是代理对象,但是对于开发人员来说,就跟直接调用实际的bean对象一样,是透明的,因为代理对象内部会调用被代理对象的对应函数。
- 代理对象调用被代理对象函数时,只有抛出了uncheck exception(RuntimeException或Error)时【或配置其他异常】,并且抛出异常的函数处于事务注解的作用范围内时,事务才会回滚。
测试1、2、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),"判断是否是父类子类关系"); }
如果把服务类中的事务注解去掉,则不会生成代理对象,那上面例子中的静态属性保存的对象,跟其他地方注册的服务对象,就是同一个对象了。
测试4、5点:
在服务类中添加如下代码:
/** * 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抛出去,让调用者知道这个方法报错了。
事务没有回滚
以上提到了回滚的两个必要条件:
- 当前方法处于事务注解的作用范围内
- 方法得抛出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方法上加事务注解