Spring 事务第四篇 - Spring 事务遇到AOP会怎么样?

目录

前言 

InfrastructureAdvisorAutoProxyCreator

Spring 如何生成事务的代理对象

为什么同时开启事务和AOP时只保留AOP的后置处理器

一个方法同时被事务和 AOP 增强会有什么问题?

事务为什么失效?

如何解决同时开启事务和AOP时事务失效的问题?

总结


前言 

        前面的文章探索了怎么拦截事务方法,怎么对其进行增强。但是我们讲解 AOP 的时候也聊过,如果想对一个对象进行增强,首先要生成一个代理对象,然后找到增强对象的拦截器链,这样才能逐个执行拦截方法进行增强。此外本文还会解答下上文提到的一个问题,就是当 AOP 和事务同时开启的话会怎么办呢?

InfrastructureAdvisorAutoProxyCreator

        我们前文也已经分析过,通过 @EnableTransactionManagement 会引入该后置处理器,看一下它的继承结构

         相信看过 AOP 的盆友在这里看到 AbstractAutoProxyCreator 和 SmartInstantiationAwareBeanPostProcessor 等后置处理器都会很眼熟,可见事务对象的增强 AOP 增强的原理是一致的,这里不多介绍了,有需要的可以移步 Spring AOP 第二篇-Spring 如何解析切面获取切点,通知和生成代理对象,相信看完那一篇文章,就知道它的几个父类是干嘛的了。

Spring 如何生成事务的代理对象

       我们在上一篇介绍了对事务方法增强的 invoke 方法,但是要调用增强方法,首先是需要代理对象和拦截器链,我们接下来看一下 Spring 是如何对事务对象生成代理对象以及如何获取拦截器链。首先看一下生成代理对象的流程图,看不清的可以移步该链接 事务生成代理流程图https://www.processon.com/view/link/614ebc5e0791290c0c42a96ahttps://www.processon.com/view/link/614ebc5e0791290c0c42a96a

        简单解释一下上面的流程,事务引入了 InfrastructureAdvisorAutoProxyCreator 后置处理器,然后可以借助其父类,爷爷类等进行 advisor 的查找与匹配。上篇文章讲到事务引入了 BeanFactoryTransactionAttributeSourceAdvisor 类型的 advisor 它实现了 Advisor 接口,所以图中 advisorRetrievalHelper.findAdvisorBeans() 这一步会查找到该 advisor, 该 advisor 依赖 TransactionAttributeSource 进行 bean 匹配,其实就是根据 classFilter 和 methodMatcher, 我们上一篇已经介绍过,也就是根据 @Transactional 进行匹配。所以 findAdvisorsThatCanApply 也就是借助该方式来判断 advisor 是否跟当前bean匹配。如果匹配的话就说明该类或者类上的某个方法上写了 @Transactional 注解,就创建动态代理了。根据被代理对象和拦截器链创建动态代理的过程前面文章都有,这里就不展开了。

