你真的确定Spring AOP的执行顺序吗

本文内容重点:

  • 问题描述

  • Spring AOP执行顺序

  • 探究顺序错误的真相

  • 代码验证

  • 结论

 

问题描述

公司新项目需要搭建一个新的前后分离HTTP服务,我选择了目前比较熟悉的SpringBoot Web来快速搭建一个可用的系统。

鲁迅说过,不要随便升级已经稳定使用的版本。我偏不信这个邪,仗着自己用了这么久Spring,怎么能不冲呢。不说了,直接引入了最新的SprinBoot 2.3.4.RELEASE版本,开始给项目搭架子。

起初,大多数的组件引入都一切顺利,本以为就要大功告成了,没想到在搭建日志切面时栽了跟头。

作为一个接口服务,为了方便查询接口调用情况和定位问题,一般都会将请求日志打印出来,而Spring的AOP作为切面支持,完美的切合了日志记录的需求。

之前的项目中,运行正确的切面日志记录效果如下图:

可以看到图内的一次方法调用,会输出请求url,出入参,以及请求IP等等,之前为了好看,还加入了分割线。

我把这个实现类放入新项目中,执行出来却是这样的:

我揉了揉眼睛,仔细看了看复制过来的老代码,精简版如下:

/**
 * 在切点之前织入
 * @param joinPoint
 * @throws Throwable
 */
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
    // 开始打印请求日志
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();

    // 初始化traceId
    initTraceId(request);

    // 打印请求相关参数
    LOGGER.info("========================================== Start ==========================================");
    // 打印请求 url
    LOGGER.info("URL            : {}", request.getRequestURL().toString());
    // 打印 Http method
    LOGGER.info("HTTP Method    : {}", request.getMethod());
    // 打印调用 controller 的全路径以及执行方法
    LOGGER.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
    // 打印请求的 IP
    LOGGER.info("IP             : {}", IPAddressUtil.getIpAdrress(request));
    // 打印请求入参
    LOGGER.info("Request Args   : {}", joinPoint.getArgs());
}

/**
 * 在切点之后织入
 * @throws Throwable
 */
@After("webLog()")
public void doAfter() throws Throwable {
    LOGGER.info("=========================================== End ===========================================");
}

/**
 * 环绕
 * @param proceedingJoinPoint
 * @return
 * @throws Throwable
 */
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    Object result = proceedingJoinPoint.proceed();
    // 打印出参
    LOGGER.info("Response Args  : {}", result);
    // 执行耗时
    LOGGER.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
    return result;
}

代码感觉完全没有问题,难道新版本的SpringBoot出Bug了。

显然,成熟的框架不会在这种大方向上犯错误,那会不会是新版本的SpringBoot把@After和@Around的顺序反过来了?

其实事情也没有那么简单。

 

Spring AOP执行顺序

我们先来回顾下Spring AOP执行顺序。

我们在网上查找关于SpringAop执行顺序的的资料,大多数时候,你会查到如下的答案:

正常情况

异常情况

多个切面的情况

所以@Around理应在@After之前,但是在SprinBoot 2.3.4.RELEASE版本中,@Around切切实实执行在了@After之后。

当我尝试切换回2.2.5.RELEASE版本后,执行顺序又回到了@Around-->@After

 

探究顺序错误的真相

既然知道了是SpringBoot版本升级导致的问题(或者说顺序变化),那么就要来看看究竟是哪个库对AOP执行的顺序进行了变动,毕竟,SpringBoot只是“形”,真正的内核在Spring。

我们打开pom.xml文件,使用插件查看spring-aop的版本,发现SpringBoot 2.3.4.RELEASE 版本使用的AOP是spring-aop-5.2.9.RELEASE。

而2.2.5.RELEASE对应的是spring-aop-5.2.4.RELEASE

于是我去官网搜索文档,不得不说Spring由于过于庞大,官网的文档已经到了冗杂的地步,不过最终还是找到了:

