一. AOP概述
1.1 什么是 AOP
AOP 全称 Aspect Oriented Programming(面向切面编程)
什么是面向切面编程呢? 切面就是指某⼀类特定问题, 所以AOP也可以理解为,面向特定方法编程. 什么是面向特定方法编程呢? 比如上个篇博客学习的 "登录校验" , 就是⼀类特定问题. 登录校验拦截器, 就是对 "登录校验" 这类问题的统⼀处理. 所以, 拦截器也是AOP的⼀种应用. AOP是⼀种思想, 拦截器是AOP 思想的⼀种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接口. 同样的, 统⼀数据返回格式和统⼀异常处理, 也是AOP思想的⼀种实现.
总结:AOP 是一种编程思想,用来把与业务无关**、**但到处都要用的通用功能(如日志、事务、权限、统计)抽离出来统一管理,不污染业务代码。(简单来说: AOP是⼀种思想, 是对某⼀类事情的集中处理.)
1.2 什么是 Spring AOP
AOP是⼀种思想, 它的实现方法法有很多, 有Spring AOP,也有AspectJ、CGLIB等.
Spring AOP是其中的⼀种实现方式.
前面我们不是学会了Spring 统一功能处理,那我们就是学会了Spring AOP吗?---当然不是
拦截器的请求维度是一次URL(也就是一次请求和响应),@ControllerAdvice 应用场景主要是全局异常处理, 数据绑定, 数据预处理 ,但是AOP作用维度更加细致,全面,可以根据包、类、方法名、参数等进行拦截), 能够实现更加复杂的业务逻辑.
比如,当我们要记录每个方法的执行时间,我们就要在每一个方法中都加上同样的代码逻辑,这样重复的学同样的代码太够麻烦,我们这时就可以将记录执行时间的代码统一抽取出来经行管理,这就是一种AOP思想;下面我们就来看如何应用spring AOP来实现这样的功能。
二. Spring AOP 快速入门
2.1 引入依赖
在 pom.xml 文件中引入依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 编写 Spring AOP 程序
记录controller中每个方法的执行时间
java
@Slf4j
@Component//使用@Aspect必须加上@Component,将这个切面类交给Spring进行管理
@Aspect//表示一个切面
public class TimeRecordAspect {
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//这个 pjp 就是目标方法
//记录开始时间
long start=System.currentTimeMillis();
//执行目标方法
Object result=pjp.proceed();//proceed():执行目标方法 ,需要处理异常
//记录结束时间
long end=System.currentTimeMillis();
log.info(pjp.getSignature()+"执行时间为:{} ms",end-start);//getSignature():获得目标方法名
return result;
}
}
运行程序,观察日志

对程序进行简单的讲解:
- @Aspect: 标识这个类是切面类
- @Around: 环绕通知(通知类型的一种,后面会介绍到),在目标方法的前后都会被执行.后面的表达式表示对哪些方法法进行增强/作用.
- ProceedingJoinPoint.proceed() :让原始/目标方法执行
我们可以将方法分为三个部分,来理解@Around的环绕通知:

三. Spring AOP 详细
3.1 Spring AOP 基本概念
3.1.1 切点 (Pointcut)
切点(Pointcut), 也称之为"切入点" Pointcut 的作用就是提供⼀组规则,告诉程序员对哪些方法进行功能增强

其中上面的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式.
3.1.2 连接点(Join Point)
满足切点表达式规则的方法,就是连接点.也就是可以被AOP控制的方法
execution(* com.example.demo.controller.*.*(..)) 对应的 com.example.demo.controller 路径下的所有方法都是一个连接点
java
package com.example.demo.controller;
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/addBook")
public Result addBook(BookInfo bookInfo) {
//...代码省略
}
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId){
//...代码省略
}
@RequestMapping("/updateBook")
public Result updateBook(BookInfo bookInfo) {
//...代码省略
}
}
上述BookController 中的方法都是连接点
切点与连接点的联系:连接点是满足切点表达式的元素. 切点可以看做是保存了众多连接点的⼀个集合.
3.1.3 通知(Advice)
通知就是具体要做的工作, 指哪些重复的逻辑,也就是共性功能(最终体现为⼀个方法) 比如上述程序中记录业务方法的耗时时间, 就是通知.