为什么同时开启事务和AOP时只保留AOP的后置处理器

        我们再看一下 AOP 创建代理对象的流程图,看不清可以移步链接 AOP 生成代理对象流程图https://www.processon.com/view/link/614ec7e0e0b34d7b343306e8https://www.processon.com/view/link/614ec7e0e0b34d7b343306e8

        AOP 创建代理对象的流程跟事务时基本差不多,只不过在查询 advisor 实例的时候,开启 AOP 时引入的后置处理器 AnnotationAwareAspectJAutoProxyCreator 重写了findCandidateAdvisors 方法,而该方法既通过调用父类 super.findCandidateAdvisors() 获取到实现了 Advisor 接口的 advisor (事务也是通过该方法获取自己需要的的 advisor), 又通过BeanFactoryAspectJAdvisorsBuilder 来创建自己通过注解配置所需要的 advisor。所以说InfrastructureAdvisorAutoProxyCreator 能干的事 AnnotationAwareAspectJAutoProxyCreator 也能干,所以当两个冲突时,只保留 AnnotationAwareAspectJAutoProxyCreator 就可以了。

        既然 AnnotationAwareAspectJAutoProxyCreator 功能完全覆盖了比 InfrastructureAdvisorAutoProxyCreator,那么只开启 AOP, 不开启事务,即只加@EnableAspectJAutoProxy 不要 @EnableTransactionManagement 行不行?答案当然是不行,因为 @EnableTransactionManagement,不仅仅是引入了后置处理器,还引入了 advisor 实例还有 TransactionAttributeSource,这才是事务的关键。

        既然 AnnotationAwareAspectJAutoProxyCreator 这么优秀,那开启事务的时候直接引入它不就行了么,为什么还要在跟 AOP 冲突的时候才选择了它呢?我觉得这样设计可能是因为比较清晰,因为事务的增强相对简单,只需要看方法或类上有没有 @Transactional 就行了,而 AOP 则很复杂,不同切点,可能只是参数类型不一样都会影响切入。所以实现过程也很复杂,如果只是开启事务的话就引入一些关于 AOP 的判断无疑是会造成一些性能消耗。此外事务和AOP分开管理也是降低了耦合性。

一个方法同时被事务和 AOP 增强会有什么问题?

//切面类
@Aspect
public class QueryAspect {
    //切点
    @Pointcut("execution(void com.transaction.service.DepositService.deposit(Long, Integer))")
    public void pointCut(){};

    @Before(value = "pointCut()")
    public void methodBefore(JoinPoint joinPoint) throws Exception {
        System.out.println("调用目标方法前 @Before 可以提前获取到参数信息:" + Arrays.toString(joinPoint.getArgs()));
        //模拟 before 出错
        //int a = 1 / 0;
    }

    @After(value = "pointCut()")
    public void methodAfter(JoinPoint joinPoint) {
        System.out.println("调用目标方法后 @After");
        // 模拟 After 异常
        //int a = 1 / 0;
    }

    @AfterReturning(returning = "result", pointcut = "pointCut()")
    public void methodAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("目标方法返回后 @AfterReturning, 此时可以获取到返回结果 " + result);
        //模拟 AfterReturning 出错
        //int a = 1 / 0;
    }

    @AfterThrowing(value = "pointCut()", throwing = "ex")
    public void methodAfterThrowing(JoinPoint joinPoint, Exception ex) {
        System.out.println("抛出异常后, @AfterThrowing " + ex);
        //模拟 @AfterThrowing 异常
        //int a = 1 / 0;
    }

    @Around(value = "pointCut()")
    public Object methodAround(ProceedingJoinPoint pdj) throws Throwable {
        Object result = null;
        System.out.println("调用目标方法前 @Around ");
        try {
            //调用目标方法
            result = pdj.proceed();
        } catch (Throwable ex) {
            System.out.println("捕捉到异常信息:" + ex);
        }
        System.out.println("调用目标方法后 @Around ");
        return result;
    }
}

//配置类
@EnableTransactionManagement
@ComponentScan({"com.transaction.*", "com.proxy"})
@EnableAspectJAutoProxy
public class TransactionConfig {

    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setUrl("jdbc:mysql://localhost:3306/transaction?useSSL=false");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setMinIdle(5);
        dataSource.setMaxActive(20);
        dataSource.setInitialSize(10);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public QueryAspect queryAspect() {
        return new QueryAspect();
    }
}

//业务接口类
public interface DepositService {
    
    public void deposit(Long userId, Integer money);
}

//业务实现类
@Service
public class DepositServiceImpl implements DepositService {

    @Autowired
    private AccountDao accountDao;

    @Override
    @Transactional
    public void deposit(Long userId, Integer money) {
        accountDao.updateAccountInfos(userId, money);
        //模拟事务出错
       int a = 1 / 0;
    }
}

//数据访问层
@Repository
public class AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
    public boolean updateAccountInfos(Long userId, Integer money) {
        String sql = "update AccountInfo set balance = balance + " + money + " where id = " + userId;
        jdbcTemplate.update(sql);

        // 模拟事务异常
        //int a = 1 / 0;
        return true;
    }
}

