《SpringBoot 3:入门与应用实战》第 8 章 AOP 的进阶机制和应用 阅读笔记 16

《SpringBoot 3:入门与应用实战》第 8 章 AOP 的进阶机制和应用 阅读笔记 16

Spring Boot 中使用 AOP 的方式相对简单,读者只需要掌握 5 种通知类型以及切入点表达式的编写方式,就可以熟练地完成 AOP 的核心开发。本章继续讲解一些 AOP 的进阶机制和应用。

8.1 AOP 联盟

Spring Framework 2.0 版本之前还没有整合 AspectJ,当时的 Spring Framework 有一套相对低层级的实现,它也是 Spring Framework 原生的实现,要了解它,需要先了解一个组织:AOP 联盟。

早在很久之前,AOP 的概念就已经被提出。一个概念和思想被提出后,总会有一批人来制定规范,于是就有了 AOP 联盟,这个联盟的成员将AOP 的概念整理好,形成了一套规范 AOP 框架底层实现的 API,并最终总结出了 5 种 AOP 通知类型,而读者要了解的就是 AOP 联盟提出的这5 种通知类型:前置通知、后置通知(返回通知)​、异常通知、环绕通知、引介通知。注意,以上 5 种通知类型跟 7.3.4 节中 AspectJ 规定的 5种通知类型有所区别:AOP 联盟提出的 5 种通知类型中多了一个引介通知。还有一点要注意,AOP 联盟定义的后置通知实际上是返回通知 (after-returning),而 AspectJ 的后置通知是真正的后置通知,与返回通知是两码事。

AOP 联盟定义的 5 种通知类型在 Spring Framework 中都有对应的接口定义。

  • 前置通知:org.springframework.aop.MethodBeforeAdvice。
  • 返回通知:org.springframework.aop.AfterReturningAdvice。
  • 异常通知:org.springframework.aop.ThrowsAdvice。
  • 环绕通知:org.aopalliance.intercept.MethodInterceptor。
  • 引介通知:org.springframework.aop.IntroductionAdvisor。

8.2 通知方法参数

在 7.4.5 节中编写环绕通知时,我们曾使用到一个特殊的接口 ProceedingJoinPoint,对于这个接口的具体使用以及切面类中的其他通知方法参数等内容,本节会重点展开讲解。

8.2.1 JoinPoint

在第 7 章的代码编写过程中,读者是否会产生一个疑问:所有的日志打印都是一模一样的,如果被切入的方法没有控制台打印,如何更准确地区分每一行日志打印是由哪个方法触发的?实际项目开发中还有可能遇到这样的问题:某一个增强的逻辑需要同时切入多个方法,但又要根据不同的方法名区分不同的逻辑细节。由上述两个问题可知,应该想办法在切面类的通知方法中获取被增强的原始对象、被切入的方法信息等。

获取切入点相关信息的核心 API 就是本节的标题:JoinPoint,类名意为 "连接点"​,是所有通知方法都允许声明的参数类型。比如 beforePrint 方法中增加 JoinPoint 类型的入参后,程序运行中不会报错,仍然可以正常运行完毕,说明这种编写方式被 Spring Framework 支持。

借助 IDE 尝试调用 JoinPoint 的 get 系列方法,可以发现可获取的内容很多。下面就其中比较重要的几个元素展开讲解。

1.getTarget 与 getThis

getTarget 与 getThis 是一对相似的方法,分别代表获取目标对象和当前代理对象。可以简单测试一下效果。

随后还要修改 SpringBootAopApplication 主启动类的代码,获取 exampleb 包下的 FinanceService 并调用 addMoney 方法。修改完毕后运行主启动类,观察控制台中打印的如下内容,很明显当前被代理的类已经可以成功打印。

虽然可以正确获取目标对象和代理对象,但是两个对象的打印结果完全一致,它们两个是不是同一个对象?可以借助 Debug 来验证。在beforePrint 方法的第一行打上断点,以 Debug 的形式运行主启动类,等到程序运行到断点时观察 joinPoint 中 proxy 和 target 对象的结构,很明显这两个对象不是同一个,proxy 是被 Cglib 增强的代理对象。

