【Spring AOP 源码解析前篇】什么是 AOP | 通知类型 | 切点表达式| AOP 如何使用

前言(关于源码航行)
在这里插入图片描述

在准备面试和学习的过程中,我阅读了还算多的源码,比如 JUC、Spring、MyBatis,收获了很多代码的设计思想,也对平时调用的 API 有了更深入的理解;但过多散乱的笔记给我的整理复习带来了比较大的麻烦。
📋 在 C 站零零散散发了 JUC 的源码解析和集合源码解析,收到了很多朋友的喜爱,这里我准备将一些源码解析的文章整合起来,为了方便阅读和归纳在这里整合成目录:源码航行阅读目录,大家感兴趣的话可以关注一下!
————————————————

第一篇:基础知识介绍

这一部分我们来谈一下关于 Spring AOP 的浮在表面上的知识,比如什么是 AOP、它有什么好处、如何使用等等

为什么需要 AOP?

AOP(Aspect Oriented Programming),意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

举一个简单的例子,如果我们想要在每次调用一个类中的方法之前输出一个日志,那其实是挺容易实现的,画出流程图就是这个样子:
在这里插入图片描述

那如果我们要在这些方法之前加一个鉴权呢?同样,按照前面的方式,无非就是多 copy 一次嘛

在这里插入图片描述

就这样,通过多重 copy 的方式,最终完成了这个业务,而这也就代表着,每次扩展新的方法,你都要去进行 n 次复制粘贴;每次要更改鉴权或者日志的逻辑,你都要去进行 n 次复制粘贴;每次。。。。。。

听起来都让人非常头大,但如果我们将逻辑改成这样呢?

在这里插入图片描述

我们将这两个方法封装成一个公共方法,每次在方法之前去调用这个公共的方法,不就实现了可维护性吗,这其实就有一点切面的感觉了。

这样,我们虽然每次都不用去复制代码了,但还是需要在我们需要的位置去调用这个接口,而调用这个代码的位置其实就是 切面。我们用具体的代码来看一下,下面实现了一个统一的接口 BeforeAdvice,然后在每个方法之前去调用它。

public interface BeforeAdvice {
    void before();
}

public class LoggingBeforeAdvice implements BeforeAdvice {
    @Override
    public void before() {
        System.out.println("方法调用之前的日志");
    }
}

public class MyService {
    private BeforeAdvice beforeAdvice = new LoggingBeforeAdvice();

    public void myMethodA() {
        beforeAdvice.before();
        // 业务逻辑
        System.out.println("执行 myMethod1A");
    }

    public void myMethodB() {
        beforeAdvice.before();
        // 业务逻辑
        System.out.println("执行 myMethodB");
    }
}

到这里,我们就自己实现了一个简单的 AOP;但每次都这样去创建一个这样的类,其实也是非常复杂的,这时候我们就可以借助 Spring AOP 的力量,它帮我们实现了简单、直观、极其易于配置的切面编程方式。

AOP 概念辨析

在正式讲解如何使用 Spring AOP 之前,我们来讲点无聊的内容,关于 AOP 的概念辨析;这部分内容是一些术语,但是如果能理解它们,就会对 AOP 整个流程有较为清晰的把握。

连接点(Join Point):连接点是程序执行中的一个点,这个点可以是方法调用、方法执行、异常抛出等。在 Spring AOP 中,连接点主要是指方法的调用或执行。连接点是通知的实际应用点。

切点(PointCut):由于连接点可能很多(比如一个类中的所有方法),想要把所有连接点罗列出现显然有些困难;切点则定义了在应用通知的连接点的集合。切点通过切点表达式(例如:execution(* com.example.service.*.*(..)))来指定匹配的方法和类。切点表达式用于筛选连接点,使得通知只在特定的连接点上执行。

通知(Advice):通知是在切点处执行的代码。通知定义了具体的横切逻辑,决定了在方法执行的什么阶段(之前、之后、环绕等)插入横切逻辑。通知有五种类型,我们会在下一部分进行详细的了解;通知就是在 何时 执行 怎样 的逻辑。

