文章目录
-
- 一、execution 切点表达式
-
- 1. 语法格式
- 2. 通配符规则
- 二、AOP 基础概念
-
- 1. AOP 是什么
- 2. 核心术语
- 3. 五种通知类型
- 4. 多切面执行顺序
- 5. 正常 / 异常时的执行流程
- 三、Spring AOP 实现原理
-
- 1. 代理模式:代理对象持有目标对象
- 2. 两种动态代理方式
- 3. SpringBoot 2.x 的代理选择规则
- 四、全文总结
- 五、核心知识点复盘
- 六、常见问题 / 避坑指南
面向切面编程(AOP)是 Spring 里绕不开的核心能力。很多人会「照葫芦画瓢」地写切面,但对 execution 表达式到底怎么匹配、五种通知分别在什么时候执行、底层又是靠什么机制拦下来的,往往一知半解。
这篇文章会从最常用的切点表达式讲起,再串起 AOP 的基础概念,最后揭开动态代理的底层原理,配合可直接运行的完整代码,带你真正把 AOP 吃透。文中所有代码均来自一个可运行的 Spring Boot 工程。
一、execution 切点表达式
切点(Pointcut)决定了 AOP 到底要「拦」哪些方法。Spring AOP 里最常用、也最强大的切点表达式,就是 execution()。
1. 语法格式
execution() 用来精确匹配目标方法,完整语法如下:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中:
- 访问修饰符(如 public、private)和 异常声明 两部分可以省略;
- 返回类型、包名、类名、方法名、参数列表是核心,必须写清楚。
一个最典型的例子,也是本项目切面里实际用到的:
execution(* com.zmt.aop.controller.*.*(..))
拆开看,从左到右:
| 第一个 * | 任意返回类型 |
| com.zmt.aop.controller | 固定的包路径 |
| 第二个 * | 该包下的任意类 |
| 第三个 * | 类里的任意方法 |
| (..) | 任意个数、任意类型的参数 |
连起来读就是:匹配 com.zmt.aop.controller 包下,所有类的所有方法,不管返回什么、不管传什么参数。
2. 通配符规则
execution 表达式里有两个「万能」通配符,理解了它们就掌握了 90% 的切点写法。
(1)* —— 匹配任意单个元素
* 只能匹配「一段」,可以出现在返回类型、包名、类名、方法名、参数等位置:
* 匹配任意返回类型
com.zmt.aop.* 匹配任意一层包(aop 的下一级包)
com.zmt.aop.controller.* 匹配该包下的任意类
*.*(..) 任意类的任意方法
注意:包名里用 * 只代表一层包。例如 com.*.controller 只能匹配 com.zmt.controller 这种「两层」结构,不能匹配 com.a.b.controller 这种更深的路径。
(2).. —— 匹配多个连续的任意符号
.. 是「全能选手」,用在两个地方:
- 包路径中:代表「当前包及其所有子包」;
- 参数列表中:代表「任意个数、任意类型的参数」。
execution(* com.zmt.aop..*(..)) 匹配 aop 包及其所有子包里的任意方法
execution(* com.zmt..User.*(..)) 匹配 com.zmt 任意层级下 User 类的任意方法
execution(* *(..)) 匹配所有类的所有方法(慎用,范围过大)
常见组合示例:
// 1. 匹配 controller 包下任意类的任意方法(本项目切点)
execution(* com.zmt.aop.controller.*.*(..))
// 2. 匹配所有 public 方法
execution(public * *(..))
// 3. 匹配返回值为 String 的方法
execution(String com.zmt.aop.controller.*.*(..))
// 4. 匹配只有一个参数、参数类型为 String 的方法
execution(* com.zmt.aop.controller.*.*(String))
// 5. 匹配方法名以 save 开头的方法
execution(* com.zmt.aop..*.save*(..))
小结:记一句话——* 管「一段」,.. 管「一串」。包路径里 * 是一层,.. 是任意层;参数里 * 是一个,.. 是任意个。
二、AOP 基础概念
写切面之前,得先把 AOP 这套「行话」理顺,否则看别人的切面代码会一头雾水。
1. AOP 是什么
AOP(Aspect Oriented Programming,面向切面编程),和 OOP(面向对象编程)是互补关系,而不是替代关系。
OOP 擅长把业务按「对象」组织,但有些需求会横向贯穿很多类——比如:
- 登录鉴权(几乎每个接口都要检查)
- 统一返回数据格式(每个 Controller 都要包一层 Result)
- 统一异常处理(每个方法都可能抛异常)
- 方法耗时统计、日志打印、事务管理……
这些「和业务逻辑无关、但到处都要写」的代码,如果手写在每个方法里,会造成大量重复和耦合。AOP 的思路是:把这些共性逻辑集中到一个地方,统一处理,然后在需要的地方「自动织入」。
Spring 框架提供了 AOP 的拦截器技术(动态代理,第三节细讲),把这种思想落到了实处。日常开发里最典型的应用就是 Spring 的 @Transactional 事务、@Cacheable 缓存,它们的本质都是 AOP。
2. 核心术语
AOP 有几个绕不开的名词,逐个说清楚:
| 切面(Aspect) | 封装共性逻辑的类,用 @Aspect 标识 | 「干活的模块」 |
| 切点(Pointcut) | 匹配目标方法的规则,决定 AOP 作用范围 | 「在哪儿干活」 |
| 连接点(JoinPoint) | 程序中被拦截的具体目标方法 | 「被干的活」 |
| 通知(Advice) | 切面里具体执行的逻辑及其时机 | 「什么时候、干什么活」 |
用通俗类比再顺一遍:
切点 = 计算机专业的全体学生(一条筛选规则);连接点 = 具体的学生张三、李四(被规则命中的一个个实例);通知 = 9 月份开学(具体要做的事);切面 = 整个「开学」这件事的统一处理逻辑。
落到代码上,一个完整的切面长这样:
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 1. 声明这是一个切面
@Component // 2. 交给 Spring 容器管理
public class LogAspect {
// 3. 定义切点:匹配 controller 包下所有方法
@Pointcut("execution(* com.zmt.aop.controller.*.*(..))")
private void pt() {}
// 4. 通知:把具体逻辑绑定到这个切点上
@Before("pt()")
public void doBefore() {
System.out.println("前置通知:目标方法执行前调用");
}
}
- @Aspect 标识切面类,@Component 让它被 Spring 扫描到(二者缺一不可);
- @Pointcut 定义切点规则,方法体为空、只是承载表达式;
- @Before("pt()") 里的 pt() 直接引用上面定义的切点,避免表达式重复书写。
3. 五种通知类型
通知的核心,是它在目标方法的哪个阶段执行。Spring AOP 一共支持 5 种:
| @Before | 目标方法执行前 | 不受影响 | 参数校验、权限检查 |
| @AfterReturning | 目标方法正常返回后 | 抛异常则不执行 | 记录成功结果 |
| @AfterThrowing | 目标方法抛出异常时 | 只有异常才执行 | 异常日志、告警 |
| @After | 目标方法执行后 | 无论如何都执行(类似 finally) | 清理资源、收尾 |
| @Around | 目标方法执行前后 | 可控制是否执行目标方法 | 耗时统计、事务、缓存 |
重点区分三对容易混淆的:
- @After vs @AfterReturning:@After 无论正常还是异常都会执行,@AfterReturning 只有正常返回才执行。可以理解为 @After 是「兜底」,@AfterReturning 是「庆功」。
- @After vs @AfterThrowing:@After 异常时也执行,@AfterThrowing 只有异常时才执行。
- @Around 最特殊:它既能做前置也能做后置,还能决定目标方法到底要不要执行,这是其他四种通知做不到的。
完整的五种通知示例:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
@Pointcut("execution(* com.zmt.aop.controller.*.*(..))")
private void pt() {}
// 环绕通知:方法执行前后都会执行,还能控制目标方法是否执行
@Around("pt()")
public Object doAround(ProceedingJoinPoint pjp) {
log.info("doAround 前");
Object result = null;
try {
// 关键:调用 proceed() 才会真正执行目标方法
result = pjp.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
log.info("doAround 后");
}
// 关键:必须把目标方法的返回值返回给调用方
return result;
}
@Before("pt()")
public void doBefore() {
log.info("doBefore …");
}
@After("pt()")
public void doAfter() {
log.info("doAfter …");
}
@AfterReturning("pt()")
public void doAfterReturning() {
log.info("doAfterReturning …");
}
@AfterThrowing("pt()")
public void doAfterThrowing() {
log.info("doAfterThrowing …");
}
}
两个必踩的坑(@Around 专属):
4. 多切面执行顺序
一个方法可能同时被多个切面拦截,谁先谁后?用 @Order(数值) 控制,数字越小,优先级越高,越「靠外」执行。
本项目用三个切面演示(AspectDemo2、AspectDemo3、AspectDemo4):
// 优先级最高,最先执行前置、最后执行后置
@Aspect
@Component
@Order(–1)
public class AspectDemo2 {
@Before("execution(* com.zmt.aop.controller.*.*(..))")
public void doBefore() { log.info("AspectDemo2 doBefore …"); }
@After("execution(* com.zmt.aop.controller.*.*(..))")
public void doAfter() { log.info("AspectDemo2 doAfter …"); }
}
// @Order(1)
public class AspectDemo4 { /* 同上的 doBefore / doAfter */ }
// @Order(5),优先级最低
public class AspectDemo3 { /* 同上的 doBefore / doAfter */ }
执行顺序是嵌套的,像洋葱一样一层套一层:
AspectDemo2(-1) 前置
AspectDemo4(1) 前置
AspectDemo3(5) 前置
目标方法
AspectDemo3(5) 后置
AspectDemo4(1) 后置
AspectDemo2(-1) 后置
可以看到:数字越小越先「进门」,越后「出门」。这个「嵌套」模型,和下文第三节「动态代理层层包裹」的实现是对应的。
5. 正常 / 异常时的执行流程
把所有通知放到一起,目标方法的完整生命周期如下。
(1)方法正常执行(无异常):
@Around 前 → @Before → 目标方法 → @AfterReturning → @After → @Around 后
(2)方法抛出异常:
@Around 前 → @Before → 目标方法(抛出异常) → @AfterThrowing → @After → @Around 后
对比两个流程,规律很清晰:
一句话记忆:@Around 是「外壳」,@Before 打头阵,@After 收尾兜底,中间「返回」和「异常」二选一。
三、Spring AOP 实现原理
搞清楚「怎么写」之后,必须弄明白「为什么能拦下来」。Spring AOP 的底层就是动态代理,而动态代理又依托 Java 的反射机制。
1. 代理模式:代理对象持有目标对象
先看「代理」这个设计模式。核心是:调用方实际调用的是代理对象,代理对象在内部持有目标对象,由代理完成「切面逻辑 + 调用目标方法」的拼接。
用「房屋中介」类比最形象:
- 房东(目标对象)负责真正的出租/出售房子;
- 中介(代理对象)在房东前后加上「带看、谈价、收中介费」等逻辑;
- 租客(调用方)找的是中介,而不是直接找房东。
先定义「房东」的接口和实现:
// 接口:规定了中介和房东都要做的事
public interface HouseSubject {
void rentHouse();
void saleHouse();
}
// 目标对象:真正的房东
public class RealHouseSubject implements HouseSubject {
@Override
public void saleHouse() {
System.out.println("我是房东,我出售房子");
}
@Override
public void rentHouse() {
System.out.println("我是房东,我出租房子");
}
}
静态代理——手写一个代理类,固定实现接口、在方法前后写死逻辑:
// 静态代理:代理类在编译期就写好了,和目标类一一对应
public class HouseProxy implements HouseSubject {
private HouseSubject realHouse; // 持有目标对象
public HouseProxy(HouseSubject realHouse) {
this.realHouse = realHouse;
}
@Override
public void rentHouse() {
System.out.println("我是中介,我开始代理"); // 前置增强
realHouse.rentHouse(); // 调用目标方法
System.out.println("我是中介,我结束代理"); // 后置增强
}
@Override
public void saleHouse() {
System.out.println("我是中介,我开始代理");
realHouse.saleHouse();
System.out.println("我是中介,我结束代理");
}
}
静态代理的痛点很明显:每多一个目标类/接口,就要手写一个代理类;方法多了,每个都要重复写前后逻辑。所以有了动态代理。
2. 两种动态代理方式
动态代理的核心区别在于:代理类不是在编译期手写的,而是在运行时通过反射动态生成,因此能灵活适配任意目标类。Java 生态里有两种主流实现。
(1)JDK 动态代理 —— 只能代理「实现了接口」的类
JDK 自带,通过 java.lang.reflect.Proxy + InvocationHandler 实现。它生成的代理对象必须实现某个接口,调用方通过接口类型来使用代理。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// 目标对象
HouseSubject target = new RealHouseSubject();
// 动态生成代理对象(参数:类加载器、要实现的接口、处理器)
HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
new Class[]{HouseSubject.class}, // 目标实现的接口
new JDKInvocationHandler(target) // 处理器
);
proxy.rentHouse();
proxy.saleHouse();
}
}
// 处理器:拦截所有方法调用,统一加前后逻辑
class JDKInvocationHandler implements InvocationHandler {
private Object target; // 持有目标对象
public JDKInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// proxy:代理对象;method:被调用的方法;args:方法参数
System.out.println("我是代理,我开始代理");
Object result = method.invoke(target, args); // 通过反射执行目标方法
System.out.println("我是代理,我结束代理");
return result;
}
}
(2)Cglib 动态代理 —— 接口有无都能代理
Cglib 通过生成目标类的子类来代理,因此不仅能代理实现接口的类,也能代理没有实现接口的普通类。Spring 内置了重打包的 Cglib(org.springframework.cglib),无需额外依赖。
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class Main {
public static void main(String[] args) {
HouseSubject target = new RealHouseSubject();
// 通过 Enhancer 生成目标类的子类代理
HouseSubject proxy = (HouseSubject) Enhancer.create(
target.getClass(), // 目标类
new CglibMethodInterceptor(target) // 拦截器
);
proxy.rentHouse();
proxy.saleHouse();
}
}
class CglibMethodInterceptor implements MethodInterceptor {
private Object target;
public CglibMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("我是代理,我开始代理");
Object result = proxy.invoke(target, args); // 执行目标方法
System.out.println("我是代理,我结束代理");
return result;
}
}
两者本质区别一句话总结:
| 实现原理 | 基于接口,生成接口实现类 | 基于继承,生成目标类的子类 |
| 能否代理普通类 | ❌ 必须实现接口 | ✅ 接口有无均可 |
| 性能 | 早期稍慢,如今差距不大 | 生成代理时稍慢,运行快 |
| 限制 | 无 | 无法代理 final 类/方法、private 方法 |
3. SpringBoot 2.x 的代理选择规则
Spring 具体用哪种代理,由 proxyTargetClass 配置项控制(对应 @EnableAspectJAutoProxy 的 proxyTargetClass 属性):
| false | 使用 JDK 代理 | 使用 Cglib 代理 |
| true | 使用 Cglib 代理 | 使用 Cglib 代理 |
即:false 时「有接口优先 JDK,无接口才 Cglib」;true 时「无论有没有接口,统一用 Cglib」。
补充一点:Spring Boot 2.x 开始,默认就开启了 proxyTargetClass = true,也就是默认统一走 Cglib 代理。所以现在大多数 Spring Boot 项目里,即使你的 Service 实现了接口,实际拦下来用的也是 Cglib。
回到「房屋中介」的类比,把两种代理的区别对应起来:
- 静态代理:类似某个房东固定签约的专属中介,每来一个房东都得专门配一个,死板;
- 动态代理:类似平台上架房源后,任意中介都能带客户看房,运行时灵活生成、自动适配,这就是 AOP 能「无侵入」地给海量方法加逻辑的原因。
四、全文总结
这篇文章沿着「怎么匹配 → 怎么使用 → 为什么能生效」三条主线,把 Spring AOP 讲了一遍:
AOP 的价值在于「横向关注点集中处理」——把登录校验、统一返回、异常处理、耗时统计这类横切逻辑从业务代码里剥离出来,让业务方法只关心业务本身。理解它,是读懂 Spring 事务、缓存、日志切面等大量基础设施的前提。
五、核心知识点复盘
| execution 语法 | execution(访问修饰符? 返回类型 包名.类名.方法(参数) 异常?),修饰符和异常可省略 |
| * 通配符 | 匹配任意单个元素;包路径里代表一层包 |
| .. 通配符 | 匹配任意连续元素;包路径里代表当前及所有子包,参数里代表任意参数 |
| 五种通知 | @Before 前、@After 后(必执行)、@AfterReturning 正常返回后、@AfterThrowing 异常时、@Around 环绕可拦截 |
| 通知执行顺序 | 无异常:Around前→Before→方法→AfterReturning→After→Around后 |
| 异常执行顺序 | Around前→Before→方法(异常)→AfterThrowing→After→Around后 |
| 多切面顺序 | @Order 数字越小优先级越高,嵌套执行 |
| JDK vs Cglib | JDK 基于接口,Cglib 基于继承(子类),Cglib 可代理无接口的类 |
| proxyTargetClass | false 有接口走 JDK;true 统一 Cglib;Spring Boot 2.x 默认 true |
六、常见问题 / 避坑指南
切面写了却不生效:九成是切点表达式写错——包路径拼错、类名/方法名大小写不符、漏了 * 或 ..。先用最宽的表达式(如 execution(* *(..)))验证切面本身没问题,再逐步收窄定位。
忘了加 @Component 或没开启 AOP:切面类既要 @Aspect 也要 @Component 交给容器管理;纯 Spring 环境还需 @EnableAspectJAutoProxy(Spring Boot 自动配置一般已开启,可不用手动加)。
@Around 忘写 pjp.proceed() 或没 return:前者导致目标方法压根不执行,后者导致调用方拿到 null。@Around 必须用 ProceedingJoinPoint 作为参数。
同类内部方法互调不生效:在一个类里 this.methodB() 调用本类的另一个方法,走的是 this 引用而不是代理对象,AOP 拦不到。这是 AOP 最经典、也最隐蔽的坑,需要通过注入代理对象或拆分 Service 等方式规避。
Cglib 拦不住 final/private/static 方法:Cglib 靠继承生成子类,final 类、final 方法、private 方法都无法被代理;JDK 代理则要求类必须实现接口。
@AfterReturning / @AfterThrowing 拿不到返回值或异常:想拿到目标方法的返回值或抛出的异常,需要配合 returning / throwing 属性显式绑定参数名,且通知方法参数名必须与之保持一致。
@After 和 @AfterReturning 搞混:@After 无论如何都执行(兜底),@AfterReturning 只有正常返回才执行。资源清理、收尾逻辑用 @After,成功后的业务处理用 @AfterReturning。