https://docs.spring.io/spring-framework/docs/5.2.9.RELEASE/spring-framework-reference/core.html#aop-ataspectj-advice-ordering

As of Spring Framework 5.2.7, advice methods defined in the same @Aspect class that need to run at the same join point are assigned precedence based on their advice type in the following order, from highest to lowest precedence: @Around, @Before, @After, @AfterReturning, @AfterThrowing.

我粗浅的翻译一下重点:

从Spring5.2.7开始,在相同@Aspect类中,通知方法将根据其类型按照从高到低的优先级进行执行:@Around,@Before ,@After,@AfterReturning,@AfterThrowing。

这样看其实对比不明显,我们再回到老版本,也就是2.2.5.RELEASE对应的spring-aop-5.2.4.RELEASE,当时的文档是这么写的:

What happens when multiple pieces of advice all want to run at the same join point? Spring AOP follows the same precedence rules as AspectJ to determine the order of advice execution. The highest precedence advice runs first "on the way in" (so, given two pieces of before advice, the one with highest precedence runs first). "On the way out" from a join point, the highest precedence advice runs last (so, given two pieces of after advice, the one with the highest precedence will run second).

简单翻译:在相同@Aspect类中Spring AOP遵循与AspectJ相同的优先级规则来确定advice执行的顺序。

再挖深一点,那么AspectJ的优先级规则是什么样的?

我找了AspectJ的文档:

https://www.eclipse.org/aspectj/doc/next/progguide/semantics-advice.html


At a particular join point, advice is ordered by precedence.

A piece of around advice controls whether advice of lower precedence will run by calling proceed. The call to proceed will run the advice with next precedence, or the computation under the join point if there is no further advice.

A piece of before advice can prevent advice of lower precedence from running by throwing an exception. If it returns normally, however, then the advice of the next precedence, or the computation under the join pint if there is no further advice, will run.

Running after returning advice will run the advice of next precedence, or the computation under the join point if there is no further advice. Then, if that computation returned normally, the body of the advice will run.

Running after throwing advice will run the advice of next precedence, or the computation under the join point if there is no further advice. Then, if that computation threw an exception of an appropriate type, the body of the advice will run.

Running after advice will run the advice of next precedence, or the computation under the join point if there is no further advice. Then the body of the advice will run.

大伙又要说了,哎呀太长不看!简短地说,Aspectj的规则就是上面我们能够在网上查阅到的顺序图展示的那样,依旧是老的顺序。

 

代码验证

我把业务逻辑从代码中删除,只验证下这几个advice的执行顺序:

/**
 * 日志切面
 */
@Aspect
@Component
public class WebLogAspect {

