aop---深入理解

22 阅读4分钟

核心概念 → 代理原理 → 通知类型与执行顺序 → 切点表达式(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 通知类型

  1. @Before:目标方法执行前
  2. @AfterReturning:目标方法正常返回后
  3. @AfterThrowing:目标方法抛异常后
  4. @After:无论成功/异常都执行(类似 finally)
  5. @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 最常见的“失效原因”(你一定会遇到)

  1. 目标对象不是 Spring Bean(你自己 new 的)
  2. 自调用失效:同一个类里 this.xxx() 调用,不经过代理
  3. 方法不是 public(默认情况下)
  4. final 类 / final 方法(CGLIB 无法覆盖)
  5. 代理类型导致注入/强转问题(JDK 代理只代理接口)
  6. 切点写错(包名、表达式不匹配)

10)实战:一个标准 Around 日志切面(思路)

你可以用它做:

  • 统一接口日志
  • 耗时统计
  • 异常日志 + 统一包装(慎重,别吞异常)

关键点:

  • try/finally 保证 after 一定执行
  • 记录耗时
  • 异常要 rethrow(否则业务以为成功)