//测试类
public class TransactionMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TransactionConfig.class);
        DepositService depositService = (DepositService) applicationContext.getBeanFactory().getBean("depositServiceImpl");
        depositService.deposit(1L, 1);
    }
}
<dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>


    </dependencies>

        上面代码的执行结果为:

调用目标方法前 @Around 
调用目标方法前 @Before 可以提前获取到参数信息:[1, 1]
抛出异常后, @AfterThrowing java.lang.ArithmeticException: / by zero
调用目标方法后 @After
捕捉到异常信息:java.lang.ArithmeticException: / by zero
调用目标方法后 @Around

        从执行结果来看,当目标方法异常的发生异常的时候,@AfterThrowing 和 @Around 都捕捉到了这个异常, 但是事务却没有生效,即数据库还是更新了,并没有执行回滚操作,也就是说,事务失效了。

事务为什么失效?

        我们前面也分析过,获取到 advisors 数组之后,会进行排序,这样事务拦截器的顺序就排AOP拦截器之前执行了。首先需要分析一下拦截器执行的流程,关于 AOP 拦截器的执行顺序前面文章已经分析的很详细了,连接如下

Spring AOP - 通知方法的执行顺序https://blog.csdn.net/paralysed/article/details/120478652?spm=1001.2014.3001.5501https://blog.csdn.net/paralysed/article/details/120478652?spm=1001.2014.3001.5501Spring AOP-代理对象增强目标方法的流程,通知方法到底什么执行顺序https://blog.csdn.net/paralysed/article/details/120489042?spm=1001.2014.3001.5501https://blog.csdn.net/paralysed/article/details/120489042?spm=1001.2014.3001.5501   ​​​    结合事务再重新分析一下 advisor 的排序,事务引入的 advisor 类型为BeanFactoryTransactionAttributeSourceAdvisor,它继承了Advisor 接口,所以是先于 AOP 的 advisor 放入 advisor 数组的,所以 advisor 数组第一次排序之后事务的 advisor 是在 AOP 事务前面的。因为事务的 advisor 和 AOP 的 advisor 都是 Ordered 类型,所以 advisor 数组第二次排序的时候主要就看谁配置的 order 优先级更高。AOP 的 advisor 可以通过在切面类加 @Order 注解改变其所有拦截器的执行顺序,而事务可以在开启事务时就指定,比如 @EnableTransactionManagement(order = 100), 前面的文章我们分析了 AOP 默认的 order 为 Ordered.LOWEST_PRECEDENCE,而点开 EnableTransactionManagement 注解可以看到 int order() default Ordered.LOWEST_PRECEDENCE; 这一行代码,代表默认也是最低优先级。所以说如果 AOP 和事务都没有指定 order, 那么第二步排序之后 advisor 列表的顺序其实是没有发生变化的。那么 advisor 执行顺序就变成了 事务advisor->Around->Before->After->AfterReturning->AfterThrowing, 拦截器执行的流程就变成了如下 

try {
	try {

		// @Around 通知在主动调用目标方法前可以添加的增强逻辑

		//执行 @Before 的通知方法
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		try {
				try {
					//执行目标方法
					return invokeJoinpoint();
				} catch (Throwable ex) {
					if (shouldInvokeOnThrowing(ex)) {
						//执行 @AfterThrowing 的通知方法
						invokeAdviceMethod(getJoinPointMatch(), null, ex);
					}
					throw ex;
				}
				//执行 @AfterReturning 的通知方法
				this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		} finally {
			//执行 @After 的通知方法
			invokeAdviceMethod(getJoinPointMatch(), null, null);
		} 

		// @Around 通知调用目标方法后还可以再继续操作
		
	} catch (IllegalArgumentException ex) {
		throw new AopInvocationException("Mismatch on arguments to advice method [" +
				this.aspectJAdviceMethod + "]; pointcut expression [" +
				this.pointcut.getPointcutExpression() + "]", ex);
	} catch (InvocationTargetException ex) {
		throw ex.getTargetException();
	}
} catch (Throwable ex) {
	// 事务捕捉到异常后进行回滚操作
	completeTransactionAfterThrowing(txInfo, ex);
	throw ex;
} finally {
	cleanupTransactionInfo(txInfo);
}

        但是本例的 around 通知方法增强逻辑对目标方法捕获了异常,也相当于对所有的拦截器进行了捕获异常的操作。补充完 around 具体逻辑之后应该是下面的执行流程

