核心概念 → 代理原理 → 通知类型与执行顺序 → 切点表达式(execution/annotation)→ 连接点JoinPoint/ProceedingJoinPoint → 常见实战与坑。默认以 Spring Boot / Spring AOP 为主
1)AOP 到底解决什么:横切关注点
业务代码只关心“做什么”(下单、扣库存),但系统还需要很多“每个接口都要做”的事:
- 日志(请求/响应、耗时、TraceId)
- 权限校验
- 参数校验(部分也可用 AOP)
- 缓存
- 限流
- 事务(
@Transactional就是一个 AOP 切面) - 监控埋点
这些逻辑分散写在每个方法里会:
- 重复、臃肿
- 修改成本高
- 容易漏
AOP 的做法:在不改业务方法代码的前提下,统一在“方法执行前/后/异常时”插入增强逻辑。(方法增强)
2)Spring AOP 的本质:代理(Proxy)
Spring AOP 不是魔法,它的核心就是:把你的 Bean 替换成一个代理对象。
调用链变成:
调用方 → 代理对象 → (执行切面逻辑) → 真实目标方法 → (执行切面逻辑) → 返回
2.1 两种代理方式(非常重要)
JDK 动态代理(默认优先)
- 前提:目标类有接口
- 代理的是“接口类型”
CGLIB 代理
- 目标类没接口,或强制开启
proxyTargetClass=true - 通过生成目标类的子类来代理
影响:有时你
@Autowired用接口能注入,强转实现类会报错;或者 final 方法无法被 CGLIB 覆盖导致 AOP 不生效。
3)AOP 核心概念(要背得很顺)
- Aspect(切面) :一组增强逻辑的集合(类)
- Advice(通知) :增强逻辑在什么时机执行(方法)
- Pointcut(切点) :匹配哪些方法需要增强(表达式)
- Join Point(连接点) :可被拦截的点(Spring AOP 主要是方法调用)
- Weaving(织入) :把切面应用到目标对象创建代理的过程
Spring AOP 只支持 方法级 JoinPoint(不像 AspectJ 可以拦构造器/字段)。
4)通知类型(Advice)与“执行顺序”
你截图里有“通知类型、通知顺序”,这个是理解 AOP 的关键。
4.1 通知类型
- @Before:目标方法执行前
- @AfterReturning:目标方法正常返回后
- @AfterThrowing:目标方法抛异常后
- @After:无论成功/异常都执行(类似 finally)
- @Around:包裹目标方法(最强,能决定是否执行目标方法、能改入参/返回值)
实战里最常用的是
@Around,因为它能做“耗时统计、统一日志、异常包装、限流、缓存”等。
4.2 单个切面内的执行顺序(理解成 try/catch/finally)
如果你用 @Around 包裹,顺序可以理解为:
@Around
Object around() {
// 1 before
try {
Object ret = pjp.proceed(); // 执行目标方法
// 2 afterReturning
return ret;
} catch (Throwable ex) {
// 3 afterThrowing
throw ex;
} finally {
// 4 after
}
}
如果同时写了多个通知(Before/After...):
@Before在 proceed 前@AfterReturning在 proceed 正常返回后@AfterThrowing在 proceed 抛异常后@After最后(finally)
5)多个切面一起生效时:顺序怎么排?
当一个方法同时被多个切面拦截,会形成“洋葱圈”:
外层切面 Around-before
内层切面 Around-before
目标方法
内层切面 Around-after
外层切面 Around-after
控制顺序用:
@Order(数字):数字越小优先级越高(越外层)- 或实现
Ordered接口
典型推荐顺序(常见做法)
- 最外层:Trace/日志(保证所有链路都有日志)
- 中间:鉴权/限流(越早失败越省资源)
- 内层:事务(事务包住真正业务)
- 最内层:缓存(看策略,有时缓存更外层)
注意:事务本身也是一个切面,它也参与顺序。
6)切点表达式:execution(最常用)
execution 用来按“方法签名”匹配。
常见模板:
execution(访问修饰符 返回值 包名.类名.方法名(参数))
例子:
- 匹配 service 包下所有方法:
execution(* com.xxx.service..*(..))
- 匹配 UserService 的所有 public 方法:
execution(public * com.xxx.service.UserService.*(..))
- 匹配返回值为 String 的方法:
execution(String com.xxx..*(..))
常见坑
..表示多级包*匹配任意- 参数
(..)表示任意参数 - 方法重载时,execution 会都匹配
7)切点表达式:annotation(更工程化)
很多项目更喜欢用注解来标记“哪些方法需要增强”,比如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {}
切点写:
@Pointcut("@annotation(com.xxx.Loggable)")
public void logPoint(){}
优点:
- 精确、可控
- 不受包结构变化影响
- 读代码就知道这个方法有切面
8)JoinPoint / ProceedingJoinPoint:能拿到什么
JoinPoint(用于 Before/After 等):能拿到方法签名、参数、目标对象等ProceedingJoinPoint(只在 Around 里):多一个proceed(),可以控制是否执行目标方法
常用:
pjp.getSignature()方法名、类名pjp.getArgs()参数pjp.proceed()执行目标方法- 记录耗时、记录入参/返回值(注意脱敏)
9)AOP 最常见的“失效原因”(你一定会遇到)
- 目标对象不是 Spring Bean(你自己 new 的)
- 自调用失效:同一个类里
this.xxx()调用,不经过代理 - 方法不是 public(默认情况下)
- final 类 / final 方法(CGLIB 无法覆盖)
- 代理类型导致注入/强转问题(JDK 代理只代理接口)
- 切点写错(包名、表达式不匹配)
10)实战:一个标准 Around 日志切面(思路)
你可以用它做:
- 统一接口日志
- 耗时统计
- 异常日志 + 统一包装(慎重,别吞异常)
关键点:
try/finally保证 after 一定执行- 记录耗时
- 异常要 rethrow(否则业务以为成功)