从 “坑“ 到 “通“:Spring AOP 通知执行顺序深度解密
前言:为什么你必须搞懂 AOP 通知通知执行顺序?
在 Spring 开发中,AOP(面向切面编程)是实现代码解耦的利器,日志记录、事务管理、权限控制等横切逻辑都离不开它。但你是否遇到过这些困惑:
- 同样的 @Before 和 @After 注解,在不同项目中执行顺序居然不一样?
- 升级 Spring 版本后,切面逻辑突然出错,排查半天发现是通知执行顺序变了?
- 方法抛出异常时,为什么有的 @AfterReturning 通知不执行了?
这些问题的根源,都指向一个核心知识点:Spring AOP 通知的执行顺序。更关键的是,Spring 在 5.2.7 版本对通知执行顺序做了重大调整,这导致很多开发者在升级后遭遇了 "灵异现象"。
本文将从底层原理到实战案例,全方位剖析 Spring AOP 通知的执行顺序,特别是 5.2.7 版本前后的差异,帮你彻底掌握这一核心知识点,避免在实际开发中踩坑。
一、Spring AOP 通知类型全解析
在探讨执行顺序之前,我们先明确 Spring AOP 支持的 5 种通知类型,这是理解后续内容的基础。
1.1 5 种通知类型及作用
- @Before(前置通知):在目标方法执行前执行
- @After(后置通知):在目标方法执行后执行,无论是否发生异常
- @AfterReturning(返回通知):在目标方法正常返回后执行
- @AfterThrowing(异常通知):在目标方法抛出异常后执行
- @Around(环绕通知):环绕目标方法执行,可控制目标方法的执行时机,功能最强大
1.2 通知类型的核心区别
通知类型 | 执行时机 | 能否阻止目标方法执行 | 能否获取返回值 | 能否获取异常 |
---|---|---|---|---|
@Before | 目标方法执行前 | 不能(除非抛出异常) | 不能 | 不能 |
@After | 目标方法执行后 | 不能 | 不能 | 不能 |
@AfterReturning | 目标方法正常返回后 | 不能 | 能 | 不能 |
@AfterThrowing | 目标方法抛出异常后 | 不能 | 不能 | 能 |
@Around | 可自定义执行时机 | 能(不调用 proceed ()) | 能 | 能 |
二、Spring 5.2.7 版本前的通知执行顺序
在 Spring 5.2.7 版本之前,通知的执行顺序有其特定的规则,尤其是多个切面作用于同一个目标方法时,顺序更为复杂。
2.1 单个切面内的通知执行顺序
当一个切面中定义了多种通知类型,作用于同一个目标方法时,其执行顺序如下:
正常执行(无异常)情况
异常执行情况
2.2 多个切面的通知执行顺序
当多个切面作用于同一个目标方法时,Spring 采用 "同心圆" 模型:
- 外层切面的 @Before 先执行,内层切面的 @Before 后执行(类似剥洋葱,从外到内)
- 目标方法执行后,内层切面的 @After 先执行,外层切面的 @After 后执行(从内到外)
2.3 5.2.7 版本前的代码示例
我们通过一个完整示例来验证上述执行顺序。
1. 项目依赖(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>spring-aop-order-demo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><spring.version>5.2.6.RELEASE</spring.version> <!-- 5.2.7之前的版本 --></properties><dependencies><!-- Spring Context --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version></dependency><!-- Spring AOP --><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>${spring.version}</version></dependency><!-- AspectJ Weaver --><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.6</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency></dependencies>
</project>
2. 业务服务类
/*** 订单服务类* @author ken*/
@Service
@Slf4j
public class OrderService {/*** 创建订单* @param orderId 订单ID* @return 订单创建结果*/public String createOrder(String orderId) {log.info("执行订单创建逻辑,订单ID:{}", orderId);// 模拟正常执行return "订单创建成功:" + orderId;// 模拟异常情况// throw new RuntimeException("订单创建失败:库存不足");}
}
3. 外层切面
/*** 外层切面(日志切面)* @author ken*/
@Aspect
@Component
@Order(1) // 数值越小,优先级越高,越先执行
@Slf4j
public class LogAspect {/*** 定义切入点*/@Pointcut("execution(* com.example.service.OrderService.createOrder(..))")public void orderPointcut() {}/*** 前置通知*/@Before("orderPointcut()")public void before() {log.info("LogAspect - @Before 前置通知执行");}/*** 后置通知*/@After("orderPointcut()")public void after() {log.info("LogAspect - @After 后置通知执行");}/*** 返回通知*/@AfterReturning(pointcut = "orderPointcut()", returning = "result")public void afterReturning(Object result) {log.info("LogAspect - @AfterReturning 返回通知执行,返回结果:{}", result);}/*** 异常通知*/@AfterThrowing(pointcut = "orderPointcut()", throwing = "ex")public void afterThrowing(Exception ex) {log.info("LogAspect - @AfterThrowing 异常通知执行,异常信息:{}", ex.getMessage());}/*** 环绕通知*/@Around("orderPointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("LogAspect - @Around 环绕通知开始");Object result = joinPoint.proceed(); // 执行目标方法log.info("LogAspect - @Around 环绕通知结束");return result;}
}
4. 内层切面
/*** 内层切面(性能监控切面)* @author ken*/
@Aspect
@Component
@Order(2) // 优先级低于LogAspect
@Slf4j
public class PerformanceAspect {/*** 定义切入点(与LogAspect相同)*/@Pointcut("execution(* com.example.service.OrderService.createOrder(..))")public void orderPointcut() {}/*** 前置通知*/@Before("orderPointcut()")public void before() {log.info("PerformanceAspect - @Before 前置通知执行");}/*** 后置通知*/@After("orderPointcut()")public void after() {log.info("PerformanceAspect - @After 后置通知执行");}/*** 返回通知*/@AfterReturning(pointcut = "orderPointcut()", returning = "result")public void afterReturning(Object result) {log.info("PerformanceAspect - @AfterReturning 返回通知执行,返回结果:{}", result);}/*** 异常通知*/@AfterThrowing(pointcut = "orderPointcut()", throwing = "ex")public void afterThrowing(Exception ex) {log.info("PerformanceAspect - @AfterThrowing 异常通知执行,异常信息:{}", ex.getMessage());}/*** 环绕通知*/@Around("orderPointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("PerformanceAspect - @Around 环绕通知开始");Object result = joinPoint.proceed(); // 执行目标方法log.info("PerformanceAspect - @Around 环绕通知结束");return result;}
}
5. Spring 配置类
/*** Spring配置类* @author ken*/
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}
6. 测试类
/*** AOP通知顺序测试类* @author ken*/
@Slf4j
public class AopOrderTest {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);OrderService orderService = context.getBean(OrderService.class);log.info("====== 开始执行订单创建 ======");orderService.createOrder("ORDER_001");log.info("====== 订单创建执行完毕 ======");}
}
7. 执行结果(正常情况)
====== 开始执行订单创建 ======
LogAspect - @Around 环绕通知开始
LogAspect - @Before 前置通知执行
PerformanceAspect - @Around 环绕通知开始
PerformanceAspect - @Before 前置通知执行
执行订单创建逻辑,订单ID:ORDER_001
PerformanceAspect - @Around 环绕通知结束
PerformanceAspect - @After 后置通知执行
PerformanceAspect - @AfterReturning 返回通知执行,返回结果:订单创建成功:ORDER_001
LogAspect - @Around 环绕通知结束
LogAspect - @After 后置通知执行
LogAspect - @AfterReturning 返回通知执行,返回结果:订单创建成功:ORDER_001
====== 订单创建执行完毕 ======
8. 执行结果(异常情况)
将 OrderService 中的代码改为抛出异常:
public String createOrder(String orderId) {log.info("执行订单创建逻辑,订单ID:{}", orderId);// 模拟异常情况throw new RuntimeException("订单创建失败:库存不足");
}
执行结果:
====== 开始执行订单创建 ======
LogAspect - @Around 环绕通知开始
LogAspect - @Before 前置通知执行
PerformanceAspect - @Around 环绕通知开始
PerformanceAspect - @Before 前置通知执行
执行订单创建逻辑,订单ID:ORDER_001
PerformanceAspect - @After 后置通知执行
PerformanceAspect - @AfterThrowing 异常通知执行,异常信息:订单创建失败:库存不足
LogAspect - @After 后置通知执行
LogAspect - @AfterThrowing 异常通知执行,异常信息:订单创建失败:库存不足
Exception in thread "main" java.lang.RuntimeException: 订单创建失败:库存不足...
2.4 5.2.7 版本前顺序总结
- 环绕通知的前置部分最早执行
- 前置通知按照 @Order 注解的顺序(从小到大)执行
- 目标方法执行
- 环绕通知的后置部分接着执行
- 后置通知按照 @Order 注解的逆序(从大到小)执行
- 最后执行返回通知或异常通知(也按照 @Order 逆序)
三、Spring 5.2.7 版本后的通知执行顺序
Spring 在 5.2.7 版本(包含该版本)对 AOP 通知执行顺序进行了调整,主要是为了让通知执行顺序更符合直觉,并与 AspectJ 保持一致。
3.1 版本调整的背景与原因
在 Spring 官方文档中,这次调整的原因被描述为:
"调整 AOP 通知的执行顺序,使其与 AspectJ 的行为保持一致,同时修复了多个切面情况下 @After 通知执行顺序不符合预期的问题。"
简单来说,旧版本的执行顺序在多个切面时,@After 通知的执行顺序容易让人混淆,调整后的顺序更符合 "先入后出" 的原则。
3.2 单个切面内的通知执行顺序
单个切面内的通知执行顺序在版本调整前后变化不大:
正常执行情况
异常执行情况
3.3 多个切面的通知执行顺序
多个切面的执行顺序变化较大,调整后的顺序如下:
3.4 5.2.7 版本后的代码示例
我们只需修改 pom.xml 中的 Spring 版本:
<properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><spring.version>5.2.7.RELEASE</spring.version> <!-- 5.2.7及之后的版本 -->
</properties>
其他代码保持不变,执行测试类。
1. 执行结果(正常情况)
====== 开始执行订单创建 ======
LogAspect - @Around 环绕通知开始
LogAspect - @Before 前置通知执行
PerformanceAspect - @Around 环绕通知开始
PerformanceAspect - @Before 前置通知执行
执行订单创建逻辑,订单ID:ORDER_001
PerformanceAspect - @AfterReturning 返回通知执行,返回结果:订单创建成功:ORDER_001
PerformanceAspect - @After 后置通知执行
PerformanceAspect - @Around 环绕通知结束
LogAspect - @AfterReturning 返回通知执行,返回结果:订单创建成功:ORDER_001
LogAspect - @After 后置通知执行
LogAspect - @Around 环绕通知结束
====== 订单创建执行完毕 ======
2. 执行结果(异常情况)
====== 开始执行订单创建 ======
LogAspect - @Around 环绕通知开始
LogAspect - @Before 前置通知执行
PerformanceAspect - @Around 环绕通知开始
PerformanceAspect - @Before 前置通知执行
执行订单创建逻辑,订单ID:ORDER_001
PerformanceAspect - @AfterThrowing 异常通知执行,异常信息:订单创建失败:库存不足
PerformanceAspect - @After 后置通知执行
LogAspect - @AfterThrowing 异常通知执行,异常信息:订单创建失败:库存不足
LogAspect - @After 后置通知执行
Exception in thread "main" java.lang.RuntimeException: 订单创建失败:库存不足...
3.5 5.2.7 版本后顺序总结
- 环绕通知的前置部分和前置通知的执行顺序与旧版本一致(按 @Order 从小到大)
- 目标方法执行后,返回通知或异常通知先执行(按 @Order 从大到小)
- 然后执行后置通知(按 @Order 从大到小)
- 最后执行环绕通知的后置部分(按 @Order 从大到小)
简单来说,调整后的顺序让 @AfterReturning、@AfterThrowing 和 @After 在每个切面内部先执行完毕,再执行外层切面的对应通知,更符合直觉。
四、版本差异对比与迁移指南
4.1 核心差异点对比
通知类型 | 5.2.7 版本前执行顺序 | 5.2.7 版本后执行顺序 |
---|---|---|
@Around(后置部分) | 在 @After 和 @AfterReturning/@AfterThrowing 之前 | 在 @After 和 @AfterReturning/@AfterThrowing 之后 |
@After | 在 @AfterReturning/@AfterThrowing 之前 | 在 @AfterReturning/@AfterThrowing 之后 |
多个切面的 @After | 按 @Order 逆序执行,但在 @Around 后置部分之前 | 按 @Order 逆序执行,在 @AfterReturning/@AfterThrowing 之后,@Around 后置部分之前 |
4.2 可视化对比
4.3 升级 Spring 版本的迁移指南
如果你的项目需要从 5.2.7 之前的版本升级到之后的版本,为避免 AOP 通知顺序变化导致的问题,可按以下步骤操作:
-
全面梳理现有切面:列出项目中所有的切面类,特别是那些依赖通知执行顺序的切面。
-
识别关键顺序依赖:找出那些 @After、@AfterReturning、@AfterThrowing 和 @Around(后置部分)中存在逻辑依赖的代码。
-
调整通知类型或顺序:
- 对于依赖 @After 在 @AfterReturning 之前执行的逻辑,可将 @After 中的代码迁移到 @AfterReturning 中
- 对于依赖 @Around 后置部分在 @After 之前执行的逻辑,可调整代码顺序或拆分切面
-
增加集成测试:为关键业务流程编写集成测试,验证 AOP 通知执行顺序是否符合预期。
-
逐步升级验证:先在测试环境升级,全面验证通过后再部署到生产环境。
五、环绕通知的特殊处理
环绕通知(@Around)是功能最强大的通知类型,它可以控制目标方法的执行时机,因此其执行顺序需要特别关注。
5.1 环绕通知的内部结构
一个完整的环绕通知通常包含三个部分:
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {// 1. 前置部分:在目标方法执行前执行log.info("环绕通知 - 前置部分");Object result;try {// 执行目标方法result = joinPoint.proceed();// 2. 后置部分:在目标方法正常返回后执行log.info("环绕通知 - 后置部分");} catch (Exception e) {// 3. 异常部分:在目标方法抛出异常后执行log.info("环绕通知 - 异常部分");throw e;}return result;
}
5.2 环绕通知与其他通知的交互
在 5.2.7 版本前后,环绕通知与其他通知的交互关系发生了变化:
5.2.7 版本前
5.2.7 版本后
这种变化意味着,在新版本中,其他通知的执行结果可以影响环绕通知的后置处理逻辑,这在事务管理等场景中非常有用。
5.3 环绕通知的最佳实践
-
始终调用 proceed () 方法:除非你明确要阻止目标方法执行,否则一定要调用 joinPoint.proceed (),否则目标方法和其他通知都不会执行。
-
正确处理异常:环绕通知中需要正确处理异常,要么捕获处理,要么重新抛出,避免异常被吞噬。
-
避免过度使用:虽然环绕通知功能强大,但也容易导致代码复杂,简单场景优先使用其他通知类型。
-
版本兼容考虑:如果项目可能跨版本运行,在环绕通知的后置部分避免依赖其他通知的执行结果。
六、实战中的常见问题与解决方案
6.1 通知执行顺序不一致
问题:在不同环境或不同版本中,通知执行顺序不一致。
解决方案:
- 显式指定 @Order 注解:确保所有切面都添加了 @Order 注解,明确指定执行顺序。
@Aspect
@Component
@Order(1) // 明确指定顺序
public class FirstAspect { ... }@Aspect
@Component
@Order(2) // 明确指定顺序
public class SecondAspect { ... }
-
避免依赖默认顺序:不要依赖类名或加载顺序来决定切面执行顺序,这在不同环境中可能不同。
-
版本适配:如果需要兼容多个 Spring 版本,尽量避免在 @After、@AfterReturning 和 @Around(后置部分)中编写有顺序依赖的逻辑。
6.2 @AfterReturning 不执行
问题:目标方法正常执行,但 @AfterReturning 通知不执行。
可能原因及解决方案:
- 目标方法被环绕通知拦截且未调用 proceed ():
// 错误示例
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) {log.info("环绕通知执行");// 忘记调用proceed(),目标方法和@AfterReturning都不会执行return null;
}// 正确示例
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("环绕通知执行");return joinPoint.proceed(); // 必须调用proceed()
}
-
切入点表达式不匹配:检查 @AfterReturning 的 pointcut 表达式是否正确匹配目标方法。
-
目标方法抛出了异常:@AfterReturning 仅在目标方法正常返回时执行,如果抛出异常,应使用 @AfterThrowing。
6.3 多个 @AfterThrowing 通知的执行顺序
问题:多个切面的 @AfterThrowing 通知执行顺序不符合预期。
解决方案:
-
明确指定 @Order 注解,@AfterThrowing 的执行顺序遵循 @Order 的逆序(5.2.7 版本后)。
-
避免在多个切面中处理同一类型的异常,这可能导致逻辑混乱。
-
对于异常处理,优先使用环绕通知,它可以更灵活地控制异常处理逻辑。
6.4 自调用导致通知不执行
问题:在同一个类中,一个方法调用另一个方法时,被调用方法的通知不执行。
解决方案:
- 通过 AopContext 获取代理对象:
@Service
public class OrderService {public void methodA() {// 错误方式:直接调用,通知不执行// methodB();// 正确方式:通过AopContext获取代理对象调用OrderService proxy = (OrderService) AopContext.currentProxy();proxy.methodB();}public void methodB() {// ...}
}
需要在 @EnableAspectJAutoProxy 中设置 exposeProxy = true:
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig { ... }
-
拆分服务类:将方法拆分到不同的服务类中,避免自调用。
-
使用 AspectJ 的编译期织入:AspectJ 的字节码织入可以解决自调用问题,但配置相对复杂。
七、最佳实践与总结
7.1 通知使用最佳实践
-
按功能划分切面:一个切面专注于一个功能(如日志、事务、权限),避免大而全的切面。
-
合理选择通知类型:
- 日志记录:优先使用 @Before 和 @AfterReturning/@AfterThrowing
- 性能监控:优先使用 @Around,可以记录方法执行时间
- 资源释放:优先使用 @After,确保无论是否异常都会执行
- 事务管理:必须使用 @Around 或 @Before+@AfterReturning/@AfterThrowing 组合
-
明确指定 @Order:只要有多个切面,就必须显式指定 @Order,避免依赖默认顺序。
-
避免通知之间的依赖:尽量让每个通知独立工作,不依赖其他通知的执行结果。
-
谨慎使用环绕通知:只有在需要控制目标方法执行时机时才使用,否则优先使用其他通知类型。
7.2 版本选择建议
-
新项目:直接使用最新稳定版本(如 Spring 6.x),遵循新版本的执行顺序。
-
旧项目升级:
- 如无特殊需求,可保持现有版本,避免升级带来的风险
- 如需升级,务必全面测试 AOP 相关逻辑
- 可考虑分阶段升级,先升级到 5.2.7 版本,适应新的执行顺序后再升级到更高版本
-
跨版本组件:如果开发的是供多个项目使用的组件,需要兼容不同 Spring 版本,应避免在通知中编写依赖特定执行顺序的逻辑。
7.3 核心知识点总结
-
Spring AOP 有 5 种通知类型:@Before、@After、@AfterReturning、@AfterThrowing 和 @Around。
-
Spring 5.2.7 版本是通知执行顺序的分水岭,主要变化是 @After、@AfterReturning/@AfterThrowing 和 @Around(后置部分)的相对顺序。
-
5.2.7 版本前:@Around(后置)→ @After → @AfterReturning/@AfterThrowing
-
5.2.7 版本后:@AfterReturning/@AfterThrowing → @After → @Around(后置)
-
多个切面的执行顺序由 @Order 注解控制,数值越小越先执行,且遵循 "先入后出" 原则。
-
环绕通知功能最强大,但也最复杂,使用时需特别注意调用 proceed () 方法和异常处理。
掌握 Spring AOP 通知的执行顺序,不仅能帮助你写出更可靠的切面代码,还能在遇到问题时快速定位原因。无论是旧版本的项目维护,还是新版本的项目开发,理解这些核心原理都至关重要。记住,AOP 是一把双刃剑,只有正确使用才能发挥其威力,否则可能带来难以调试的问题。