try {
	try {

		Object result = null;
        System.out.println("调用目标方法前 @Around ");
        try {
			//执行 @Before 的通知方法
			this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
			try {
					try {
						//执行目标方法
						return invokeJoinpoint();
					} catch (Throwable ex) {
						if (shouldInvokeOnThrowing(ex)) {
							//执行 @AfterThrowing 的通知方法
							invokeAdviceMethod(getJoinPointMatch(), null, ex);
						}
						throw ex;
					}
					//执行 @AfterReturning 的通知方法
					this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
			} finally {
				//执行 @After 的通知方法
				invokeAdviceMethod(getJoinPointMatch(), null, null);
			} 
		} catch (Throwable ex) {
            System.out.println("捕捉到异常信息:" + ex);
        }
        System.out.println("调用目标方法后 @Around ");
        //如果执行过程中都没有异常,则返回执行结果,就不会执行事务的回滚操作
        return result;
	} catch (IllegalArgumentException ex) {
		throw new AopInvocationException("Mismatch on arguments to advice method [" +
				this.aspectJAdviceMethod + "]; pointcut expression [" +
				this.pointcut.getPointcutExpression() + "]", ex);
	} catch (InvocationTargetException ex) {
		throw ex.getTargetException();
	}
} catch (Throwable ex) {
	// 事务捕捉到异常后进行回滚操作
	completeTransactionAfterThrowing(txInfo, ex);
	throw ex;
} finally {
	cleanupTransactionInfo(txInfo);
}

        那么事务失效的原因也就找到了,那就是自定义 Around 的增强方法捕获了目标方法或者任何一个拦截器抛出的异常,但是却有没有把异常抛出来,所以事务捕捉不到这个异常了,自然无法进行回滚。

如何解决同时开启事务和AOP时事务失效的问题?

        我们已经知道了事务失效的原因,那么怎么解决的呢?

        方法之一就是从自定义 Around 增强方法入手,现在它把异常给私吞了,所以可以在它捕捉到异常之后执行 throw ex把异常继续向外抛出,就像 @AfterThrowing 一样,这样一来事务就可以捕捉到该异常了。或者就不要在这个通知方法里捕获异常了,爱咋咋滴,让外边的事务去处理吧。

        方法一可以解决问题,但是也会有一些不适用的情况,比如有些时候就是不想把异常抛出去,因为外边可能处理不了这个异常的话就会导致程序中断或者引起其他一些更为棘手的问题。

        更合适的方法应该是直接让事务拦截器直接尝试捕获目标方法的异常,即下面的执行流程

