# 事务 @Transactional 注解的失效请狂
当我们使用 @Transactional 声明式事务时,有一些情况会导致事务并没有重启,因此总结一下我已经遇到了的 @Transactional 不启用的情况
# 1. 同一类里,未标注 @Transactional 的方法调用了标注 @Transactional 的方法
对我来说,这个是最常遇到的情况,因为声明式事务是根据 Spring AOP 来实现的 (即环绕通知,在方法执行前声明一个事务,并在方法结束后关闭事务), 但是以下这种情况就不会触发事务
题外话 : AOP 一共有三种织入方式
| 织入方式 | 说明 |
|---|
| 编译时织入 | 在编译的时候,把切面代码融合进来,需要特殊的编译器,AspectJ 属于这一类 |
| 类加载时织入 | 在类被加载进内存的时候织入,这需要特殊的类加载器,AspectJ 实现了此类 |
| 运行时织入 | 在运行的时候,通过动态代理的方式织入,调用切面代码增强业务,Spring 就是使用的这种方式,会带来性能上的开销,但是不用特殊的编译器或者类加载器 |
1 2 3 4 5 6 7 8 9 10
| public class temp { public void createFirst() { this.createSecond(); }
@Transactional public void createSecond() { } }
|
当外部方法新建一个 temp 类并且调用 createFirst 方法时,并不会触发事务,因为事务功能需要通过代理对象来实现,而在 createFirst() 方法中,调用了 this.createSecond() , 这里的 this 指向的是 temp 对象而不是代理对象,所以不会经过加强处理,就不会有事务支持了
我们可以先验证以下上面的对错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Transactional( rollbackFor = {Exception.class} ) public boolean saveOrUpdate(T entity) { if (null == entity) { return false; } else { TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass); Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!", new Object[0]); String keyProperty = tableInfo.getKeyProperty(); Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!", new Object[0]); Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty()); return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity); } }
@GetMapping("transfer")
public R transfer(@RequestBody List<Employee> employees) { Employee rollOut = employees.get(0); Employee shiftTo = employees.get(1); if (rollOut == null || shiftTo == null) { log.info("参数错误"); return new R(false, null, "参数错误"); } else { UpdateWrapper<Employee> rollOutWrapper = new UpdateWrapper<>(); UpdateWrapper<Employee> shiftToWrapper = new UpdateWrapper<>(); rollOutWrapper.set(rollOut.getSalary() >= 500 && rollOut.getEmployeeId() != null, "salary", rollOut.getSalary() - 500); shiftToWrapper.set(shiftTo.getEmployeeId() != null, "salary", shiftTo.getSalary() + 500); rollOutWrapper.eq("employee_id", rollOut.getEmployeeId()); shiftToWrapper.eq("employee_id", shiftTo.getEmployeeId()); employeeService.saveOrUpdate(rollOut, rollOutWrapper); int i = 1 / 0; employeeService.saveOrUpdate(shiftTo, shiftToWrapper); return new R(true, true); } }
|
运行结果 :
![20220603023209]()
运行前数据运行后数据 :
![20220603023951]()
运行后数据可以看到, 只有一组数据发生了变化, 也就是说并没有回滚, 事务并没有启动
那我们有什么办法能让这种调用生效吗
刚刚分析了一下,出问题的地方在于 this 的指向问题,通过 this 直接绕过了代理类,所以如果我们指定代理类,也就是说将 this 换成代理类应该就可以解决问题了
但是不能用上面那个例子了,mybatis-plus 的源码太深了不好进行改动,我自己写一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @RestController public class TempController { @Autowired private TempService service;
@GetMapping("/temp")
public void temp() { service.createFirst(); } }
public class TempServiceImpl implements TempService { @Autowired private EmployeeDao employeeDao; @Override public void createFirst() { this.createSecond(); }
@Override @Transactional(rollbackFor = Exception.class) public void createSecond() { Employee employee1 = employeeDao.selectById(123); Employee employee2 = employeeDao.selectById(124); UpdateWrapper<Employee> employee1Wrapper = new UpdateWrapper<>(); UpdateWrapper<Employee> employee2Wrapper = new UpdateWrapper<>(); employee1Wrapper.set(employee1.getSalary() >= 500 && employee1.getEmployeeId() != null, "salary", employee1.getSalary() - 500); employee2Wrapper.set(employee1.getEmployeeId() != null, "salary", employee1.getSalary() + 500); employee1Wrapper.eq("employee_id", employee1.getEmployeeId()); employee2Wrapper.eq("employee_id", employee1.getEmployeeId()); employeeDao.update(employee1, employee1Wrapper); int i = 1 / 0; employeeDao.update(employee2, employee2Wrapper); } }
|
运行结果 :
![20220603121806]()
可以在 mybatis 的日志里面看到,使用的是非事务 SqlSession, 并没有回滚
稍微改一下代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @RestController public class TempController { @Autowired private TempService service;
@GetMapping("/temp")
public void temp() { service.createFirst(service); } }
@Service public class TempServiceImpl implements TempService { @Autowired private EmployeeDao employeeDao; @Override public void createFirst(TempService service) { service.createSecond(); }
@Override @Transactional(rollbackFor = Exception.class) public void createSecond() { Employee employee1 = employeeDao.selectById(123); Employee employee2 = employeeDao.selectById(124); UpdateWrapper<Employee> employee1Wrapper = new UpdateWrapper<>(); UpdateWrapper<Employee> employee2Wrapper = new UpdateWrapper<>(); employee1Wrapper.set(employee1.getSalary() >= 500 && employee1.getEmployeeId() != null, "salary", employee1.getSalary() - 500); employee2Wrapper.set(employee1.getEmployeeId() != null, "salary", employee1.getSalary() + 500); employee1Wrapper.eq("employee_id", employee1.getEmployeeId()); employee2Wrapper.eq("employee_id", employee1.getEmployeeId()); employeeDao.update(employee1, employee1Wrapper); int i = 1 / 0; employeeDao.update(employee2, employee2Wrapper); } }
|
如果不出意外的话,那么应该这种方法就是事务类型的 :
![20220603122931]()
可以看到,验证正确
# 访问修饰符导致的 @Transactional 失效
@Transactional 是基于 AOP 来完成事务操作的,所以对于一些 AOP 的失效情况,对于事务控制来说也会失效
点击查看 : AOP 的实现原理
AOP 是无法代理私有方法的,而 @Transactional 也会检测所注释的方法,若不为 Public 则不会触发事务,而且不会有警告或者报错,比较容易忽视