    private final static Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);

    /** 以 controller 包下定义的所有请求为切入点 */
    @Pointcut("execution(public * com.xx.xxx.xxx.controller..*.*(..))")
    public void webLog() {}

    /**
     * 在切点之前织入
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        LOGGER.info("-------------doBefore-------------");
    }

    @AfterReturning("webLog()")
    public void afterReturning() {
        LOGGER.info("-------------afterReturning-------------");
    }
    @AfterThrowing("webLog()")
    public void afterThrowing() {
        LOGGER.info("-------------afterThrowing-------------");
    }

    /**
     * 在切点之后织入
     * @throws Throwable
     */
    @After("webLog()")
    public void doAfter() throws Throwable {
        LOGGER.info("-------------doAfter-------------");
    }

    /**
     * 环绕
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        LOGGER.info("-------------doAround before proceed-------------");
        Object result = proceedingJoinPoint.proceed();
        LOGGER.info("-------------doAround after proceed-------------");
        return result;
    }

我们将版本改为2.2.5.RELEASE,结果如图:

我们将版本改为2.3.4.RELEASE,结果如图:

结论

经过上面的资料文档查阅,我能给出的结论是

从Spring5.2.7开始,Spring AOP不再严格按照AspectJ定义的规则来执行advice,而是根据其类型按照从高到低的优先级进行执行:@Around,@Before ,@After,@AfterReturning,@AfterThrowing。

这次的研究思考十分仓促,如果结论有误请大家踊跃指正,也欢迎大家自己尝试,毕竟口说无凭,实验是检验真理的唯一标准!

参考

https://www.cnblogs.com/dennyLee2025/p/13724981.html

https://segmentfault.com/a/1190000011283029

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

<think>我们正在讨论Spring框架中拦截器(Interceptor)和面向切面编程(AOP)的执行顺序问题。根据提供的引用信息,我们可以总结以下几点: 1. **作用范围**: - Filter:处理所有HTTP请求(包括静态资源)[^1]。 - Interceptor:仅拦截Spring MVC处理的请求(即Controller映射的请求)[^1]。 - AOP:拦截Spring Bean的方法调用(如Service层)[^1]。 2. **执行顺序**: - 在请求处理流程中,Filter最先执行,然后是Interceptor,最后是AOP(如果拦截的是Controller中的方法,则Interceptor在Controller方法之前执行,而AOP通常用于Service层,所以执行顺序上Interceptor在Controller方法前后,而AOP在Service方法前后)[^1]。 - 但是,如果我们在Controller层也使用AOP(例如,对Controller的方法进行增强),那么Interceptor和AOP执行顺序就需要明确。 3. **AOP执行顺序**: - AOP切面可以通过实现`Ordered`接口或使用`@Order`注解来指定执行顺序。数值越小,优先级越高,即越先执行[^3]。 - 例如,在提供的代码示例中,`OrderedAspect`的`getOrder()`返回1,`AnotherOrderedAspect`返回2,因此`OrderedAspect`的增强会先执行[^3]。 4. **Interceptor和AOP在Controller层的执行顺序**: - 当请求到达Controller时,首先会经过Interceptor的`preHandle`方法,然后进入Controller方法。在Controller方法内部,如果调用了Service层的方法,那么Service层的AOP才会被触发。但是,如果我们在Controller方法上直接应用AOP(例如,使用`@Around`注解拦截Controller方法),那么Interceptor和AOP顺序如何? - 实际上,Interceptor是Spring MVC的一部分,它在HandlerAdapter调用Controller方法之前和之后执行。而AOP(如果应用于Controller)是通过代理实现的,它会在Interceptor之后执行吗?还是之前? 根据Spring官方文档和常见实践,在同一个请求处理流程中,执行顺序通常是: 1. Filter(Servlet规范) 2. Interceptor(Spring MVC) - 在HandlerMapping确定对应的Controller之后,HandlerAdapter调用Controller方法之前,会执行Interceptor的`preHandle`方法。 3. 然后,如果Controller方法被AOP代理,那么AOP的增强(如`@Before`)会在Controller方法执行执行。 4. 接着,Controller方法执行。 5. 之后,AOP的增强(如`@After`)会在Controller方法执行执行。 6. 最后,Interceptor的`postHandle`和`afterCompletion`方法会被调用。 因此,在Controller方法上的AOP增强,其执行顺序在Interceptor的`preHandle`之后,在Interceptor的`postHandle`之前。 但是,请注意,AOP的增强顺序可以通过`@Order`或实现`Ordered`接口来调整,而Interceptor的顺序可以通过`Ordered`接口或`@Order`注解在配置时指定(通过WebMvcConfigurer的`addInterceptors`方法注册拦截器时设置顺序)。 **总结执行顺序(以Controller方法调用为例)**: 1. Filter的doFilter 2. Interceptor的preHandle 3. AOP增强(如果拦截Controller方法,且多个切面按order顺序执行) 4. 实际Controller方法执行 5. AOP增强的后置部分(如@After,@AfterReturning等,多个切面按order逆序执行) 6. Interceptor的postHandle 7. Interceptor的afterCompletion 8. Filter的后续处理 **注意**:以上顺序是典型的,但具体细节可能因配置和实际使用方式而有所不同。 下面是一个简单的代码示例,展示如何为AOP切面指定顺序: ```java @Aspect @Component @Order(1) // 数值越小,优先级越高 public class LoggingAspect { @Before("execution(* com.example.controller.*.*(..))") public void logBefore(JoinPoint joinPoint) { // 在Controller方法执行前记录日志 } } ``` 而对于Interceptor的顺序,可以在注册时指定: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()).order(Ordered.HIGHEST_PRECEDENCE); // 最高优先级 } } ``` 在Interceptor中,`order`值越小,执行越靠前。 因此,如果我们想要一个全局的执行顺序(包括Filter、Interceptor、AOP),我们需要分别设置它们的顺序。 **相关问题**: 1. 如何控制多个AOP切面之间的执行顺序? 2. 在Spring MVC中,如何设置多个拦截器的执行顺序? 3. Filter、Interceptor和AOPSpring Boot应用中的执行顺序是怎样的? 4. 能否在Interceptor中获取AOP增强后的结果? 5. 在Controller方法上使用AOP有什么注意事项? 请根据上述问题选择您想进一步了解的内容。</think>在 Spring 框架中,拦截器(Interceptor)和 AOP执行顺序遵循明确的规则,主要取决于它们在请求处理流程中的位置和作用范围: ### 执行顺序总结 1. **Filter**(过滤器) - 最先执行,处理所有 HTTP 请求(包括静态资源) - 作用范围:Servlet 容器级别 - 示例顺序:`Filter#doFilter()` 2. **Interceptor**(拦截器) - 在 Filter 之后执行,仅作用于 Spring MVC 处理的请求(Controller 层) - 执行流程: ```text Interceptor#preHandle() → Controller 方法 → Interceptor#postHandle() → Interceptor#afterCompletion() ``` 3. **AOP**(面向切面编程) - 最后执行,作用于 Spring Bean 的方法(如 Service 层) - 通过切面(Aspect)定义增强逻辑(如 `@Before`, `@Around`) - 多个切面可通过 `@Order` 或实现 `Ordered` 接口指定优先级(数值越小优先级越高)[^3] ### 完整执行流程示例 ```text HTTP 请求 │ ├─ 1. Filter 链 (按配置顺序执行) │ ├─ 2. Interceptor#preHandle() (按注册顺序执行) │ ├─ 3. Controller 方法 │ │ │ ├─ 3.1 AOP 增强 (按 @Order 数值升序执行) │ │ ├─ OrderedAspect (Order=1) [^3] │ │ └─ AnotherOrderedAspect (Order=2) [^3] │ │ │ └─ 3.2 实际业务逻辑 (Service 方法) │ ├─ 4. Interceptor#postHandle() (按注册顺序逆序执行) │ └─ 5. Interceptor#afterCompletion() (按注册顺序逆序执行) ``` ### 关键区别 | 组件 | 作用范围 | 执行阶段 | 配置方式 | |--------------|----------------------------------|-----------------------|--------------------------| | **Filter** | 所有 HTTP 请求(包括静态资源)[^1] | Servlet 容器级别 | `web.xml` 或 `FilterRegistrationBean` | | **Interceptor** | 仅 Spring MVC 请求(Controller)[^1] | Controller 方法前后 | 实现 `HandlerInterceptor` + `WebMvcConfigurer` | | **AOP** | Spring Bean 方法(如 Service)[^1] | 方法调用前后 | `@Aspect` + `@Order` [^3] | ### 配置示例 1. **Interceptor 顺序控制**(通过注册顺序): ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LogInterceptor()).order(1); // 先执行 registry.addInterceptor(new AuthInterceptor()).order(2); // 后执行 } } ``` 2. **AOP 顺序控制**(通过 `@Order`): ```java @Aspect @Order(1) // 数值小的先执行 public class LogAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore() { ... } } ``` ### 注意事项 - AOP **不会**拦截 `private` 方法或非 Spring 管理的对象 - Interceptor 的 `postHandle()` 在 Controller 方法之后、视图渲染之前执行 - 若 Controller 调用 Service 方法,AOP 会在 Service 方法执行时触发,但仍在 Interceptor 的 `postHandle()` 之前完成
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值