前言公司最近两个月上了一个新项目,项目排的满满的,但是学习还是有必要的。没有,在给公司搭建项目时,本文重点:问题描述SpringAOP执行顺序探究顺序错误的真相代码验证结论问题描述公司新项目需要新建一个前后端分离的HTTP服务。我选择了大家熟悉的SpringBootWeb来快速搭建一个可以使用的系统。鲁迅说,不要只升级已经稳定使用的版本。不信这个邪,用Spring这么久怎么能不着急呢。不说了,直接引入最新的SprinBoot2.3.4.RELEASE版本,开始搭建工程。起初,大部分组件都顺利导入。本来以为大功告成,没想到建日志段的时候磕磕绊绊。作为一个接口服务,为了方便查询接口调用,定位问题,一般都会打印请求日志,并且作为切面支持Spring的AOP,完美满足了日志记录的需求。在之前的项目中,运行正确切面的日志记录效果如下:可以看到图中有一个方法调用,会输出请求url、输入输出参数、请求IP等,并添加了一个分割线好看。我把这个实现类放到了一个新项目中,但是执行是这样的:我揉了揉眼睛,仔细看了看复制的旧代码。简化版如下:/***weaving*@切点前paramjoinPoint*@throwsThrowable*/@Before("webLog()")publicvoiddoBefore(JoinPointjoinPoint)throwsThrowable{//开始打印请求日志ServletRequestAttributesattributes=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequestRequest=ittributes(Id/属性)。(request);//打印请求相关参数LOGGER.info("=============================================开始==========================================");//打印请求urlLOGGER.info("URL:{}",request.getRequestURL().toString());//打印HttpmethodLOGGER.info("HTTPMethod:{}",request.获取方法());//打印调用控制器的完整路径和执行方法LOGGER.info("ClassMethod:{}.{}",joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName());//PrintrequestIPLOGGER.info("IP:{}",IPAddressUtil.getIpAdrress(request));//打印请求入参数LOGGER.info("RequestArgs:{}",joinPoint.getArgs());}/***在编织中*@throwsThrowable*/@After("webLog()")publicvoiddoAfter()throwsThrowable{LOGGER.info("==============================================结束==============================================");}/***Surround*@paramproceedingJoinPoint*@return*@throwsThrowable*/@Around("webLog()")publicObjectdoAround(ProceedingJoinPointproceedingJoinPoint)throwsThrowable{longstartTime=System.currentTimeMillis();Objectresult=proceedingJoinPoint.proceed();//打印出参数LOGGER.info("ResponseArgs:{}",result);//执行耗时LOGGER.info("Time-Consuming:{}ms",System.currentTimeMillis()-startTime);returnresult;}代码感觉完全没问题。是不是新版的SpringBoot有bug?很显然,成熟的框架是不会在这个大方向上出错的。会是新版本吗?SpringBoot会把@After和@Around的顺序颠倒吗?事实上,事情并没有那么简单。SpringAOP的执行顺序我们先来回顾一下SpringAOP的执行顺序。我们在网上搜索有关SpringAop执行顺序的资料。大多数时候,你会找到以下答案:正常情况和异常情况,有多个方面,所以@Around应该在@After之前,但是在SprinBoot2.3.4.RELEASE版本中,@Around实际上是在@After之后执行的。当我尝试切换回2.2.5.RELEASE版本时,执行顺序又回到了@Around-->@After,探究顺序错误的真相。既然知道是SpringBoot版本升级(或者顺序改变)导致的问题,那我们就要看看是哪个库改变了AOP的执行顺序。说到底,SpringBoot只是一个“造型”,真正的核心在Spring。我们打开pom.xml文件,使用插件查看spring-aop的版本,发现SpringBoot2.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从SpringFramework5.2.7开始,需要在同一连接点运行的同一@Aspect类中定义的建议方法根据它们的优先级分配按照以下顺序输入建议,从最高到最低的优先级:@Around、@Before、@After、@AfterReturning、@AfterThrowing。我的粗略翻译这里重点说一下:从Spring5.2.7开始,在同一个@Aspect类中,通知方法会按照其类型优先级从高到低依次执行:@Around、@Before、@After、@AfterReturning、@AfterThrowing。这样一来,对比就不明显了。我们回到老版本,也就是2.2.5.RELEASE对应的spring-aop-5.2.4.RELEASE。当时的文档是这样写的:当多条advice都想运行在同一个joinpoint会怎样?SpringAOP遵循与AspectJ相同的优先级规则来确定通知执行的顺序。最高优先级的建议首先“在进入的路上”运行(因此,给定两条之前的建议,具有最高优先级的建议首先运行)。从连接点“出去”时,最高优先级的通知最后运行(因此,给定两条后通知,具有最高优先级的通知将第二运行)。简单翻译:在同一个@SpringAOP中的Aspect类遵循与AspectJ相同的优先级规则来决定advice的执行顺序。再挖深一点,那么AspectJ的优先级规则是怎样的?我找到了AspectJ的文档:https://www.eclipse.org/aspectj/doc/next/progguide/semantics-advice.html在一个特定的连接点,advice是按优先级排序的。一条aroundadvice控制低优先级的advice是否会通过调用proceed来运行。对proceed的调用将运行具有下一个优先级的建议,或者如果没有进一步的建议,则在连接点下进行计算。一段之前的建议可以通过抛出异常来阻止较低优先级的建议运行。但是,如果它正常返回,则下一个优先级的建议,或者如果没有进一步的建议,则连接pint下的计算将运行。返回建议后运行将运行下一个优先级的建议,或连接下的计算点如果没有进一步的建议。然后,如果该计算正常返回,建议的主体将运行。在抛出建议后运行将运行下一个优先级的建议,或th如果没有进一步的建议,则在连接点下进行计算。然后,如果该计算抛出适当类型的异常,建议的主体将运行。在建议之后运行将运行下一个优先级的建议,或者如果没有进一步的建议,则运行连接点下的计算。然后建议的主体将运行。大家又要说了,哎呀,看得太长了!总之,Aspectj的规则如上面的时序图所示,我们可以上网查一下那样,还是老规矩为了验证代码,我删除了代码中的业务逻辑,只验证了这几个advice的执行顺序:with控制器包下定义的所有请求都是入口点*/@Pointcut("execution(public*com.xx.xxx.xxx.controller..*.*(..))")publicvoidwebLog(){}/***编织成*@paramjoinPoint*@throwsThrowable*/@Before("webLog()")publicvoiddoBefore(JoinPointjoinPoint)throwsThrowable{LOGGER.info("------------doBefore--------------");}@AfterReturning("webLog()")publicvoidafterReturning(){LOGGER.info("------------afterReturning-------------");}@AfterThrowing("webLog()")publicvoidafterThrowing(){LOGGER.info("------------afterThrowing------------");}/***编织成*@throwsThrowable*/@After("webLog()")publicvoiddoAfter()throwsThrowable{LOGGER.info("--------------doAfter------------");}/***around*@paramprocedingJoinPoint*@返回*@throwsThrowable*/@Around("webLog()")publicObjectdoAround(ProceedingJoinPointproceedingJoinPoint)throwsThrowable{longstartTime=System.currentTimeMillis();LOGGER.info("------------doAroundbeforeproceed-------------");Objectresult=proceedingJoinPoint.proceed();LOGGER.info("------------doAroundafterproceed------------");returnresult;}我们把版本改成2.2.5.RELEASE,结果如图:我们把版本改成2.3.4.RELEASE,结果如图:我能给出的结论是:从Spring5开始,从2.7开始,SpringAOP不再严格按照AspectJ定义的规则执行advice,而是按照其优先级从高到低的类型执行:@Around、@Before、@After、@AfterReturning,@AfterThrowing这次的研究和思考很仓促,如果结论有误,欢迎大家踊跃指正,也欢迎大家自己尝试,毕竟自己说的并没有被证实,唯一实验室检验真伪的标准!参考https://www.cnblogs.com/dennyLee2025/p/13724981.htmlhttps://segmentfault.com/a/1190000011283029本文转载自微信公众号“后端技术漫谈”,可关注以下二维码代码。转载本文请联系后台技术讲座公众号。