切面(Aspect):切面是 AOP 的核心模块,它封装了跨越多个类的关注点,例如日志记录、事务管理或安全控制。切面通过通知(Advice)和切点(Pointcut)来定义在何时、何地应用这些关注点;可以将切面看作是切点(Pointcut)和通知(Advice)的组合。切面定义了在何处(切点)以及何时(通知)应用横切逻辑。

五种通知类型

在 Spring AOP 中,通知(Advice)是指在程序执行过程中插入的代码,它定义了在何时以及在什么情况下进行切面的操作。通知是切面中的实际动作部分,是横切关注点的具体实现;直观来说就是要插入的那一组方法。

除了上面提到的在执行方法之前执行的 Before Advice,还有其他四种类型的通知,也就是说 Spring AOP 为我们提供了五个插入代码的位置选择。

前置通知(Before Advice)

在目标方法执行之前执行的通知。可以用来执行日志记录、安全检查等。

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice() {
        System.out.println("前置通知:方法调用之前执行");
    }
}

后置通知(After Advice)

在目标方法执行之后执行的通知,无论方法是成功返回还是抛出异常。常用于清理资源等。

@Aspect
@Component
public class LoggingAspect {
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice() {
        System.out.println("后置通知:方法调用之后执行");
    }
}

返回后通知(After Returning Advice)

在目标方法成功返回结果之后执行的通知。可以用来记录返回值或对返回值进行处理。

@Aspect
@Component
public class LoggingAspect {
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(Object result) {
        System.out.println("返回后通知:方法返回值为 " + result);
    }
}

抛出异常后通知(After Throwing Advice)

在目标方法抛出异常后执行的通知。可以用来记录异常信息或执行异常处理逻辑。

@Aspect
@Component
public class LoggingAspect {
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
    public void afterThrowingAdvice(Exception exception) {
        System.out.println("抛出异常后通知:异常为 " + exception.getMessage());
    }
}

环绕通知(Around Advice)

环绕通知在目标方法执行的前后都执行,可以完全控制目标方法的执行,包括决定是否执行目标方法,以及在目标方法执行前后添加自定义逻辑。环绕通知最为强大和灵活。

@Aspect
@Component
public class LoggingAspect {
    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知:方法调用之前");
        Object result = joinPoint.proceed();  // 执行目标方法
        System.out.println("环绕通知:方法调用之后");
        return result;
    }
}

这五种通知方式的执行顺序是这样的:

  • 前置通知(Before Advice)
  • **环绕通知(Around Advice)**的前半部分
  • 目标方法执行
  • **环绕通知(Around Advice)**的后半部分
  • 返回后通知(After Returning Advice)(如果目标方法成功返回)
  • 抛出异常后通知(After Throwing Advice)(如果目标方法抛出异常)
  • 后置通知(After Advice)

切点表达式

前面提到过,切面直观来讲就是插入方法的位置;在前面五种通知类型中,我们已经看到了如何通过注解选择方法的执行位置,但是诸如
* com.example.service.*.*(..)) 这样,定位方法位置的格式其实是没有提及的,这一部分重点来讲一下如何配置方法的位置。

切点则表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

切点表达式由以下几部分组成:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

其中,各部分的含义如下,注意上面的问号(?)表示可选

  • execution:指定切点类型为方法执行。
  • modifiers-pattern:可选,方法的访问修饰符,如 publicprotectedprivate。通常省略不写,表示任意访问修饰符。
  • ret-type-pattern:方法的返回类型模式,例如 voidString、``(任意返回类型)。
  • declaring-type-pattern:可选,方法所在类的全限定名称模式,如 com.example.service.*。指定要匹配的类或包。
  • name-pattern:方法名称模式,如 Serviceget*。支持通配符 ``(匹配任意字符序列)和 ..(匹配任意数量和类型的参数)。
  • param-pattern:方法参数模式,如 ()(无参数)、(*)(一个任意类型的参数)、(..)(任意数量和类型的参数)。
  • throws-pattern:可选,方法可能抛出的异常模式。

下面来做几个小练习

1)指定 com.example.service 包下的所有类的所有方法:

@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceMethods() {}

2)com.example.service 包下所有类的方法中第一个参数为 String 类型的方法

