因为之前读过spring transaction相关源码《Transaction Management源码阅读路径》,所以对@Transactional和普通自定义切面执行顺序有一定的了解,本篇文章主要通过源码的角度解释下两个切面的执行顺序以及平时开放中需要注意的问题。
先说下为啥会考虑到这个,我们可以知道@Transaction一般加在具体要执行业务的service方法上,那如果我要进行并发控制对业务进行加锁,那么尝试锁和开启事务孰先孰后呢?按照业务流程上来看我们需要先尝试锁后开启事务,因为没获得锁开启事务需要和数据库进行交互开启一个新的事务,平常对业务结果是不会影响的,但是当高并发时是会对数据库带来不小压力。
但是平时的时候真的有注意和确定两者的顺序吗?下面我们来通过源码来看看。
先定义普通的切面
- package com.study.spring.aspect;
-
- import com.study.spring.annotation.SimpleAnnotation;
- import com.study.spring.service.impl.TestServiceImpl;
- import com.study.spring.entity.TestDo;
- import lombok.SneakyThrows;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.core.annotation.Order;
- import org.springframework.stereotype.Component;
-
- @Aspect
- @Slf4j
- @Component
- public class SimpleAspect {
- @Autowired
- private TestServiceImpl testService;
-
- @Around("@annotation(simpleAnnotation)")
- @SneakyThrows
- public Object simpleAround(ProceedingJoinPoint pjp, SimpleAnnotation simpleAnnotation) {
- log.info("进入simpleAround");
- Object proceed = pjp.proceed();
- log.info("完成simpleAround");
- return proceed;
- }
- }
-
再在service方法中加上@SimpleAnnotation和@SimpleAnnotation
- @Override
- @SimpleAnnotation
- @Transactional(rollbackFor = Exception.class)
- public void test() {
- log.info("执行业务");
- TestDo testUpdate = new TestDo();
- testUpdate.setId(1);
- testUpdate.setName("执行业");
- testService.updateById(testUpdate);
- }
-
下面我们直接来看看生成代理时两个切面的顺序。
有两个关键点我们需要注意:
下面看下执行顺序:
会先执行事务切面
后面执行自定义切面。
如果普通切面没指定order会比transaction后执行。当锁或者一些检查性切面被使用时如果条件不满足不能进入业务也会导致事务的开启产生了不必要的消耗,当并发高时尤为明显。
如果是synchronized等阻塞性锁还会导致提前创建事务因为mvcc会导致读旧值的情况,并发时会出现问题。
那么我们怎么避免此类影响呢?
其实避免方式有三种,一种是指定order,一种是把自定义切面移到更外层中,一种是使用编程式事务。
- package com.study.spring.aspect;
-
- import com.study.spring.annotation.SimpleAnnotation;
- import com.study.spring.service.impl.TestServiceImpl;
- import lombok.SneakyThrows;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.core.annotation.Order;
- import org.springframework.stereotype.Component;
- @Aspect
- @Slf4j
- @Component
- @Order(1)
- public class SimpleAspect {
- @Autowired
- private TestServiceImpl testService;
-
- @Around("@annotation(simpleAnnotation)")
- @SneakyThrows
- public Object simpleAround(ProceedingJoinPoint pjp, SimpleAnnotation simpleAnnotation) {
- log.info("进入simpleAround");
- Object proceed = pjp.proceed();
- log.info("完成simpleAround");
- return proceed;
- }
- }
-
这里我们指定了@Order(1),下面看看APC中的advisors中的顺序。
再看看实际执行顺序
可以看到自定义的切面先执行了。
移到更外层中就不用证明了,调用的自然顺序,比如放在Controller的方法上。
当然可以,调用的自然顺序,事务的开启更加现式。
因为声明式事务比较好用,生产中使用的比较多,只有为了控制事务粒度或者不需要抽出一个新的类(为了使事务生效)才会使用编程式事务。
所以更加倾向于移到更外层,因为指定order的前提是你知道事务切面的和不指定order普通切面的顺序,同时一旦切面变多比如有统一加锁切面、统一检查是否认证切面等需要控制自定义切面顺序容易和事务切面搞混,不利于维护,这个也相当于自定义切面和框架前面隔离。这也从一个侧面证明了校验放controller的合理性。