Spring事务管理方式,我们大部分都是使用声明式来实现,即贴@Transacational注解。但是在我们使用的过程中,会因为使用不当而导致事务失效的问题。下面就罗列出事务失效的常见使用场景,并加以讲解。
场景1:spring的事务注解@Transactional只能放在public非final修饰的方法上才起作用,如果放在其他非public(private,protected)方法上,事务不起作用。
在应用系统调用声明了 @Transactional 的目标方法时,Spring 默认使用 AOP 代理,而Spring AOP实现方式有两种:jdk动态代理实现和cglib动态代理实现,但是无论使用jdk动态代理还是cglib动态代理,@Transactional也只能放在public或public final修饰的方法上才起作用。
下面是从源码的角度来看其中的原因。
jdk动态代理实现
目前,我们大部分业务代码都是写在Service层,即创建一个Service接口,然后创建ServiceImpl类,且该类实现Service接口,再在实现类的对应实现方法上写相关业务逻辑。
这种方式,如果我们在ServiceImpl类或方法上贴上@Transactional 注解,实际上底层是使用了jdk动态代理,会动态的生成一个Service代理对象。
类似这样:
/**
* @author: Longer
*/
public interface PersonService {
void eat();
}
/**
* @author: Longer
*/
@Service
public class PersonServiceImpl implements PersonService {
@Transactional
@Override
public void eat() {
}
}
源码解析
newProxyInstance类是jdk动态生成代理对象的时候需要调用的类。
public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* 首先从缓存查找是否有代理类,没有就生成一个
*/
Class> cl = getProxyClass0(loader, intfs);
/*
* 通过InvocationHandler调用目标类的构造函数
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
//如果构造函数不是public修饰,修改
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
其中查找Proxy类的源码如下:
private static Class> getProxyClass0(ClassLoader loader,
Class>... interfaces) {
//长度检查
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
//调用了下面的WeakCache.get(K key, P parameter)方法,loader作为key,interfaces作为parameter参数
//定义如下:proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory())
return proxyClassCache.get(loader, interfaces);
}
//首先当前key(也就是上面的ClassLoader)已经加载存在,就直接从缓存中返回
//如果不存在,就会通过ProxyClassFactory来创建代理对象
public V get(K key, P parameter) {
Objects.requireNonNull(parameter);
expungeStaleEntries();
//根据key的hash值和一个ReferenceQueue来构造
Object cacheKey = CacheKey.valueOf(key, refQueue);
// 从map中取出cacheKey的值
ConcurrentMap
再看上面提到的ProxyClassFactory类
//类定义
private static final class ProxyClassFactory
implements BiFunction[], Class>>{
@Override
public Class> apply(ClassLoader loader, Class>[] interfaces) {
Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class> intf : interfaces) {
/*
* 校验当前类加载器ClassLoader解析到的名称和定义的名称是否相同
*/
Class> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* 校验是否是接口类型,这也就是为什么JDK动态代理只能基于接口
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* 防重
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
// 代理对象的目录
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
.....
/*
* 生成指定Proxy代理对象的字节码
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
//调用的native方法
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* 生成的代理类有bug
*/
throw new IllegalArgumentException(e.toString());
}
}
}
从ProxyClassFactory类,我们可以看到,该类是会去校验目标类是否是接口类型。这也就是为什么jdk动态代理只能基于接口实现。
JDK动态代理生成的代理class类名实际上是这样的:public final class $Proxy0 extends Proxy implements PersonService{}(PersonService是被代理接口)
继承了Proxy 和实现了目标接口类PersonService。因为Java不允许多重继承,这就限制了:使用JDK代理不能是普通类或者抽象类,只能是接口类型。
由于接口定义的方法是public的,java要求实现类所实现接口的方法必须是public的(不能是protected,private等),同时不能使用static的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施AOP增强,也即不能进行Spring事务增强
cglib动态代理实现
对于普通@Service注解的类(未实现接口)并通过 @Autowired直接注入类的方式,是通过cglib动态代理实现的。
类似这样:
/**
* @author: Longer
*/
@Service
public class PersonServiceImpl{
@Transactional
public void eat() {
}
}
public class PersonController{
@Autowired
private PersonServiceImpl personService;
@PostMapping(value = "/eat")
public Result eat() {
personService.eat();
return Result.ok();
}
}
cglib动态代理生成的代理class类名实际上是这样的:
public class Student$$EnhancerByCGLIB$$92f3e3f6 extends Student implements Factory(){}
Student 是被代理类。可以看到代理类是继承了被代理类Student ,这就是与jdk动态代理的区别。jdk代理类是实现被代理接口。
由于cglib是继承代理类,而Java继承由于使用final,static,private修饰符的方法都不能被子类复写,所以这些方法将不能被实施的AOP增强,即不会生成cglib代理对象。所以事务是不生效的。
结论:
cglib字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行AOP增强植入的,由于使用final,static,private修饰符的方法都不能被子类复写,所以这些方法将不能被实施的AOP增强。即除了public的非final的实例方法,其他方法均无效。
场景2:方法自调用
目标类直接调用该类的其他标注了@Transactional 的方法(相当于调用了this.对象方法),事务不会起作用。事务不起作用其根本原因就是未通过代理调用,因为事务是在代理中处理的,没通过代理,也就不会有事务的处理。
类似下面的写法,事务是不生效:
/**
* @author: Longer
*/
@RestController
@RequestMapping("/person")
public class PersonController {
private PersonServiceImpl personServiceImpl;
@GetMapping(value = "/eat")
public Result> eat() {
personServiceImpl.eat();
return Result.ok();
}
}
/**
* @author: Longer
*/
@Service
public class PersonServiceImpl{
public void eat() {
run();
}
@Transactional
public void run(){
}
}
场景3:事务方法抛出非RuntimeException异常,事务不回滚
原因:Spring 默认只为 RuntimeException 异常回滚事务,如果方法往外抛出 checked exception,该方法虽然不会再执行后续操作,但仍会提交已执行的数据操作。这样可能使得只有部分数据提交,造成数据不一致。
比如下面的代码,不会回滚:
@Transactional
public void eat() throws IOException {
Person person = new Person();
person .setId(1);
this.save(person);
throw new IOException("testCheckedTran");
}
代码不回滚的原因是在插入数据库之后,抛出了IOException,这个异常不属于RuntimeException ,所以不会回滚。
解决办法:自定义回滚策略,可使用@Transactional 的 noRollbackFor,noRollbackForClassName,rollbackFor,rollbackForClassName 属性。如使用:@Transactional(rollbackFor = Exception.class)或@Transactional(rollbackFor = Throwable.class)
场景4:方法部分代码try...catch住,而catch语句块没有往外跑出回滚异常,事务也不会回滚。
在业务代码上,一般不try...catch住,如果要捕获异常的话,需要在catch里跑出回滚异常,否则事务不会回滚。
场景5:数据库存储引擎不支持事务
如:使用mysql作为数据库的话,如果存储引擎不是INNODB,而是MyISAM的话,则事务不生效。
场景6:如果采用spring+spring mvc,则context:component-scan重复扫描问题可能会引起事务失败
如果spring和mvc的配置文件中都扫描了service层,那么事务就会失效。
原因:因为按照spring配置文件的加载顺序来讲,先加载springmvc配置文件,再加载spring配置文件,我们的事物一般都在srping配置文件中进行配置,如果此时在加载srpingMVC配置文件的时候,把servlce也给注册了,但是此时事物还没加载,也就导致后面的事物无法成功注入到service中。所以把对service的扫描放在spring配置文件中或是其他配置文件中。
场景7: 多个事务管理器
当一个应用存在多个事务管理器时,如果不指定事务管理器,@Transactional 会按照事务管理器在配置文件中的初始化顺序使用其中一个。
如果存在多个数据源 datasource1 和 datasource2,假设默认使用 datasource1 的事务管理器,当对 datasource2 进行数据操作时就处于非事务环境。
解决办法是,可以通过@Transactional 的 value 属性指定一个事务管理器。在使用多个事务管理器的情况下,事务不生效的原因在本系列后续文章中会有分析
参考文献链接:
Spring事务Transactional和动态代理-事务失效的场景
jdk动态代理
cglib动态代理