3.1.4 切面 (Aspect)
切面 = 切点+ 通知
通过切面就能够描述当前AOP程序需要针对于哪些方法,什么时候执型什么样的操作.

切面所在的类,我们⼀般称为切面类(被 @Aspect 注解标识的类)
3.2 通知类型
上面我们讲了什么是通知,接下来学习通知的类型. @Around 就是其中⼀种通知类型, 表式环绕通知
| 注解类型 | 通知名称 | 执行时机 |
|---|---|---|
@Around |
环绕通知 | 目标方法执行前 + 执行后 |
@Before |
前置通知 | 目标方法执行前 |
@After |
后置通知 | 目标方法执行后 |
@AfterReturning |
返回后通知 | 目标方法正常返回后 |
@AfterThrowing |
异常后通知 | 目标方法抛出异常后 |
注意:@Around 通知的方法签名要求:
- 参数 :必须包含
ProceedingJoinPoint(唯一能调用proceed()的参数); - 返回值 :必须是
Object类型(或其子类,如String/Integer),不能是 void; - 异常 :必须声明
throws Throwable(因为proceed()会抛出任意异常)。
当上面这个几个注解都存在的时候,这几个注解的执行顺序是有先后顺序的,下面我们来实际运行以下,观察它们的执行顺序
java
@Slf4j
@Component
@Aspect
public class TimeRecordAspect {
//环绕通知
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
log.info("do around 前");
//执行目标方法
Object result=pjp.proceed();
log.info("do around 后");
return result;
}
//前置通知
@Before("execution(* com.example.demo.controller.*.*(..))")
public void doBefore() {
log.info("do Before ");
}
//后置通知
@After("execution(* com.example.demo.controller.*.*(..))")
public void doAfter() {
log.info("do After ");
}
//返回后通知
@AfterReturning("execution(* com.example.demo.controller.*.*(..))")
public void doAfterReturning() {
log.info("do AfterReturning ");
}
//抛出异常后通知
@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
public void doAfterThrowing() {
log.info("do AfterThrowing ");
}
}
日志:

程序正常运行的情况下(不发生异常), @AfterThrowing 标识的通知方法不会执行 从上图也可以看出来, @Around 标识的通知方法包含两部分,⼀个"前置逻辑", ⼀个"后置逻辑" . 其 中"前置逻辑" 会先于 @Before 标识的通知方法执行 , "后置逻辑" 会晚于 @After 标识的通知方法执行

下面是目标/原始方法发生异常的情况:

程序发生异常的情况下:
• @AfterReturning 标识的通知方法不会执行, @AfterThrowing 标识的通知方法执行了
• @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为 原始方法调用出异常了)
如果将错误的地方处理一下(try catch)的情况下:

异常会被 "吃掉",导致 AfterReturning 执行,而 AfterThrowing 不会执行。
| 场景 | 未加 try-catch (抛异常) | 加 try-catch (捕获异常) |
|---|---|---|
| 目标方法 | 抛出异常,中断 | 抛出异常,但被 catch 捕获 |
| @AfterReturning | ❌ 不执行 | ✅ 执行 (因为逻辑认为正常返回) |
| @AfterThrowing | ✅ 执行 | ❌ 不执行 (异常没有向上抛出) |
| @After | ✅ 执行 (finally) | ✅ 执行 (finally) |
| Around 后续 | ❌ proceed() 后的代码不执行 |
✅ proceed() 后的代码继续执行 |
如果在捕获异常后在抛出去呢:

在@Around通知的try-catch中捕获异常后重新抛出,会触发@AfterThrowing(异常通知)、执行@After(最终通知),但@AfterReturning(正常返回通知)不会执行,且@Around中抛出异常点后的代码也不再执行。
注意:
- @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行.
- @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.
- 一个切面类可以有多个切点.
- 在实际开发中, @Around 使用的更多
3.3 @PointCut
上面代码存在⼀个问题, 就是存在⼤量重复的切点表达execution(*com.example.demo.controller.*.*(..)) , Spring提供了 @PointCut 注解, 把公共的切点表达式提取出来, 需要用到时引用该切入点表达式即可.
使用@PointCut,上面代码可改为:
java
@Slf4j
@Component
@Aspect
public class AspectDeamo {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//环绕通知
@Around("pt()")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
log.info("do around 前");
//执行目标方法
Object result=pjp.proceed();
log.info("do around 后");
return result;
}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("do Before ");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("do After ");
}
//返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
log.info("do AfterReturning ");
}
//抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
log.info("do AfterThrowing ");
}
}
如果将 pt()的访问限定修饰符从 pubic 改为 private ,这个切点点就只能在当前切面类使用,如果想要在其它的切面类中使用,就需要将 private 改回 pubic,引用方式为: 全限定类名.方法名()
java
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
//前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执⾏ AspectDemo2 -> Before ⽅法");
}
}
3.4 切面优先级 @Order
当一个项目有多个切面类存在,并且这些切面类的多个切入点都匹配到了同⼀个目标方法. 当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?,下面我们通过代码来验证:(这里只展示了一个切面类,剩下两个切面类代码和它一样)

java
@Slf4j
@Component
@Aspect
public class AspectDemo1 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//环绕通知
@Around("pt()")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
log.info("do around 1 前");
//执行目标方法
Object result=pjp.proceed();
log.info("do around 1 后");
return result;
}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("do Before 1 ");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("do After 1 ");
}

通过上述程序的运行结果,可以看出:存在多个切面类时,默认按照切面类的类名字母排序:
- @Before 通知:字母排名靠前的先执行
- @After 通知:字母排名靠前的后执行
其实我们可以下面这个图,来理清当存在多个切面类时的执行顺序:

想要定义切面类的执行顺序可以通过 @Order 来定义,@Order 中数字越小,优先级越高
java
@Aspect
@Component
@Order(3)
public class AspectDemo1 {
//...代码省略
}
@Aspect
@Component
@Order(2)
public class AspectDemo2 {
//...代码省略
}
@Aspect
@Component
@Order(1)
public class AspectDemo3 {
//...代码省略
}

3.5 切点表达式
上面的代码中, 我们⼀直在使用切点表达式来描述切点. 下面我们来介绍⼀下切点表达式的语法.
切点表达式常见有两种表达方式:
- execution(......):根据方法的签名来匹配
- @annotation(......): 根据注解匹配
3.5.1 execution 表达式
execution() 是最常常的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符><返回类型> <包名.类名.方法(方法参数)> <异常> )
其中: 访问修饰符 和 异常 是可以省略的

切点表达式支持通配符表达:
| 通配符 | 作用说明 | 具体使用场景 |
|---|---|---|
| * | 匹配任意字符,只匹配一个元素 (返回类型,包,类名,方法或者方法参数) | 1. 包名使用 * 表示任意包 (一层包使用一个 *) 2. 类名使用 * 表示任意类 3. 返回值使用 * 表示任意返回值类型 4. 方法名使用 * 表示任意方法 5. 参数使用 * 表示一个任意类型的参数 |
| .. | 匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数 | 1. 使用 .. 配置包名,标识此包以及此包下的所有子包2. 可以使用 .. 配置参数,任意个任意类型的参数 |
示例1:
execution(public String com.example.demo.controller.TestController.t1())
表示:com.example.demo.controller包下的TestController类中的public修饰,返回类型为String,方法名为t1,无参方法
示例2:
execution(* com.example.demo.controller.*.*(..))
表示:com.example.demo.controller包中所有类的所有方法
示例3:
execution(String com.example.demo.controller.TestController.t1())
表示:com.example.demo.controller包下TestController类中的返回类型为String,并且方法名为t1的无参方法
补( execution 表达式中的):
- 访问修饰符:在
execution表达式中,它是方法匹配的权限条件 ------ 只有方法的实际访问修饰符与表达式中指定的一致,才会被匹配;如果省略访问修饰符,则表示匹配所有访问修饰符的方法(不限制权限)。 - 异常:不省略异常部分时,匹配的是「声明抛出指定异常或其子类异常」的方法(父异常表达式可匹配子类异常);
3.5.2 @annotation
execution表达式更适用有规则的, 如果我们要匹配多个无规则的方法呢----execution表达式就不适用了
这个时候我们使用 execution 这种切点表达式来描述就不是很方便了.我们可以借助自定义注解的方式以及另⼀种切点表达式 @annotation 来描述这⼀类的切点
使用 @annotation 可分为三步:
-
编写自定义注解
-
使用 @annotation 表达式来描述切点
-
在连接点的方法上添加自定义注解
- 编写自定义注解