既然对象不是同一个,为什么打印的结果一致?仔细观察 proxy 中的几个 CALLBACK,可以发现代理对象增强了 equals 方法、hashcode 方法,唯独没有增强 toString 方法,所以 proxy 代理对象执行 toString 方法时,实际上调用的是 target 目标对象的 toString 方法,相当于 target 的 toString 方法被调用两次。

2.getArgs

getArgs 方法非常容易理解,它可以获取被拦截方法的调用参数列表,它返回的是一个 Object 数组。重新运行 Spring Boot 的主启动类,可以发现控制台成功打印了 addMoney 方法被调用时传入的参数。

java 复制代码
package com.yangjunbo.springboot.aop.exampleb;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.exampleb.Log)")
    public void beforePrint(JoinPoint joinPoint) {
        System.out.println(joinPoint.getTarget());
        System.out.println(joinPoint.getThis());
        System.out.println(Arrays.toString(joinPoint.getArgs()));
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    //@After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    //@AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    //@AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.exampleb.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Logger aroundPrint before run ......");
        try {
            Object retVal = joinPoint.proceed();
            System.out.println("Logger aroundPrint afterReturning run ......");
            return retVal;
        } catch (Throwable e) {
            System.out.println("Logger aroundPrint afterThrowing run ......");
            throw e;
        } finally {
            System.out.println("Logger aroundPrint after run ......");
        }
    }

}

3.getSignature

getSignature 从方法名上看是获取签名,但是签名这个概念该如何理解?不妨先直接打印 getSignature 方法返回的结果。重新运行主启动类后,控制台打印的内容是被拦截方法的全限定名等信息。

java 复制代码
package com.yangjunbo.springboot.aop.exampleb;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.exampleb.Log)")
    public void beforePrint(JoinPoint joinPoint) {
        System.out.println(joinPoint.getTarget());
        System.out.println(joinPoint.getThis());
        System.out.println(Arrays.toString(joinPoint.getArgs()));
        System.out.println(joinPoint.getSignature());
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    //@After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    //@AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    //@AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.exampleb.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Logger aroundPrint before run ......");
        try {
            Object retVal = joinPoint.proceed();
            System.out.println("Logger aroundPrint afterReturning run ......");
            return retVal;
        } catch (Throwable e) {
            System.out.println("Logger aroundPrint afterThrowing run ......");
            throw e;
        } finally {
            System.out.println("Logger aroundPrint after run ......");
        }
    }

}

由此可以得知一点:getSignature 可以得到当前被拦截方法的方法签名,包括方法名、方法的参数类型、方法的返回值等。但是直接调用getSignature 方法返回的 Signature 对象中的方法时,会发现其中只包含与类相关的信息,并没有包含与方法相关的信息。

对此,读者是否产生了一些疑惑:既然基于 AspectJ 的 AOP 都是对方法的拦截,为什么获取的 Signature 对象中反而没有方法相关的信息?借助 IDE 可以得知,Signature 只是一个接口,其下有一个子接口 MethodSignature,这个子接口中刚好可以获取当前被拦截的方法和返回值等信息,由此就可以获取并打印更多的信息。

java 复制代码
package com.yangjunbo.springboot.aop.exampleb;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.exampleb.Log)")
    public void beforePrint(JoinPoint joinPoint) {
        System.out.println(joinPoint.getTarget());
        System.out.println(joinPoint.getThis());
        System.out.println(Arrays.toString(joinPoint.getArgs()));
        System.out.println(joinPoint.getSignature());
        //Signature signature = joinPoint.getSignature();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        System.out.println(method.getName());
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    //@After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    //@AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    //@AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.exampleb.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Logger aroundPrint before run ......");
        try {
            Object retVal = joinPoint.proceed();
            System.out.println("Logger aroundPrint afterReturning run ......");
            return retVal;
        } catch (Throwable e) {
            System.out.println("Logger aroundPrint afterThrowing run ......");
            throw e;
        } finally {
            System.out.println("Logger aroundPrint after run ......");
        }
    }

}

