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

news2024/11/22 16:20:05

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

在准备面试和学习的过程中,我阅读了还算多的源码,比如 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 中使用,可以决定是否继续执行目标方法,以及在执行前后进行额外的处理。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PXIe-7976【K410T】

起售价 RMB 152,880.00 块RAM(BRAM): 28620 kbit 动态RAM(DRAM): 2 GB FPGA: Kintex-7 410T PXI背板链路: PCI-Express Gen2 x 8 FPGA片: 63550 DSP片: 1540

240707-Sphinx配置Pydata-Sphinx-Theme

Step A. 最终效果 Step B. 为什么选择Pydata-Sphinx-Theme主题 Gallery of sites using this theme — PyData Theme 0.15.4 documentation Step 1. 创建并激活Conda环境 conda create -n rtd_pydata python3.10 conda activate rtd_pydataStep 2. 安装默认的工具包 pip in…

基于Java的水果商品销售网站

1 水果商品销售网站概述 1.1 课题简介 随着电子商务在当今社会的迅猛发展,水果在线销售已逐渐演变为一种极为便捷的购物方式,日益受到人们的青睐。本系统的设计初衷便是构建一个功能完备、用户体验友好的水果销售平台,致力于为用户提供优质、…

入门PHP就来我这(高级)12 ~ 获取数据

有胆量你就来跟着路老师卷起来! -- 纯干货,技术知识分享 路老师给大家分享PHP语言的知识了,旨在想让大家入门PHP,并深入了解PHP语言。 1 从结果集中获取一行作为对象 表中数据行如下: 利用mysqli_fetch_array()函数获…

SLF4J的介绍与使用(有logback和log4j2的具体实现案例)

目录 1.日志门面的介绍 常见的日志门面 : 常见的日志实现: 日志门面和日志实现的关系: 2.SLF4J 的介绍 业务场景(问题): SLF4J的作用 SLF4J 的基本介绍 日志框架的绑定(重点&#xff09…

【CSS in Depth 2精译】2.5 无单位的数值与行高

当前内容所在位置 第一章 层叠、优先级与继承第二章 相对单位 2.1 相对单位的威力2.2 em 与 rem2.3 告别像素思维2.4 视口的相对单位2.5 无单位的数值与行高 ✔️2.6 自定义属性2.7 本章小结 2.5 无单位的数值与行高 有些属性允许使用无单位的数值(unitless value…

Linux:DHCP服务配置

目录 一、DHCP概述以及DHCP的好处 1.1、概述 1.2、DHCP的好处 二、DHCP的模式与分配方式 2.1、模式 2.2、DHCP的分配方式 三、DHCP工作原理 四、安装DHCP服务 五、DHCP局部配置并且测试 5.1、subnet 网段声明 5.2、客户机预留指定的固定ip地址 一、DHCP概述以及DHCP…

【Java13】包

“包”这个机制,类似于分组。主要作用是区分不同组内的同名类。例如,高三三班有一个“王五”,高二八班也有一个“王五”。高三三班和高三八班就是两个不同的包。 Java中的包(package)机制主要提供了类的多层命名空间&…

驾校管理系统设计

驾校管理系统设计旨在提高驾校运营效率、学员管理、教练安排、考试预约、财务结算等方面的能力。以下是一个基本的设计框架,包括关键模块和数据表设计: 1. 系统架构设计 前端界面:提供给学员、教练和管理员使用的Web界面或移动应用&#xf…

51单片机STC89C52RC——16.1 五项四线步进电机

目的/效果 让步进电机 正向转90度,逆向转90度 一,STC单片机模块 二,步进电机 2.2 什么是步进电机? 步进电机可以理解为:是一个按照固定步幅运动的“小型机器”。它与普通电机不同点在于,普通电机可以持…

插入排序——C语言

假设我们现在有一个数组,对它进行排序,插入排序的算法如同它的名字一样,就是将元素一个一个插入到合适的位置,那么,该如何做呢? 如果我们要从小到大进行排序的话,步骤如下: 1.对于…

WAWA鱼曲折的大学四年回忆录

声明:本文内容纯属个人主观臆断,如与事实不符,请参考事实 前言: 早想写一下大学四年的总结了,但总是感觉无从下手,不知道从哪里开始写,通过这篇文章主要想做一个记录,并从现在的认…

大数据之路 读书笔记 Day4 数据同步

回顾: Day 3 总结了无限客户端的日志采集 大数据之路 读书笔记 Day 3Day 2总结了浏览器端的日志采集 大数据之路 读书笔记 Day 2 数据同步 阿里数据体系中的数据同步,主要指的是在不同的数据存储系统之间进行数据的传输与更新,以保证数据的一…

自用款 复制粘贴工具 Paste macOS电脑适配

Paste是一款专为Mac和iOS用户设计的剪贴板管理工具,它提供了强大的剪贴板增强功能。Paste能够实时记录用户复制和剪切的内容,包括文本、图片、链接等多种数据类型,并形成一个可视化的剪贴板历史记录,方便用户随时访问和检索。此外…

【密码学】密码学中的四种攻击方式和两种攻击手段

在密码学中,攻击方式通常指的是密码分析者试图破解加密信息或绕过安全机制的各种策略。根据密码分析者对明文、密文以及加密算法的知识程度,攻击可以分为以下四种基本类型: 一、四种攻击的定义 (1)唯密文攻击(COA, C…

蚂蚁全媒体总编刘鑫炜谈新媒体时代艺术家如何创建及提升个人品牌

新媒体时代艺术家如何创建及提升个人品牌形象——专访蚂蚁全媒体总编刘鑫炜 图为蚂蚁全媒体总编刘鑫炜 在新媒体风潮席卷全球的今天,传统艺术与新媒体技术的融合越来越紧密。这种变革不仅改变了艺术作品的呈现方式,也给艺术家们提供了更多的可能性。那么…

python 10个自动化脚本

目录 🌟 引言 📚 理论基础 🛠️ 使用场景与代码示例 场景一:批量重命名文件 场景二:自动下载网页内容 场景三:数据清洗 场景四:定时执行任务 场景五:自动化邮件发送 场景六…

【面向就业的Linux基础】从入门到熟练,探索Linux的秘密(十二)-管道、环境变量、常用命令

大致介绍了一下管道、环境变量、一些常用的基本命令,可以当作学习笔记收藏学习一下!!! 文章目录 前言 一、管道 二、环境变量 1.概念 2.查看 3.修改 4.常用环境变量 三、系统状况 总结 前言 大致介绍了一下管道、环境变量、一些常…

【因果推断】优惠券政策对不同店铺的影响

这次依然是用之前rossmann店铺竞赛的数据集。 之前的数据集探索处理在这里已经做过了,此处就不再赘述了CSDN链接 数据集地址:竞赛链接 这里探讨数据集中Promo2对于每家店铺销售额的影响。其中,Promo2是一个基于优惠券的邮寄活动,发…

ZYNQ-LINUX环境C语言利用Curl库实现HTTP通讯

前言 在Zynq-Linux环境中,需要使用C语言来编写APP时,访问HTTP一般可以使用Curl库来实现,但是在Zynq的SDK中,并没有集成该库,在寻找了很多资料后找到了一种使用很方便的额办法。这篇文章主要记录一下移植Curl的过程。 …