这又是导致事务注解@Transactional不生效的一个原因

原创 吴就业 79 0 2020-05-14

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/f12ff6511cf1432fabd1f4c98a1d4150

作者:吴就业
链接:https://wujiuye.com/article/f12ff6511cf1432fabd1f4c98a1d4150
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

事务方法A调用事务方法B,当方法B抛出的异常被方法A catch后会发生什么?

场景描述

在一个事务方法中调用另一个事务方法。如在ServiceAmethodA方法中调用ServiceBmethodB方法,两个方法都设置了事务,传播机制都是PROPAGATION_REQUIRED

ServiceBmethodB方法声明事务如下。

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方法执行完成时,事务切面走的不是回滚逻辑,而是提交逻辑。这就出现了如下异常。

img

异常信息:

Transaction rolled back because it has been marked as rollback-only

异常原因追溯

由于methodB方法抛出异常导致事务已经回滚,且当前事务被标志为仅回滚,因此当前事务只能回滚,不能再执行提交,如果执行提交,就能看到上述异常。该异常在AbstractPlatformTransactionManagerprocessRollback方法抛出。该方法源码如下。

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);
	}
    }
}

没有声明事务为什么会存在事务?

虽然方法没有声明事务,可是该方法却在事务中执行,那么我们可以在TransactionAspectSupportinvokeWithinTransaction方法中下断点调试。invokeWithinTransaction方法中会调用TransactionAttributeSourcegetTransactionAttribute方法获取事务的配置信息。

如使用注解声明事务时,会调用AnnotationTransactionAttributeSourcegetTransactionAttribute方法。经调试得知,这里调用的是NameMatchTransactionAttributeSourcegetTransactionAttribute方法,如下图所示。

img

ServiceAmethodA方法匹配了'*'这一项。可是这又是在哪里配置的呢?只要找出在哪里配置的,将配置去掉问题也就能解决了。

首先找到nameMap字段是在什么时候初始化的,什么时候赋值的。

看源码可知:在NameMatchTransactionAttributeSourcesetProperties方法中调用setNameMap方法为nameMap字段赋值,而setProperties方法由TransactionAspectSupportsetTransactionAttributes调用,该方法的源码如下。

public void setTransactionAttributes(Properties transactionAttributes) {
	NameMatchTransactionAttributeSource tas = new NameMatchTransactionAttributeSource();
	tas.setProperties(transactionAttributes);
	this.transactionAttributeSource = tas;
}

再继续查看哪里会调用TransactionAspectSupportsetTransactionAttributes方法。

最终找到是项目中配置事务拦截器时注入的。

img

因为我对这个项目不熟悉,所以才有这么一波源码分析的操作。

这个事务拦截器是怎么生效的呢?

这个事务拦截器是怎么生效的?答案是通过InstantiationAwareBeanPostProcessor代理bean,拦截beanpublic方法的执行,交给事务拦截器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(){
        
    }
    
}

如上面代码所示,method1方法虽然没有加事务注解,但由于加了BeanNameAutoProxyCreator配置,等同于给该方法加了事务注解,所以methid1方法的事务生效,虽然method1调用method2,method2的事务注解不生效,但由于method1在事务中,所以method2也能在事务中执行。

如果我们把BeanNameAutoProxyCreator配置去掉,那么method1就不会在事务中执行,这种情况下method2方法的事务注解也是不生效的,因为method1是通过this调用,不是调用代理对象的method2方法,所以method2的事务注解的AOP逻辑没有执行,那么method1和method2的事务就都不生效了。但我们希望method2的事务生效。

此案例我们可以通过获取代理类实例再调用method2方法,而不是使用this调用。

public class Servie{
    public void method1(){
      getBean(Service.class).method2();
    }
    
    @Transactional
    public void method2(){
        
    }
}

但是,整个项目可能存在很多像method1方法一样需要依赖BeanNameAutoProxyCreator配置而使事务生效的。如果直接去掉配置,对系统的影响很大。

为什么不把method1中catch异常的逻辑去掉呢?使method1和method2合并成一个事务,提交一起提交回滚一起回滚。因为业务需要,method1方法不抛出异常,所以实际上method1也是不需要在事务中执行的。只是method2需要在事务中执行。

那么怎么解决这个问题呢?

既然所有业务类的public方法都会被放在事务中执行,那么我就添加一个注解@NotNeedTransactional,被该注解声明的方法不在事务中执行,与@Transactional的作用正好相反。这样问题就能解决。

那么,怎么让@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;
}

案例修改如下:

public class Servie{
    @NotNeedTransactional
    public void method1(){
      getBean(Service.class).method2();
    }
    
    @Transactional
    public void method2(){
        
    }
    
}

由于method1声明了@NotNeedTransactional注解,TransactionInterceptor中,直接调用method1方法,不走事务逻辑了。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Java反序列化JSON,要避免泛型的类型擦除问题

如图,反序列化JSON数组正常,却在获取数组元素时抛出了类型转换异常。

基准测试框架JMH快速上手

基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。

如何获取泛型类的参数化类型?解密Java泛型

框架怎么知道这个`T`到底是什么类型呢?

在同一个线程下数据源多次切换的回溯问题

在某些场景下,我们可能需要多次切换数据源才能处理完同一个请求,也就是在一个线程上多次切换数据源。

深入理解类加载阶段之准备阶段

准备阶段是为类中定义的静态变量分配内存并设置初始化值的阶段,这里的初始值通常情况下指的是对应类型的零值,比如int类型的零值为0。

访问者模式在ASM框架中的使用

访问者模式的定义是:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。