自定义注解,我们选择 Annotation
java
package com.example.demo.aspect;
@Target({ ElementType.METHOD}) //定义注解为方法注解
@Retention(RetentionPolicy.RUNTIME) //注解生命周期
public @interface TimeRecord {
}
代码解释:
@Target标识了 Annotation 所修饰的对象范围,即该注解可以用在什么地方. 常用取值:
ElementType.TYPE:用于描述类、接⼝(包括注解类型)或enum声明
ElementType.METHOD:描述方法
ElementType.PARAMETER: 描述参数
ElementType.TYPE_USE:可以标注任意类型
@Retention 指Annotation被保留的时间长短,标明注解的⽣命周期
- 使用 @annotation 表达式来描述切点
java
@Slf4j
@Component
@Aspect
public class AspectDemo {
@Pointcut("@annotation(com.example.demo.aspect.TimeRecord)")
private void pt(){}
//环绕通知
@Around("pt()")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
log.info("do around 1 前");
//执行目标方法
Object result=pjp.proceed();
log.info("do around 1 后");
return result;
}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("do Before 1 ");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("do After 1 ");
}
}
- 在连接点的方法上添加自定义注解
java
@TimeRecord
@RequestMapping("/t1")
public void t1(){
System.out.println("执行目标方法");
}
表示:只有在方法上显式添加了 @TimeRecord 注解时,该方法才会被切面拦截,触发 @Around/@Before/@After 这些通知逻辑;没有加这个注解的方法,不会有任何切面行为。
查看日志:

四. Spring AOP 原理
上面我们学习了如何使用 Spring AOP,现在我们来学习Spring是如何实现 AOP的
Spring AOP是基于动态代理来实现AOP的
知道结论后,我们就需要了解两点:
- 什么是代理模式
- Spring AOP实现的是那种代理
4.1 代理模式
代理模式, 也叫委托模式.
代理模式定义:为其他对象提供⼀种代理以控制对这个对象的访问.它的作用就是通过提供⼀个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用.在某些情况下,⼀个对象不适合或者不能直接引用另⼀个对象,而代理对象可以在客户端和目标对象之间起到中介的作用.
使用代理前:

使用代理后:

代理模式的三个主要角色:
| 角色 | 核心定义 | 核心作用 | 典型示例 |
|---|---|---|---|
| 抽象主题(Subject) | 以接口 / 抽象类定义的基础约定,规定目标对象和代理对象的公共方法规范 | 保证代理与目标对象接口一致,外界调用无需区分二者(符合里氏替换原则) | 租房场景:RentHouse接口(定义rent()方法);Spring AOP:UserService的方法签名 |
| 目标对象(RealSubject) | 代理模式中真正执行业务核心逻辑的角色,是代理对象包裹的核心 | 仅专注核心业务,不处理任何额外增强逻辑 | 租房场景:房东(仅负责租房);Spring AOP:UserService实例(仅处理查询用户核心逻辑) |
| 代理对象(Proxy) | 持有目标对象引用、实现抽象主题接口,对外伪装成目标对象的核心执行者 | 1. 控制访问:校验权限 / 参数,决定是否允许访问目标对象;2. 增强功能:在目标方法前后添加日志、计时等额外逻辑 | 租房场景:中介(筛选租客 + 收中介费);Spring AOP:动态代理对象(执行计时 / 日志通知逻辑) |
代理模式可以在不修改被代理对象的基础上, 通过扩展代理类, 进行⼀些功能的附加与增强.
根据代理的创建时期,代理模式分为静态代理和动态代理.
- 静态代理:由程序员创建代理类或特定⼯具自动生成源代码再对其编译,在程序运行前代理类的 .class文件就已经存在了.
- 动态代理:在程序运行时,运用反射机制动态创建而成.
4.1.1 静态代理
静态代理是代理模式中最基础的一种实现方式,核心思想是在不修改目标对象代码的前提下,通过一个代理类来控制对目标对象的访问
之所以称为"静态",是因为代理类在编译期就已经确定(不是运行时动态生成的),代理类和目标类的关系在代码编写阶段就固定了。
4.1.2 动态代理
相比于静态代理来说,动态代理更加灵活. 我们不需要针对每个目标对象都单独创建⼀个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM来实现.也就是说动态代理在程序运行时,根据需要动态创建⽣成.
动态代理是代理模式的进阶实现方式,核心解决了静态代理 "一个目标类对应一个代理类、代码冗余" 的问题。它的关键特点是:代理类不是在编译期手动编写的,而是在程序运行时由框架(如 JDK、CGLIB)动态生成的。也就是说,你只需要编写一套通用的代理逻辑,就能代理任意多个目标类,无需为每个目标类单独写代理类。
Java也对动态代理进行了实现,并给我们提供了⼀些API,常见的实现方式有两种:
1. JDK动态代理
步骤
-
定义⼀个接口及其实现类(静态代理中的 HouseSubject 和 RealHouseSubject )
-
自定义InvocationHandler 并重写 invoke 方法,在invoke 方法中我们会调用目标方法(被代理类的方法)并自定义⼀些处理逻辑
-
通过Proxy.newProxyInstance(ClassLoader loader,Class[ ] interfaces,InvocationHandler h) 方法创建代理对象
2. CGLIB动态代理
JDK动态代理有⼀个最致命的问题是其只能代理实现了接⼝的类. 有些场景下,我们的业务代码是直接实现的,并没有接⼝定义.为了解决这个问题,我们可以用 CGLIB动态代理机制 CGLIB(Code Generation Library)是⼀个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态⽣成. CGLIB 通过继承方式实现代理,很多知名的开源框架都使用到了CGLIB.
步骤:
-
定义⼀个类(被代理类)
-
自定义 MethodInterceptor 并重写 intercept 方法, intercept 用于增强目标方法,和 JDK 动态代理中的 invoke 方法类似 3. 通过 Enhancer 类的 create()创建代理类
| 代理类型 | 实现原理 | 核心限制 |
|---|---|---|
| JDK 动态代理 | 基于 Java 反射,生成实现目标接口的代理类 | 只能代理接口,无法直接代理无接口的普通类 |
| CGLIB 动态代理 | 基于 ASM 字节码框架,生成目标类的子类 | 可代理接口 / 类,但无法代理 final 类 /final 方法(无法继承 / 重写) |
4.2 Spring AOP实现的是那种代理
Spring AOP是基于动态代理来实现AOP的
而 JDK 与 CGLIB 这两种动态代理实现,Spring AOP 都在使用
只不过 Spring AOP 默认使用CGLIB代理
JDK与CGLIB的使用条件:
- JDK只能代理接口
- CGLIB 即能代理接口,也能代理类
Spring AOP 具体使用那种代理是可以配置的-----spring.aop.proxy-target-class=true/false
| 配置值 | 代理方式 | 核心规则 |
|---|---|---|
false |
JDK 动态代理 | ① 目标类实现了接口 → 用 JDK 动态代理; ② 目标类无接口 → 自动降级为 CGLIB 代理 |
true |
CGLIB 动态代理 | 强制使用 CGLIB 代理(无论目标类是否有接口) |
补:如果强制让JDK代理类,会报错