@Pointcut("execution(* com.example.service.*.*(String, ..))")
private void stringParamMethods() {}

通过上面的练习,相信大家对切点表达式的书写方式有了一定的掌握,下面我们来看看,除了 execution 方法执行切点,Spring 还为我们提供了哪些指定切点的方式

within:限定匹配特定类型的连接点。within(com.example.service.*) 匹配 com.example.service 包及其子包中所有方法。

this:限定匹配特定类型的 bean。this(com.example.service.MyService) 匹配实现 com.example.service.MyService 接口的 bean。

target:限定匹配特定类型的目标对象。target(com.example.service.MyService) 匹配目标对象是 com.example.service.MyService 的连接点。

args:限定匹配特定参数类型的方法。args(String, ..) 匹配第一个参数是 String 类型的方法。

@annotation:限定匹配特定注解的方法。@annotation(org.springframework.transaction.annotation.Transactional) 匹配标注有 @Transactional 注解的方法。

因为 within、this 和 target,都可以通过 execution 作为一定程度上的替代,所以这里我们重点关注一下匹配特定注解的方式,即 @annotation 即可。

但同时,这些表达式其实是可以共用的,比如通过这样的方式:

@Before("execution(* com.example.service.UserService.getUserById(int)) && args(userId)")
    public void logBeforeGetUserById(JoinPoint joinPoint, int userId) {
        System.out.println("Before calling getUserById with userId: " + userId);
    }

上面的 userId 参数是通过切点表达式中的 args(userId) 指定的,所以在方法体内可以直接使用 userId 参数来获取方法执行时的具体值;但如果仅仅使用 joinPoint 的话就需要 getArgs() 再拿取参数了;这里只涉及写法上的偏好,我们平时使用的大部分的内容通过 execution 和 @annotation 都是可以实现的。

正式使用

通过前面的介绍,我们已经辨析了 AOP 的基本概念,了解了控制何时执行逻辑的通知类型(Advice),定义在什么位置执行的切点表达式(PointCut),下面我们正式来尝试使用 AOP 来解决一些现实的问题。

就举一个前面提到的日志记录的 AOP 吧

定义一个简单的服务类

@Service
public class UserService {

    public String getUserById(int userId) {
        // 模拟方法体
        return "User: " + userId;
    }
}

创建日志记录的切面类

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.UserService.getUserById(int))")
    public void logBeforeGetUserById(JoinPoint joinPoint) {
		    // 获取方法参数
		    Object[] args = joinPoint.getArgs();
        System.out.println("Before calling getUserById with userId: " + args[0]);
    }

    @AfterReturning(pointcut = "execution(* com.example.service.UserService.getUserById(int))", returning = "result")
    public void logAfterReturningGetUserById(JoinPoint joinPoint, Object result) {
        System.out.println("After returning from getUserById with result: " + result);
    }
}

然后我们去做一个简单的测试,可以看到如下的输出:

Before calling getUserById with userId: 123
After returning from getUserById with result: User: 123
User retrieved: User: 123

关于 JoinPoint 和 ProceedingJoinPoint

在 Spring AOP 中,JoinPointProceedingJoinPoint 是两个重要的接口,用于在切面中获取方法执行时的信息和控制方法执行;我们在书写切面逻辑的时候,需要的大部分参数或者方法信息等,都是从这里面获取的。

JoinPoint 接口

JoinPoint 接口是 Spring AOP 提供的一个核心接口,用于描述正在执行的连接点(join point),它可以用来获取方法的签名、参数等信息,但是不能直接控制方法的执行流程。

常用方法

Signature getSignature(); // 获取代表被通知方法签名的对象,可以进一步获取方法名、声明类型等信息。

Object[] getArgs(); // 获取被通知方法的参数对象数组。

Object getTarget(); // 获取目标对象,即被通知的目标类实例。

Object getThis(); // 获取代理对象的引用,即代理对象本身。

Object[] getArgs(); // 获取调用方法时传递的参数

其中有个特殊一点的是 Signature,方法签名接口:

public interface Signature {
    String toString(); // 返回方法的字符串表示形式。

