Spring中@Transactional失效
Spring中的声明式注解@Transactional
很大程度的方便了开发者进行DB数据保存。但是在一些特殊情况下,可能会造成注解不是按想定的方式生效,这里说几种可能造成的几种情况。
常见的几种情况:
异常被捕获
这是一种比较简单不过稍不注意也可能会犯的情况。
Spring中事务提交还是回滚是根据调用的方法是否抛出异常来决定的,因此如果把异常捕获之后又不抛出的话,即使出了问题,事务还是会提交。
@Autowired
private ClassB b;
@Autowired
private ClassC c;
@Transactional
public void methodA(){
try {
b.methodB();
c.methodC();
} catch (Exception e) {
}
}
上例中,想要的结果是b.methodB()
和c.methodC()
同时提交或回滚,但是由于异常被捕获,即使在执行方法C的时候出现了异常,方法B的操作仍旧会生效。
(如果方法A是一次转账,方法B是转账中的加钱操作,方法C是减钱操作,B和C只执行其中一个的话会导致总金额就发生了变化)
@Transactional修饰了非public方法
这种也是有可能犯的一种情况。
@Transactional
只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启AspectJ
代理模式。(默认代理模式CGlib)
CGlib是使用继承进行动态代理的,所以理论上protect方法无修饰符时应该也可以?没有测试,有空试一下。
同一个类中的方法调用
这是一种非常容易犯,又不容易察觉的情况。
@Component
public class A{
@Transactional
public void methodA() {
methodB();
}
@Transactional
public void methodB() {
// do something
}
}
上例中,methodB的事务是不生效的,因为这里是同一个类中的调用(更确切的说是同一个类同一个对象中),Spring事务的原理是调用时检查@Transactional
注解,然后生成代理类进行事务管理,但是内部调用时不会生成代理类(或者说默认不会),因此也就无法进行事务管理。
这种情况有好几种解决方法,下面会说到。
非常见的情况
非常见是指对@Transactional
进行了一些属性的配置导致不生效。
@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性。
// 希望自定义的异常进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)
@Transactional 注解属性 propagation 设置错误
这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。
- PROPAGATION.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- PROPAGATION.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- PROPAGATION.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
propagation默认值PROPAGATION.REQUIRED
:没有事务时创建事务。有事务了则加入该事务(相当于使用一个session)
同一个类中事务方法调用解决方法
方法拆分
这是一种最简单的方法,也就是把上面例子中的methodB拆分到一个单独的类里面,这样就是一般情况下的事务调用。
下面三种方法都是在methodB上添加@Transactional(propagation = Propagation.REQUIRES_NEW)
之后进行的测试
使用AspectJ代理
具体操作就是,application.yml中需要配置spring.aop.auto :true
,然后在启动类开启AspectJ代理,并暴露代理类:@EnableAspectJAutoProxy(exposeProxy = true)
这样的话就可以在调用的时候获取到代理类,并进行方法调用:
((TestClassA)AopContext.currentProxy()).insertB();
从ApplicationContext获取Bean
这个原理应该是和上面一样的,直从ApplicationContext中获取到当前Bean,然后再调用方法:
// applicationContext 可以自动注入
applicationContext.getBean(TestClassA.class).insertB();
注入自身
@Component
public class TestClassA {
@Autowired
private TestClassA testClassA;
}
用这种方法也可以使methodB的事务生效,但是需要注意的是,第一次生成的代理类和自动注入的代理类不是一个对象,也就是代码中this
和testClassA
不是同一个对象,并且testclassA
中不会再次自动注入。
所以如果methodB插入数据时需要当前对象的属性,这种方法便不能再使用
总之,同类事务不生效是一种很容易疏忽的情况,具体怎么处理,还是要结合业务。