问题概述
在Transactional方法中使用this方式调用另一个Transactional方法时,拦截器无法拦截到被调用方法,严重时会使事务失效。
类似以下代码:
@Transactional
public void insertBlogList(List<Blog> blogList) {for (Blog blog : blogList) {this.blogMapper.insertBlog(blog);}try {TimeUnit.SECONDS.sleep(15);} catch (InterruptedException e) {e.printStackTrace();}
}@Transactional
public void deleteBlogByCondition(BlogSearchParameter parameter) {List<Blog> blogs = this.blogMapper.selectBlogByParameter(parameter);for (Blog blog : blogs) {this.blogMapper.deleteBlog(blog.getId());}// 抛出一个RuntimeExceptionthrow new RuntimeException("deleteBlogByCondition抛出一个异常");
}@Transactional
public void insertAndDeleteBlogList2(List<Blog> blogList, BlogSearchParameter parameter) {// 插入数据this.insertBlogList(blogList);// 删除数据try {this.deleteBlogByCondition(parameter);} catch (Exception e) {System.err.printf("Err:%s%n", e.getMessage());}System.out.println("继续插入数据");// 继续插入数据this.insertBlogList(blogList);
}
正常情况下,执行到"继续插入数据"时会抛出一个"rollback only"的异常,然后事务回滚。
而现在的现象是:
- 三个操作都不会开启事务,出现异常也不会回滚
- "删除数据"操作会把符合条件的数据都删除掉
- "继续插入数据"操作会再插入数据
原因分析
在EnableTransactionManagement注解mode属性的文档中:
The default is AdviceMode.PROXY. Please note that proxy mode allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way; an Transactional annotation on such a method within a local call will be ignored since Spring's interceptor does not even kick in for such a runtime scenario. For a more advanced mode of interception, consider switching this to AdviceMode.ASPECTJ.
大概意思是:mode属性的默认值是AdviceMode.PROXY,这种方式仅允许通过代理对来调用事务方法,同一个类的本地调用无法被事务切面拦截。如果要解决这个问题,可以使用AdviceMode.ASPECTJ模式。
其实这个问题的根本原因与spring-tx无关,而是spring-aop的实现方式造成的。
从spring-aop拦截器分析问题原因
在DynamicAdvisedInterceptor和JdkDynamicAopProxy中有一段类似的代码:
其中target就是原始的业务层Bean对象。
在后续创建ReflectiveMethodInvocation/CglibMethodInvocation时又将此target传递了进去:
// JDK
MethodInvocation invocation =new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();// Cglib
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
proceed方法中在拦截器链最后会调用目标方法:
public Object proceed() throws Throwable {// We start with an index of -1 and increment early.if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {return invokeJoinpoint();}// 略
}protected Object invokeJoinpoint() throws Throwable {// 反射调用目标方法// 这个target就是原始Bean对象return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}
所以如果在目标方法中使用this方法调用另一个需要被拦截的方法,将不会执行拦截逻辑。