    String toShortString(); // 返回方法的简短字符串表示形式。

    String toLongString(); // 返回方法的长字符串表示形式。

    String getName();// 获取方法名。 

    int getModifiers(); // 获取方法的修饰符,返回一个整数,具体取值需要通过 java.lang.reflect.Modifier 类来解析。

    Class getDeclaringType(); // 获取声明该方法的类的 Class 对象。

    String getDeclaringTypeName(); // 获取声明该方法的类的全限定名。
}

大家可以自己写个方法测试一下,这里就不过多赘述了。

ProceedingJoinPoint 接口

ProceedingJoinPoint 接口继承自 JoinPoint 接口,它扩展了 JoinPoint 接口,提供了控制方法执行流程的能力。通常在 Around Advice 中使用 ProceedingJoinPoint 来调用目标方法,并可以控制是否继续执行该方法,以及在执行前后进行额外的处理。

常用方法

Object proceed() throws Throwable; // 继续执行连接点(即目标方法),返回方法的返回值。

Object proceed(Object[] args) throws Throwable; // 按照给定的参数继续执行连接点。

由于是继承,所以 JoinPoint 提供的方法,也都可以使用。

区别和用途

  • JoinPoint 主要用于获取方法的元数据信息,如方法名、参数等,不具备控制方法执行流程的能力。
  • ProceedingJoinPoint 继承自 JoinPoint,可以控制方法的执行流程,在 Around Advice 中使用,可以决定是否继续执行目标方法,以及在执行前后进行额外的处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/781449.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

自动化设备上位机设计 四