try {

	Object result = null;
    System.out.println("调用目标方法前 @Around ");
    try {
		//执行 @Before 的通知方法
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		try {
				try {
					//事务的 try catch 在最内层执行
					try {
						//执行目标方法
						return invokeJoinpoint();
					} catch (Throwable ex) {
						// 事务捕捉到异常后进行回滚操作
						completeTransactionAfterThrowing(txInfo, ex);
						throw ex;
					} finally {
						cleanupTransactionInfo(txInfo);
					}
				} catch (Throwable ex) {
					if (shouldInvokeOnThrowing(ex)) {
						//执行 @AfterThrowing 的通知方法
						invokeAdviceMethod(getJoinPointMatch(), null, ex);
					}
					throw ex;
				} 
				//执行 @AfterReturning 的通知方法
				this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		} finally {
			//执行 @After 的通知方法
			invokeAdviceMethod(getJoinPointMatch(), null, null);
		} 
	} catch (Throwable ex) {
        System.out.println("捕捉到异常信息:" + ex);
    }
    System.out.println("调用目标方法后 @Around ");
    //如果执行过程中都没有异常,则返回执行结果,就不会执行事务的回滚操作
    return result;
} catch (IllegalArgumentException ex) {
	throw new AopInvocationException("Mismatch on arguments to advice method [" +
			this.aspectJAdviceMethod + "]; pointcut expression [" +
			this.pointcut.getPointcutExpression() + "]", ex);
} catch (InvocationTargetException ex) {
	throw ex.getTargetException();
}

        这样以来,事务就能获取一手资料,不用担心别的拦截器吞了异常了。但是怎么让事务拦截器方法放在目标方法后执行呢,其实就是排序的时候把事务的 advisor 放到最后,这样的话 advisor 执行顺序就变成了 Around->Before->After->AfterReturning->AfterThrowing->事务advisor。那么拦截器的执行方法就会变成我们想要的样子。所以要实现这一点还是要在 advisor 排序那里做手脚。第一步排序其实已经无法改变了,所以只能借助第二步通过设置 order 改变顺序,但是因为默认的 order 都是 Integer.MAX_VALUE, 所以要想让事务方法排列到最后,只能选择减小 AOP advisor 的 order 值(数值越小优先级越高),而不能通过增大事务advisor。或者这两个都设置 order 值,保证事务的order值比较大就好。所以最简单的方法是在切面类上加上 @Order(1)就万事大吉了, 当然这个数字也可以不用 1,只要比 Integer.MAX_VALUE 小就行了。

总结

        本文讨论了事务对目标方法进行增强(也就是回滚)的流程,还分析了事务和 AOP 同时开启时可能会遇到的一些问题,之所以说可能也是因为毕竟 Around 增强逻辑是我们自己写的,所以只要不私吞异常是不会导致事务失效的。但是我们也给出了事务失效的解决方案,可以在使用的时候注意一下。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring4 中,我们可以通过以下步骤来配置事务: 1. 配置数据源:在 Spring 中,我们可以使用 JdbcTemplate 或者 HibernateTemplate 来访问数据库。我们需要在 Spring 的配置文件中配置数据源,这样 Spring 才能够知道我们使用的是哪个数据库。可以使用 Spring 内置的数据源,也可以使用第三方的数据源,如 C3P0、DBCP 等。 2. 配置事务管理器:在 Spring 中,我们可以通过配置事务管理器来管理事务Spring 支持多种事务管理器,如 DataSourceTransactionManager、HibernateTransactionManager、JtaTransactionManager 等。我们需要在 Spring 的配置文件中配置事务管理器,并指定其使用的数据源。 3. 配置事务属性:在 Spring 中,我们可以通过配置事务属性来指定事务的传播行为、隔离级别、超时时间、只读等特性。可以在 Spring 的配置文件中为每个事务方法指定事务属性,也可以在基于注解的事务中使用 @Transactional 注解来指定事务属性。 下面是一个简单的 Spring4 配置事务的示例: ```xml <!-- 配置数据源 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/test"></property> <property name="username" value="root"></property> <property name="password" value="123456"></property> </bean> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务属性 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT"/> <tx:method name="update*" propagation="REQUIRED" isolation="DEFAULT"/> <tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT"/> <tx:method name="*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice> <!-- 配置 AOP 通知 --> <aop:config> <aop:pointcut id="serviceMethod" expression="execution(* com.example.service.impl.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod"/> </aop:config> ``` 在上面的配置中,我们首先配置了一个数据源,然后配置了一个事务管理器,并将其关联到数据源上。接着,我们使用 tx:advice 元素配置了事务属性,其中指定了在执行 add、update、delete 方法时需要开启事务,并且传播行为为 REQUIRED,隔离级别为默认级别。对于其他方法,我们设置了只读事务。最后,我们使用 aop:config 元素将事务通知织入到 service 层的所有方法中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值