当多个切面同时作用于同一个连接点时,Spring AOP 会将它们组织成一个有序的通知调用链。开发者需要理解这个调用链的构建规则和执行流程,才能避免因顺序混乱导致的隐形 Bug,并合理利用顺序实现业务需求(如先执行权限校验再开启事务)。
5.4.1 单一切面的通知执行顺序
在一个切面类内部,不同类型的通知会按照固定的生命周期顺序执行。假设有如下切面:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("Before");
}
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice() {
System.out.println("After");
}
@AfterReturning("execution(* com.example.service.*.*(..))")
public void afterReturningAdvice() {
System.out.println("AfterReturning");
}
@AfterThrowing("execution(* com.example.service.*.*(..))")
public void afterThrowingAdvice() {
System.out.println("AfterThrowing");
}
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around before");
Object result = joinPoint.proceed();
System.out.println("Around after");
return result;
}
}
当被拦截的服务方法正常执行(无异常)时,输出顺序为:
Around before
Before
(目标方法执行)
AfterReturning
After
Around after
若目标方法抛出异常,则顺序变为:
Around before
Before
(目标方法抛出异常)
AfterThrowing
After
(异常向上传播,Around 不再执行 after 部分)
可以看到规则:
- 环绕通知包裹在最外层,
proceed()之前的部分最先执行,之后的部分最后执行。 - 前置通知在进入目标方法之前执行。
- 返回通知(或异常通知,二者互斥)在目标方法执行后触发。
- 后置通知
@After总是执行,行为类似finally块。
5.4.2 多个切面的执行顺序
当多个切面作用于同一个目标方法时,它们的执行顺序需要通过 切面优先级 来控制。Spring 遵循以下规则:
- 默认顺序:多个切面之间没有明确的先后关系,执行顺序取决于切面类的全限定名或 Spring 容器加载顺序,这是不可靠的,不应依赖。
- 显式指定顺序:让切面类实现
org.springframework.core.Ordered接口,或使用@Order注解指定整数值。数值越小,优先级越高,前置通知越先执行,但返回/后置通知越后执行(类似同心圆结构)。
例如:
@Order(1)
@Aspect
@Component
public class SecurityAspect {
@Before("...")
public void checkAuth() {
System.out.println("Security check");
}
}
@Order(2)
@Aspect
@Component
public class TransactionAspect {
@Around("...")
public Object manageTx(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Transaction begin");
Object result = joinPoint.proceed();
System.out.println("Transaction commit");
return result;
}
}
当它们同时拦截一个服务方法时,执行顺序为:
Security check ← 优先级高(1)的前置通知先执行
Transaction begin ← 优先级低(2)的环绕通知前逻辑
(目标方法执行)
Transaction commit ← 低优先级后逻辑
(Security 无后置逻辑,但如果有 @After 或 @AfterReturning,会在此之后执行)
规律总结:多个切面的执行模型可以理解为同心圆嵌套——优先级最高的切面在最外层,优先级最低的在最内层。前置通知按优先级从高到低(小→大)执行,后置/返回/环绕的后半部分按优先级从低到高(大→小)执行。
5.4.3 实际场景:控制事务与锁的顺序
在真实项目中,通常会为同一连接点配置事务切面和自定义切面(如分布式锁、权限校验)。如果顺序不对,可能出现“先提交事务再释放锁”导致数据不一致,或者“先校验权限再开启事务”以避免事务占用时间过长。
以事务先执行,锁后执行为例(保证方法内先获取锁,事务最后提交):
@Order(1) // 外层:事务
@Aspect
@Component
public class TransactionalAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object handleTx(ProceedingJoinPoint joinPoint) throws Throwable {
// 事务开始逻辑(实际 Spring 事务更复杂,这里仅示意)
System.out.println("TX begin");
Object result = joinPoint.proceed();
System.out.println("TX commit");
return result;
}
}
@Order(2) // 内层:锁
@Aspect
@Component
public class LockAspect {
@Around("@annotation(com.example.Locked)")
public Object handleLock(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Lock acquired");
Object result = joinPoint.proceed();
System.out.println("Lock released");
return result;
}
}
执行时序:TX begin → Lock acquired → 业务方法 → Lock released → TX commit。事务获取在最外层,确保锁在事务内释放,避免并发问题。
5.4.4 环绕通知与连接点的传递
在调用链中,每个环绕通知的 ProceedingJoinPoint 对象的 proceed() 方法实际上是调用链中的下一个节点。如果某个环绕通知忘记调用 proceed(),整个链就会中断,目标方法不会被执行,后续通知也不会触发(除非有意而为,比如权限校验失败主动阻止调用)。
因此,使用环绕通知时必须谨慎处理异常:
@Around("...")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
try {
// 前置增强
Object result = joinPoint.proceed();
// 返回增强
return result;
} catch (Throwable e) {
// 异常增强
throw new RuntimeException(e);
} finally {
// 类似 @After 的逻辑
}
}
这里的 catch 块若吞掉了异常而不重新抛出,则后续的异常通知和事务回滚可能被跳过,需要特别注意。
5.4.5 通过日志验证执行顺序
为了在实际开发中验证切面的执行顺序,可以临时为多个切面添加日志,利用线程 ID 和时间戳观察输出。一个更系统的做法是引入 org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator 相关的 TRACE 日志,或利用 AOP 拦截器链的调试工具。
但更简单的做法永远是编写一个集成测试,固定 @Order 后断言日志输出的先后次序,将顺序规则固化为测试用例,防止未来重构时误调优先级。
5.4.6 小结
掌握通知调用链与执行顺序需重点记住:
- 单切面内:
@Around包裹全部,@Before最先,然后是目标方法,再是@AfterReturning/@AfterThrowing,最后是@After(类似 finally)。 - 多切面间:通过
@Order或Ordered接口决定嵌套层级,小数值在外层,大数值在内层。 - 实际应用:按业务需求精心编排切面顺序,并用测试锁定该约定。
理解了这套调用链机制,当排查“为什么事务没有回滚”或“为什么权限检查跑到日志打印之后”这类问题时,就能有清晰的排查方向——检查 @Order 的配置,跟踪环绕通知中的 proceed() 调用点,一切都会变得直观明确。