当前位置: 首页 > news >正文

从 “坑“ 到 “通“: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 版本前顺序总结

  1. 环绕通知的前置部分最早执行
  2. 前置通知按照 @Order 注解的顺序(从小到大)执行
  3. 目标方法执行
  4. 环绕通知的后置部分接着执行
  5. 后置通知按照 @Order 注解的逆序(从大到小)执行
  6. 最后执行返回通知或异常通知(也按照 @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 版本后顺序总结

  1. 环绕通知的前置部分和前置通知的执行顺序与旧版本一致(按 @Order 从小到大)
  2. 目标方法执行后,返回通知或异常通知先执行(按 @Order 从大到小)
  3. 然后执行后置通知(按 @Order 从大到小)
  4. 最后执行环绕通知的后置部分(按 @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 通知顺序变化导致的问题,可按以下步骤操作:

  1. 全面梳理现有切面:列出项目中所有的切面类,特别是那些依赖通知执行顺序的切面。

  2. 识别关键顺序依赖:找出那些 @After、@AfterReturning、@AfterThrowing 和 @Around(后置部分)中存在逻辑依赖的代码。

  3. 调整通知类型或顺序

    • 对于依赖 @After 在 @AfterReturning 之前执行的逻辑,可将 @After 中的代码迁移到 @AfterReturning 中
    • 对于依赖 @Around 后置部分在 @After 之前执行的逻辑,可调整代码顺序或拆分切面
  4. 增加集成测试:为关键业务流程编写集成测试,验证 AOP 通知执行顺序是否符合预期。

  5. 逐步升级验证:先在测试环境升级,全面验证通过后再部署到生产环境。

五、环绕通知的特殊处理

环绕通知(@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 环绕通知的最佳实践

  1. 始终调用 proceed () 方法:除非你明确要阻止目标方法执行,否则一定要调用 joinPoint.proceed (),否则目标方法和其他通知都不会执行。

  2. 正确处理异常:环绕通知中需要正确处理异常,要么捕获处理,要么重新抛出,避免异常被吞噬。

  3. 避免过度使用:虽然环绕通知功能强大,但也容易导致代码复杂,简单场景优先使用其他通知类型。

  4. 版本兼容考虑:如果项目可能跨版本运行,在环绕通知的后置部分避免依赖其他通知的执行结果。

六、实战中的常见问题与解决方案

6.1 通知执行顺序不一致

问题:在不同环境或不同版本中,通知执行顺序不一致。

解决方案

  1. 显式指定 @Order 注解:确保所有切面都添加了 @Order 注解,明确指定执行顺序。
@Aspect
@Component
@Order(1) // 明确指定顺序
public class FirstAspect { ... }@Aspect
@Component
@Order(2) // 明确指定顺序
public class SecondAspect { ... }
  1. 避免依赖默认顺序:不要依赖类名或加载顺序来决定切面执行顺序,这在不同环境中可能不同。

  2. 版本适配:如果需要兼容多个 Spring 版本,尽量避免在 @After、@AfterReturning 和 @Around(后置部分)中编写有顺序依赖的逻辑。

6.2 @AfterReturning 不执行

问题:目标方法正常执行,但 @AfterReturning 通知不执行。

可能原因及解决方案

  1. 目标方法被环绕通知拦截且未调用 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()
}
  1. 切入点表达式不匹配:检查 @AfterReturning 的 pointcut 表达式是否正确匹配目标方法。

  2. 目标方法抛出了异常:@AfterReturning 仅在目标方法正常返回时执行,如果抛出异常,应使用 @AfterThrowing。

6.3 多个 @AfterThrowing 通知的执行顺序

问题:多个切面的 @AfterThrowing 通知执行顺序不符合预期。

解决方案

  1. 明确指定 @Order 注解,@AfterThrowing 的执行顺序遵循 @Order 的逆序(5.2.7 版本后)。

  2. 避免在多个切面中处理同一类型的异常,这可能导致逻辑混乱。

  3. 对于异常处理,优先使用环绕通知,它可以更灵活地控制异常处理逻辑。

6.4 自调用导致通知不执行

问题:在同一个类中,一个方法调用另一个方法时,被调用方法的通知不执行。

解决方案

  1. 通过 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 { ... }
  1. 拆分服务类:将方法拆分到不同的服务类中,避免自调用。

  2. 使用 AspectJ 的编译期织入:AspectJ 的字节码织入可以解决自调用问题,但配置相对复杂。

七、最佳实践与总结

7.1 通知使用最佳实践

  1. 按功能划分切面:一个切面专注于一个功能(如日志、事务、权限),避免大而全的切面。

  2. 合理选择通知类型

    • 日志记录:优先使用 @Before 和 @AfterReturning/@AfterThrowing
    • 性能监控:优先使用 @Around,可以记录方法执行时间
    • 资源释放:优先使用 @After,确保无论是否异常都会执行
    • 事务管理:必须使用 @Around 或 @Before+@AfterReturning/@AfterThrowing 组合
  3. 明确指定 @Order:只要有多个切面,就必须显式指定 @Order,避免依赖默认顺序。

  4. 避免通知之间的依赖:尽量让每个通知独立工作,不依赖其他通知的执行结果。

  5. 谨慎使用环绕通知:只有在需要控制目标方法执行时机时才使用,否则优先使用其他通知类型。

7.2 版本选择建议

  1. 新项目:直接使用最新稳定版本(如 Spring 6.x),遵循新版本的执行顺序。

  2. 旧项目升级

    • 如无特殊需求,可保持现有版本,避免升级带来的风险
    • 如需升级,务必全面测试 AOP 相关逻辑
    • 可考虑分阶段升级,先升级到 5.2.7 版本,适应新的执行顺序后再升级到更高版本
  3. 跨版本组件:如果开发的是供多个项目使用的组件,需要兼容不同 Spring 版本,应避免在通知中编写依赖特定执行顺序的逻辑。

7.3 核心知识点总结

  1. Spring AOP 有 5 种通知类型:@Before、@After、@AfterReturning、@AfterThrowing 和 @Around。

  2. Spring 5.2.7 版本是通知执行顺序的分水岭,主要变化是 @After、@AfterReturning/@AfterThrowing 和 @Around(后置部分)的相对顺序。

  3. 5.2.7 版本前:@Around(后置)→ @After → @AfterReturning/@AfterThrowing

  4. 5.2.7 版本后:@AfterReturning/@AfterThrowing → @After → @Around(后置)

  5. 多个切面的执行顺序由 @Order 注解控制,数值越小越先执行,且遵循 "先入后出" 原则。

  6. 环绕通知功能最强大,但也最复杂,使用时需特别注意调用 proceed () 方法和异常处理。

掌握 Spring AOP 通知的执行顺序,不仅能帮助你写出更可靠的切面代码,还能在遇到问题时快速定位原因。无论是旧版本的项目维护,还是新版本的项目开发,理解这些核心原理都至关重要。记住,AOP 是一把双刃剑,只有正确使用才能发挥其威力,否则可能带来难以调试的问题。

http://www.dtcms.com/a/485915.html

相关文章:

  • 博途SCL语言仿真七段数码管
  • 关于网站建设的介绍本地搭建wordpress建站教程
  • 免费网站收录网站推广苏州网站建设推荐q479185700霸屏
  • 【LeetCode热题100(43/100)】验证二叉搜索树
  • 养殖场疫病预警新方案:小吉快检BL-08plus现场快速锁定病原
  • 【ADS-1】【python基础-3】函数封装与面向对象
  • 攻防世界-Web-baby_web
  • SQLite数据库基本操作
  • git创建分支,以及如何管理
  • Netty线程模型与Tomcat线程模型对比分析
  • STEMlab 125-14 Gen 2
  • 如何租用网站服务器寿光营销型网站建设
  • 云手机玩游戏卡顿的原因都有哪些
  • Python Web框架对比与模型部署
  • C# 实现高保真 Excel 转 PDF(无需 Office 环境)
  • cycloneV nios 华邦flash程序固化方案
  • FreeBSD-14.3基本安装过程
  • 细说Docker命令
  • 大型门户网站建设效果好吗重庆网站建设公司怎么做
  • 【Web】LilCTF2025 WP(随便看看
  • Vue3+Ts+Element Plus 权限菜单控制节点
  • PP-OCRv5 MCP服务器在海光主板的部署与实战
  • Linux 服务器NFS文件共享
  • 吃透大数据算法-算法地图(备用)
  • 前端性能优化实战:从指标到落地的全流程指南
  • 120html
  • 四川建设人才官方网站制作app需要学哪些东西专业知识
  • 二叉搜索树 --- 概念 + 模拟
  • 系统安全-主流密码加密算法BCrypt 和PBKDF2详解
  • 【具身智能】具身机器人VLA算法入门及实战(一):具身智能系统及VLA