【线上排查实战】AOP切面执行顺序你真的了解吗
前言
忙,是我这个月的主旋律,也是我频繁鸽文章的接口————蛮三刀把刀
公司这两个月启动了全新的项目,项目排期满满当当,不过该学习还是要学习。这不,给公司搭项目的时候,踩到了一个Spring AOP的坑。
本文内容重点:
- 问题描述
- Spring AOP执行顺序
- 探究顺序错误的真相
- 代码验证
- 结论
本文阅读大概需要:3分钟
码字不易,求个关注,欢迎关注我的个人原创公众号:后端技术漫谈(二维码见文章底部)
问题描述
公司新项目需要搭建一个新的前后分离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由于过于庞大,官网的文档已经到了冗杂的地步,不过最终还是找到了:
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的执行顺序:
package com.bj58.xfbusiness.cloudstore.system.aop;
import com.bj58.xfbusiness.cloudstore.utils.IPAddressUtil;
import com.bj58.xfbusiness.cloudstore.utils.TraceIdUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 日志切面
*/
@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面试知识
- 设计模式/数据结构/算法题解
- 读书笔记/逸闻趣事/程序人生
个人公众号:后端技术漫谈
如果文章对你有帮助,不妨点赞,收藏起来~
【线上排查实战】AOP切面执行顺序你真的了解吗的更多相关文章
- linux实用指令 | 程序员线上排查必知必会linux指令(持续更新中)
Linux线上排查程序员实用指南 一.乱码问题 二.帮助指令 1. help命令 2. man命令 3. info命令 三.性能监测与优化 1. top命令 参考资源 Linux线上排查程序员实用指南 ...
- Spring Aop的执行顺序
Spring Aop的执行顺序 首先回忆一下 AOP 的常用注解 @Before:前置通知:目标方法之前执行 @After:后置通知:目标方法之后执行 @AfterReturning:返回后通知:执行 ...
- 浅谈Spring AOP 面向切面编程 最通俗易懂的画图理解AOP、AOP通知执行顺序~
简介 我们都知道,Spring 框架作为后端主流框架之一,最有特点的三部分就是IOC控制反转.依赖注入.以及AOP切面.当然AOP作为一个Spring 的重要组成模块,当然IOC是不依赖于Spring ...
- aspx页面Page_Load和aspx页面上控件Page_Load事件执行顺序
今天公司的同事问了我一个问题,就是页面的Load方法和控件上的Load方法执行顺序的问题,看完了这个图片的递归调用之后大家就笑了,吼吼.
- 线上排查Class、Jar加载问题的一般方法
问题背景 本问题源于<ojdbc6中OraclePreparedStatement的ArrayIndexOutOfBoundsException异常BUG-6396242>这篇博文中最后思 ...
- Django线上部署实战教程之Nginx+Gunicorn+Django篇
############################################# 推荐 nginx supervisor gunicorn 配置简单,运维方便. Nginx (engi ...
- 线上CPU飙升100%问题排查,一篇足矣
一.引子 对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!特此整理排查方法一篇,供大家参考讨论提高. 二.问题复现 线上系统突然 ...
- 线上CPU飙升100%问题排查
本文转载自线上CPU飙升100%问题排查 引子 对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!特此整理排查方法一篇,供大家参考 ...
- 推荐几个我近期排查线上http接口偶发415时用到的工具
导读:近期有一个业务部门的同学反馈说他负责的C工程在小概率情况下SpringMvc会返回415,通过输出的日志可以确定是SpringMvc找不到content-type这个头了,具体为什么找不到了呢? ...
随机推荐
- 使用Mysql分区表对数据库进行优化
早期工作中没有做好足够的设计,目前记录表单表数据2000w且无有效索引,表现是分页缓慢,模糊查询拉闸. 当前业务中,写操作会多于读操作,时不时会遇到慢SQL占用过多的数据连接,导致写操作无法正常进行. ...
- Keepalived服务详解
1. VRRP协议 1.1 VRRP协议概述 VRRP协议的出现是为了解决静态路由的单点故障,它是通过一种竞选机制来将路由任务交给某个vrrp路由器的 在VRRP物理结构中,有多个物理的VRRP路由器 ...
- 《Linux从入门到精通》笔记
第一篇 基础篇 第1章 Linux概述 1.1 Linux的起源 1991年芬兰学生Linus Torvalds写的磁盘驱动和文件系统开源发布,Linux即"Linus的Minix&qu ...
- php+html实现用户登录退出
随着渗透学习,逐渐意识到了学会开发也是非常重要的,仅仅是看懂感觉还是差了一些,所以写一写php的开发,这套程序目前并未有较完整的功能,之后会不断进行完善 登录页面.html <!DOCTYPE ...
- SSTI服务器模板注入(以及关于渲染,solt的学习)&&[BJDCTF2020]The mystery of ip 1
ssti服务器模板注入 ssti:利用公共 Web 框架的服务器端模板作为攻击媒介的攻击方式,该攻击利用了嵌入模板的用户输入方式的弱点.SSTI 攻击可以用来找出 Web 应用程序的内容结构. slo ...
- 082 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 02 构造方法介绍 01 构造方法-无参构造方法
082 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 02 构造方法介绍 01 构造方法-无参构造方法 本文知识点:构造方法-无参构造方法 说明:因为时间紧张, ...
- 【题解】[LNOI2014]LCA
题目戳我 \(\text{Solution:}\) 这题的转化思想很巧妙-- 考虑把\(dep\)给拆掉. 首先可以明确的是,每一个\(LCA\)一定在\(root\to z\)的路径上. 考虑一个\ ...
- ESP8266 玩板记
一.前言 esp8266的玩板记,后面应该会去更一些其他东西,这一块内容,这算是收官之战了. IoT,江湖有缘再相会 二.ESP8266实现WiFi杀手/钓鱼 这次的博客做的是一个娱乐性较强的项目. ...
- AA.Dapper升级了
AA.Dapper基于dapper进一步封装而成的orm框架,提供增删改查.分页.事务.原生sql的功能,以满足日常的业务开发. 1.Repository层: DapperRepository类包含大 ...
- 在自己电脑上查看git远程分支列表比实际云端的远程分支要多一些
问题 最近打开一个很久没有打开过的项目,使用git branch -a查看了一下所以分支,其中有些远程分支没有什么用了 于是准备去gitlab上删除它,结果到gitlab上发现没有这些分支,猜测是自己 ...