事务方法A调用事务方法B,当方法B抛出的异常被方法A catch后会发生什么?
场景描述
在一个事务方法中调用另一个事务方法。如在ServiceA的methodA方法中调用ServiceB的methodB方法,两个方法都设置了事务,传播机制都是PROPAGATION_REQUIRED。
ServiceB的methodB方法声明事务如下。
public class ServiceB{
@Transactional(rollbackFor = Exception.class)
public void methodB(){
}
}
在methodA方法中捕获methodB异常,代码如下。
public class ServiceA{
public void methodA(){
try{
serviceB.methodB();
}catch(Exception e){
// do
}
}
}
methodA没有加事务注解,但methodA是在事务中执行的,也是因为如此,我才调试了半天Spring事务源码。其效果等同于:
public class ServiceA{
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void methodA(){
try{
serviceB.methodB();
}catch(Exception e){
// do
}
}
}
当methodB方法抛出异常后,当前事务回滚,异常往外抛出,被methodA方法catch。由于methodA方法catch了异常,异常不再往外抛出,当methodA方法执行完成时,事务切面走的不是回滚逻辑,而是提交逻辑。这就出现了如下异常。
异常信息:
Transaction rolled back because it has been marked as rollback-only
异常原因追溯
由于methodB方法抛出异常导致事务已经回滚,且当前事务被标志为仅回滚,因此当前事务只能回滚,不能再执行提交,如果执行提交,就能看到上述异常。该异常在AbstractPlatformTransactionManager的processRollback方法抛出。该方法源码如下。
public abstract class AbstractPlatformTransactionManage{
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
try {
triggerBeforeCompletion(status);
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
else {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
catch (RuntimeException | Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}
}
没有声明事务为什么会存在事务?
虽然方法没有声明事务,可是该方法却在事务中执行,那么我们可以在TransactionAspectSupport的invokeWithinTransaction方法中下断点调试。invokeWithinTransaction方法中会调用TransactionAttributeSource的getTransactionAttribute方法获取事务的配置信息。
如使用注解声明事务时,会调用AnnotationTransactionAttributeSource的getTransactionAttribute方法,经调试得知,这里调用的是NameMatchTransactionAttributeSource的getTransactionAttribute方法,如下图所示。
ServiceA的methodA方法匹配了'*'这一项。可是这又是在哪里配置的呢?只要找出在哪里配置的,将配置去掉问题也就能解决了。
首先找到nameMap字段是在什么时候初始化的,什么时候赋值的。
看源码可知:在NameMatchTransactionAttributeSource的setProperties方法中调用setNameMap方法为nameMap字段赋值,而setProperties方法由TransactionAspectSupport的setTransactionAttributes调用,该方法的源码如下。
public void setTransactionAttributes(Properties transactionAttributes) {
NameMatchTransactionAttributeSource tas = new NameMatchTransactionAttributeSource();
tas.setProperties(transactionAttributes);
this.transactionAttributeSource = tas;
}
再继续查看哪里会调用TransactionAspectSupport的setTransactionAttributes方法。
最终找到是项目中配置事务拦截器时注入的。
因为我对这个项目不熟悉,所以才有这么一波源码分析的操作。
这个事务拦截器是怎么生效的呢?
这个事务拦截器是怎么生效的?答案是通过InstantiationAwareBeanPostProcessor代理bean,拦截bean的public方法的执行,交给事务拦截器TransactionInterceptor处理。项目中的配置如下。
@Bean
public BeanNameAutoProxyCreator getBeanNameAutoProxyCreator() {
BeanNameAutoProxyCreator creator = new BeanNameAutoProxyCreator();
// 设置方法拦截器的bean名称
creator.setInterceptorNames("getTransactionInterceptor");
// 拦截哪些bean
creator.setBeanNames("*Service", "*ServiceImpl");
// 使用cglib
creator.setProxyTargetClass(true);
creator.setOrder(100);
return creator;
}
解决方案
在与同事沟通后,本来想将这些配置去掉,但去掉后会导致一些事务方法不生效,如:
public class Servie{
public void method1(){
this.method2();
}
@Transactional
public void method2(){
}
}
如上面代码所示,这种情况下method2方法的事务是不生效的。method1方法虽然没有加事务注解,但由于加了BeanNameAutoProxyCreator配置,等同于给该方法加了事务注解,所以methid1方法的事务生效,所以method2也能在事务中执行。
去掉配置后对系统的影响很大,事务不生效会引发很多问题,将整个系统让测试部门重新测试一遍也不现实。那么怎么解决这个问题呢?
既然所有业务类的public方法都会被放在事务中执行,那么我就添加一个注解@NotNeedTransactional,被该注解声明的方法不在事务中执行,与@Transactional的作用正好相反。这样问题就能解决。(为什么不打catch去掉?业务需要)
那么,怎么让@NotNeedTransactional注解生效呢?
继承事务拦截器,重写invoke方法,判断如果方法加了@NotNeedTransactional注解,则直接调用方法,不走切面。代码如下。
@Bean(name = "getTransactionInterceptor")
public TransactionInterceptor getTransactionInterceptor(AbstractPlatformTransactionManager transactionManager) {
TransactionInterceptor ti = new TransactionInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 有@NotNeedTransaction注解
if (invocation.getMethod().getAnnotation(NotNeedTransaction.class) != null) {
return invocation.proceed();
} else {
return super.invoke(invocation);
}
}
};
ti.setTransactionManager(transactionManager);
ti.setTransactionAttributes(getTransactionAttributes());
return ti;
}
总结
本篇给出的解决方案我个人也不建议使用,也是因为目前想不到完美的解决方案。
如果这个配置不去掉,未来可能遇到的问题会更多。比如可能会将原本一个小的事务变成一个大事务。