目录 一 设计原型 二 后台代码 一 设计原型 二 后台代码 using SimpleTCP; using SqlSugar; using System.Text;namespace 自动化上位机设计 {public partial class Form1 : Form{SqlHelper sqlHelper new SqlHelper();SqlSugarClient dbContent null;bool IsRun false;i…

【机器学习实战】Datawhale夏令营:Baseline精读笔记2

# AI夏令营 # Datawhale # 夏令营 在原有的Baseline上除了交叉验证,还有一种关键的优化方式,即特征工程。 如何优化特征,关系着我们提高模型预测的精准度。特征工程往往是对问题的领域有深入了解的人员能够做好的部分,因为我们要…

链式二叉树oj题

1.输入k ,找第k层节点个数 int TreeKlevel(BTNode*root,int k) {if (root NULL) {return 0;}if (k 1) {return 1;}return TreeKlevel(root->left, k - 1)TreeKlevel(root->right, k - 1); } 在这里我们要确定递归子问题,第一个就是NULL时返回&…

强化学习中的Q-Learning和Sarsa算法详解及实战

强化学习(Reinforcement Learning, RL)是一种通过与环境交互来学习最优策略的机器学习方法。在强化学习中,Q-Learning和Sarsa是两种重要的基于值的算法。本文将详细讲解这两种算法,并通过实际代码示例展示其应用。 1. 强化学习基…

algorithm算法库学习之——不修改序列的操作

algorithm此头文件是算法库的一部分。本篇介绍不修改序列的操作函数。 不修改序列的操作 all_ofany_ofnone_of (C11)(C11)(C11) 检查谓词是否对范围中所有、任一或无元素为 true (函数模板) for_each 应用函数到范围中的元素 (函数模板) for_each_n (C17) 应用一个函数对象到序…

Vue88-Vuex中的mapActions、mapMutations

一、mapMutations的调用 此时结果不对,因为:若是点击事件不传值,默认传的是event!,所以,修改如下: 解决方式1: 解决方式2: 不推荐,写法麻烦! 1-…

排序算法简述(第八jiang)

目录 排序 选择排序 O(n2) 不稳定:48429 归并排序 O(n log n) 稳定 插入排序 O(n2) 堆排序 O(n log n) 希尔排序 O(n log2 n) 图书馆排序 O(n log n) 冒泡排序 O(n2) 优化: 基数排序 O(n k) 快速排序 O(n log n)【分治】 不稳定 桶排序 O(n…

【图解大数据技术】Flume、Kafka、Sqoop

【图解大数据技术】Flume、Kafka、Sqoop FlumeFlume简介Flume的应用场景 KafkaKafka简介Kafka架构Flume与Kafka集成 SqoopSqoop简介Sqoop原理sqoop搭配任务调度器实现定时数据同步 Flume Flume简介 Flume是一个数据采集工具,多用于大数据技术架构下的日志采集。 …

设计模式之模版方法

模版方法介绍 模版方法(Template Method)模式是一种行为型设计模式,它定义了一个操作(模板方法)的基本组合与控制流程,将一些步骤(抽象方法)推迟到子类中,使得子类可以在…

C语言下的文件详解

主要内容 文件概述文件指针文件的打开与关闭文件的读写 文件 把输入和输出的数据以文件的形式保存在计算机的外存储器上,可以确保数据能随时使用,避免反复输入和读取数据 文件概述 文件是指一组相关数据的有序集合 文件是存储数据的基本单位&#…

# mysql 中文乱码问题分析

mysql 中文乱码问题分析 一、问题分析: MySQL 中文乱码通常是因为字符集设置不正确导致的。MySQL 有多种字符集,如 latin1、utf8、utf8mb4 等,如果在创建数据库、数据表或者字段时没有指定正确的字符集,或者在插入数据时使用了与…

关于Java异常机制及finally关键字的详解

异常机制(Exception) 软件程序在运行过程中,非常可能遇到异常问题。常见的异常: 1、用户输入错误 2、设备错误 3、硬件问题,例如打印机关掉、服务器问题 4、物理限制:磁盘满了 Java是采用面向对象的方式来处理异常的。 处理过程…

哈希表——C语言

哈希表(Hash Table)是一种高效的数据结构,能够在平均情况下实现常数时间的查找、插入和删除操作。 哈希表的核心是哈希函数,哈希函数是一个将输入数据(通常称为“键”或“key”)转换为固定长度的整数的函数…

使用vue3-treeselect问题

1.当vue3-treeselect是单选时,使用watch监听绑定value,无法监听到值清空 对照后将:value改为v-model,如图 2.使用vue3-treeselect全部清空按钮如何置空select的值,使用watch监听 多选:pageInfo.officeName(val) {// …

【Linux进阶】文件系统6——理解文件操作

目录 1.文件的读取 1.1.目录 1.2.文件 1.3.目录树读取 1.4.文件系统大小与磁盘读取性能 2.增添文件 2.1.数据的不一致(Inconsistent)状态 2.2.日志式文件系统(Journaling filesystem) 3.Linux文件系统的运行 4、文件的删…

Java--方法重写

1.方法的重写首先需要有继承关系,且为子类重写父类的方法 2.方法名必须相同 3.参数列表必须相同 4.修饰符的范围可以扩大但不能缩小,public>protected>default>private,即父类的属性可以从private改为public,但不能反过来 5.抛出…

python爬虫入门(四)之Beautiful Soup库

一、什么是Beautiful Soup库 1、Beautiful Soup库是用来做HTML解析的库 Beautiful Soup把看起来复杂的HTML内容,解析成树状结构,让搜索和修改HTML结构变得更容易 2、第三方库,先安装 终端输入pip install bs4 from bs4 import Beautiful…

Cyber Weekly #14:WAIC 2024

赛博新闻 1、WAIC2024开幕:一半机器人,一半大模型 7月4日,AI界春晚——2024世界人工智能大会(WAIC 2024)在上海开幕,大会展示了500家企业的1500项展品,突出了机器人和大模型技术。国产机器人和…

【排序算法】—— 快速排序

快速排序的原理是交换排序,其中qsort函数用的排序原理就是快速排序,它是一种效率较高的不稳定函数,时间复杂度为O(N*longN),接下来就来学习一下快速排序。 一、快速排序思路 1.整体思路 以升序排序为例: (1)、首先随…

学生管理系统(通过顺序表,获取连续堆区空间实现)

将学生的信息,以顺序表的方式存储(堆区),并且实现封装函数 : 1】顺序表的创建, 2】判满、 3】判空、 4】往顺序表里增加学生信息、 5】遍历学生信息 6】任意位置插入学生信息 7】任意位置删除学生信…