【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 {@Overridepublic 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:可选,方法的访问修饰符,如
public、protected、private。通常省略不写,表示任意访问修饰符。 - ret-type-pattern:方法的返回类型模式,例如
void、String、``(任意返回类型)。 - declaring-type-pattern:可选,方法所在类的全限定名称模式,如
com.example.service.*。指定要匹配的类或包。 - name-pattern:方法名称模式,如
Service、get*。支持通配符 ``(匹配任意字符序列)和..(匹配任意数量和类型的参数)。 - 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 中,
JoinPoint和ProceedingJoinPoint是两个重要的接口,用于在切面中获取方法执行时的信息和控制方法执行;我们在书写切面逻辑的时候,需要的大部分参数或者方法信息等,都是从这里面获取的。
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 中使用,可以决定是否继续执行目标方法,以及在执行前后进行额外的处理。
相关文章:
【Spring AOP 源码解析前篇】什么是 AOP | 通知类型 | 切点表达式| AOP 如何使用
前言(关于源码航行) 在准备面试和学习的过程中,我阅读了还算多的源码,比如 JUC、Spring、MyBatis,收获了很多代码的设计思想,也对平时调用的 API 有了更深入的理解;但过多散乱的笔记给我的整理…...
Laravel HTTP客户端:网络请求的瑞士军刀
标题:Laravel HTTP客户端:网络请求的瑞士军刀 Laravel的HTTP客户端是一个功能强大的工具,它提供了一种简洁、直观的方式来发送HTTP请求。无论是与外部API集成,还是进行网络数据抓取,Laravel的HTTP客户端都能满足你的需…...
7月07日,每日信息差
第一、6 月份,北京、上海、广州和深圳的新建商品住宅成交量分别环比增加 21%、66%、48% 和 38%,均创年内新高 第二、2024 年世界人工智能大会上,上海向四家企业发放了首批无驾驶人智能网联汽车示范应用许可,这些企业可以在浦东部…...
ubuntu 网络常用命令
在Ubuntu中,有许多网络相关的常用命令。以下是一些主要命令及其用途: ifconfig:此命令用于显示和配置网络接口信息。你可以使用它来查看IP地址、子网掩码、广播地址等。 例如:ifconfig 注意:在新版本的Linux发行版中…...
Python28-7.4 独立成分分析ICA分离混合音频
独立成分分析(Independent Component Analysis,ICA)是一种统计与计算技术,主要用于信号分离,即从多种混合信号中提取出独立的信号源。ICA在处理盲源分离(Blind Source Separation,BSS࿰…...
Spring Boot与Okta的集成
Spring Boot与Okta的集成 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将探讨如何在Spring Boot应用中集成Okta,实现身份认证和授权的功能…...
MVC(Model-View-Controller)模式
MVC(Model-View-Controller)模式三个主要组件:模型(Model),视图(View),和控制器(Controller): 模型(Model)&a…...
MuLan:模仿人类画家的多对象图像生成
在图像生成领域,处理包含多个对象及其空间关系、相对大小、重叠和属性绑定的复杂提示时,现有的文本到图像模型仍面临挑战:当文本提示中包含多个对象,并且这些对象之间存在特定的空间关系时,现有模型往往难以准确地捕捉…...
如何在Android中实现网络通信,如HttpURLConnection和HttpClient。
在Android开发中,网络通信是一个不可或缺的功能,它允许应用与服务器交换数据,实现丰富的功能。在实现网络通信时,HttpURLConnection和HttpClient是两种常用的方式。下面将从技术难点、面试官关注点、回答吸引力以及代码举例四个方…...
评价ChatGPT与强人工智能的未来
在人工智能领域,ChatGPT的出现无疑是一个里程碑事件。它不仅展示了自然语言处理技术的巨大进步,也引发了人们对于强人工智能(AGI)的无限遐想。本文将从多个角度评价ChatGPT,并探讨强人工智能距离我们还有多远。 ChatGP…...
【web前端HTML+CSS+JS】--- CSS学习笔记02
一、CSS(层叠样式表)介绍 1.优势 2.定义解释 如果有多个选择器共同作用的话,只有优先级最高那层样式决定最终的效果 二、无语义化标签 div和span:只起到描述的作用,不带任何样式 三、标签选择器 1.标签/元素选择器…...
linux 安装 ImageMagick 及 php imagick扩展
安装imagick扩展前必须安装ImageMagick 一、安装ImageMagick wget http://www.imagemagick.org/download/ImageMagick.tar.gz 上面如果报错(cannot verify download.imagemagick.org’s certificate)执行 sudo yum install -y ca-certificates tar zxv…...
秋招突击——7/5——复习{}——新作{跳跃游戏II、划分字母区间、数组中的第K个大的元素(模板题,重要)、前K个高频元素}
文章目录 引言正文贪心——45 跳跃游戏II个人实现参考实现 划分字母区间个人实现参考实现 数组中的第K个最大元素个人实现参考做法 前K个高频元素个人实现参考实现 总结 引言 今天就开始的蛮早的,现在是九点多,刚好开始做算法,今天有希望能够…...
【Linux】信号的处理
你很自由 充满了无限可能 这是很棒的事 我衷心祈祷你可以相信自己 无悔地燃烧自己的人生 -- 东野圭吾 《解忧杂货店》 信号的处理 1 信号的处理2 内核态 VS 用户态3 键盘输入数据的过程4 如何理解OS如何正常的运行5 如何进行信号捕捉信号处理的总结6 可重入函数volatile关…...
Python数据分析的数据导入和导出
在Python数据分析中,数据的导入和导出是非常关键的步骤。这些步骤通常涉及到将数据从外部文件(如CSV、Excel、数据库等)读入到Python程序中,以及将处理后的数据导出回外部文件或数据库。以下是一些常用的库和方法来实现这些操作。…...
【JAVA多线程】线程池概论
目录 1.概述 2.ThreadPoolExector 2.1.参数 2.2.新任务提交流程 2.3.拒绝策略 2.4.代码示例 1.概述 线程池的核心: 线程池的实现原理是个标准的生产消费者模型,调用方不停向线程池中写数据,线程池中的线程组不停从队列中取任务。 实现…...
java双亲委派机制
Java中的双亲委派机制(Parent Delegation Model)是一种类加载机制,它确保了类加载的安全性和一致性。该机制规定了类加载器在加载类时的顺序和方式,从而避免了重复加载和类冲突问题。 以下是一个简单的自定义类加载器的示例&#…...
记录第一次使用air热更新golang项目
下载 go install github.com/cosmtrek/airlatest 下载时提示: module declares its path as: github.com/air-verse/air but was required as: github.com/cosmtrek/air 此时,需要在go.mod中加上这么一句: replace github.com/cosmtrek/air &…...
Leetcode 3213. Construct String with Minimum Cost
Leetcode 3213. Construct String with Minimum Cost 1. 解题思路2. 代码实现 题目链接:3213. Construct String with Minimum Cost 1. 解题思路 这一题的话思路上还是比较直接的,就是一个trie树加一个动态规划,通过trie树来快速寻找每一个…...
python操作SQLite3数据库进行增删改查
python操作SQLite3数据库进行增删改查 1、创建SQLite3数据库 可以通过Navicat图形化软件来创建: 2、创建表 利用Navicat图形化软件来创建: 存储在 SQLite 数据库中的每个值(或是由数据库引擎所操作的值)都有一个以下的存储类型: NULL. 值是空值。 INTEGER. 值是有符…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...