重新运行主启动类,控制台中可以打印当前被拦截的方法名,方法信息可以被成功获取。

4.改造示例

由上述 4 个关键因素,下面就可以实现具体逻辑来判断和处理。可以在通知方法中轻松获取被拦截的类、方法、参数等信息。

java 复制代码
package com.yangjunbo.springboot.aop.exampleb;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.exampleb.Log)")
    public void beforePrint(JoinPoint joinPoint) {
        System.out.println(joinPoint.getTarget());
        System.out.println(joinPoint.getThis());
        System.out.println(Arrays.toString(joinPoint.getArgs()));
        System.out.println(joinPoint.getSignature());
        //Signature signature = joinPoint.getSignature();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        System.out.println(method.getName());
        System.out.println("Logger beforePrint run ......");
        System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
        System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()). getMethod().getName());
        System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    //@After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    //@AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    //@AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.exampleb.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Logger aroundPrint before run ......");
        try {
            Object retVal = joinPoint.proceed();
            System.out.println("Logger aroundPrint afterReturning run ......");
            return retVal;
        } catch (Throwable e) {
            System.out.println("Logger aroundPrint afterThrowing run ......");
            throw e;
        } finally {
            System.out.println("Logger aroundPrint after run ......");
        }
    }

}

8.2.2 ProceedingJoinPoint 的扩展

第 7 章中讲解环绕通知时提前接触到了 ProceedingJoinPoint 这个接口,它本身是 JoinPoint 的扩展接口,扩展的方法只有 proceed 方法,也就是那个能在环绕通知中显式执行目标对象目标方法的 API。

另外有一点要注意:proceed 方法还有一个带参数的重载方法,它允许传入一个 Object 参数,这个设计可以说明另一点:在环绕通知中,可以自行替换掉原始目标方法执行时所传入的参数列表。

8.2.3 返回通知和异常通知的特殊参数

返回通知中我们要获取方法的返回值,异常通知中我们要获取具体抛出的异常信息。

在返回通知的方法中,可以在方法签名中声明一个 Object 类型的参数,并在 @AfterReturning 注解中声明这个参数就是接收目标方法执行完毕后的返回值。

可以发现,Spring Framework 本身也不知道声明的哪个方法参数用于接收目标方法执行的返回结果,所以必须在注解中告诉它才可以。随后调用 subtractMoney 方法触发返回通知。重新运行主启动类,控制台成功打印返回值,证明返回通知已正确捕获目标方法的返回值。

同理,异常通知的使用需要在方法签名中声明一个 Throwable 或具体异常类型的参数,并在 @AfterThrowing 注解中注明接收异常抛出的参数名。

相关推荐
mldong6 小时前
一个 App,十三套后端:手机审批端 uni-jeeflow-app 开源了
java·架构
tqs_123457 小时前
MySQL RR隔离级别死锁|Gap间隙锁、临键锁,订单并发范围查询死锁根因方案
java
逆境不可逃7 小时前
Pi Agent 学习笔记:多个工具怎样并行执行
java
weixin_BYSJ19878 小时前
【计算机毕设】基于SpringBoot与Vue的文物保护档案管理系统08621
vue.js·spring boot·spring cloud·微服务·架构·django·课程设计
老当益壮梁奶奶8 小时前
51单片机DS18B20温度传感器详解:单总线时序与C51驱动实现
笔记·stm32·单片机·嵌入式硬件·51单片机
Flynt8 小时前
Java 27 升级实测:默认值动得比新特性多,有个老参数会让 JVM 直接起不来
java·jvm·后端
Bs_MoneyMagnet8 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
星云API技术支持9 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
niucloud-admin9 小时前
JAVA V6 多商户商城 开发文档——job 计划任务开发
java·python·github
小叶肥辉9 小时前
LangChain链和LangGraph图的学习笔记【三】——分别用langchain_openai库和langchain_community库调用大模型
笔记·python·langchain