AOP 核心知识整理

AOP 核心知识整理

AOP是面向切面编程,核心思想是把多个模块的通用逻辑抽取出来,通过动态代理织入目标方法。

一、AOP 核心概念速查

概念 说明 示例
切面 (Aspect) 抽取出来的通用逻辑模块 事务切面、日志切面、权限切面
连接点 (Join Point) 程序执行过程中可插入切面的点 Spring AOP 仅支持方法级别的连接点
切入点 (Pointcut) 定义哪些连接点需要被拦截的表达式 execution(* com.example.service.*.*(..))
通知 (Advice) 在切入点执行的具体操作 @Before@After@Around
织入 (Weaving) 把切面应用到目标对象的过程 Spring AOP 在运行时通过代理完成织入

二、Advice 五种通知类型

类型 注解 执行时机 典型用途
前置通知 @Before 目标方法执行 权限校验、参数验证、记录入参日志
后置通知 @After 目标方法执行 (无论是否异常,类似 finally 释放资源、清理临时数据
返回通知 @AfterReturning 目标方法正常返回 缓存结果、返回值加工、记录出参日志
异常通知 @AfterThrowing 目标方法抛出异常 异常日志记录、异常转换、告警通知
环绕通知 @Around 包裹整个目标方法,前后都可插入逻辑 事务控制、性能监控、重试机制、分布式锁

执行顺序

正常情况:

复制代码
@Around(前) → @Before → 目标方法 → @AfterReturning → @After → @Around(后)

异常情况:

复制代码
@Around(前) → @Before → 目标方法抛异常 → @AfterThrowing → @After

注意:

  • @Around 如果不主动调用 proceed(),后面的通知和目标方法都不会执行。
  • 异常时若 @Around 吞掉了异常,@AfterThrowing 也收不到。

三、切入点表达式:execution vs @annotation

方式 匹配规则 适用场景
execution 基于方法名匹配,支持通配符 批量拦截某个包或某类方法
@annotation 基于注解匹配 精确控制哪些方法需要拦截

execution 表达式语法

java 复制代码
execution([修饰符] 返回值 包名.类名.方法名(参数))

示例:

java 复制代码
// 拦截 service 包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")

// 拦截 service 包及子包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")

// 拦截 UserService 中以 find 开头的方法
@Pointcut("execution(* com.example.service.UserService.find*(..))")

通配符规则:

通配符 含义
* 匹配一个层级
.. 匹配多层包,或在参数列表中表示任意参数

四、代理机制对比:JDK 动态代理 vs CGLIB

维度 JDK 动态代理 CGLIB 代理
原理 基于 java.lang.reflect.Proxy,生成接口的实现类 基于 ASM 字节码框架,生成目标类的子类
前置条件 目标类必须实现至少一个接口 目标类不能是 finalfinal 类无法继承)
代理对象类型 接口类型,强转时必须用接口接收 目标类的子类,可用目标类本身接收
方法拦截范围 只能拦截接口中定义的方法 能拦截目标类中的所有非 final 方法
性能 反射调用,JDK8+ 已大幅优化 直接调用子类方法,生成代理对象时稍慢
默认策略 Spring AOP 早期默认(有接口时) Spring Boot 2.0+ 统一默认使用 CGLIB
获取方式 Proxy.newProxyInstance() Enhancer.create()

核心区别一句话

  • JDK 代理:代理的是接口,调用方必须通过接口引用。
  • CGLIB 代理 :代理的是类本身,生成一个子类,不能代理 final 方法。

为什么 Spring Boot 2.0 后默认 CGLIB?

项目中很多类可能没有实现接口,JDK 代理会直接失败。统一用 CGLIB 可以避免开发者因忘记写接口而导致 AOP 失效。

如需切回 JDK 代理:

properties 复制代码
spring.aop.proxy-target-class=false

五、Spring AOP vs AspectJ

维度 Spring AOP AspectJ
实现机制 运行时动态代理(JDK/CGLIB) 编译时 织入(ajc编译器)或类加载时织入(LTW)
织入时机 运行时 编译期、编译后、类加载期
连接点范围 方法执行 方法执行、方法调用、构造器调用、字段赋值/读取、静态初始化等
代理限制 内部调用不走代理,不能代理 final/static 方法 无代理,直接修改字节码,所有方法均可织入
性能 有代理层,略有性能损耗 直接织入字节码,运行时无额外代理开销,性能更高
依赖 仅需 Spring 容器,零额外配置 需要 AspectJ 编译器或 weaving 代理,配置稍复杂
切入点表达式 支持 AspectJ 部分语法 完整 AspectJ 语法,功能更强
使用场景 大多数企业应用(与 Spring 无缝集成) 需要细粒度控制、非 Spring 项目或极高性能要求

怎么选?

  • 90% 的场景用 Spring AOP 就够了,配置简单,与 Spring 生态天然融合。
  • 需要拦截 static 方法、构造器、字段赋值,或对非 Spring Bean 做切面时,才考虑 AspectJ。

Spring 中集成 AspectJ 的两种方式

方式 说明
仅使用 AspectJ 的切入点表达式 @Pointcut 中写 AspectJ 语法,本质还是 Spring AOP
真正使用 AspectJ 织入 通过 @EnableLoadTimeWeaving 开启 LTW,配合 META-INF/aop.xml 配置织入规则

六、AOP 代理调用流程

复制代码
1. 调用方调用代理对象(非原始对象)
2. 代理对象拦截方法调用,先执行前置通知
3. 代理对象调用目标对象的真实方法
4. 方法执行完成后,代理对象执行后置通知
5. 最终返回给调用方

七、常见坑:代理失效

1. 同类方法内部调用不走代理

java 复制代码
@Service
public class UserService {
    
    // 外部调用这个方法,AOP 生效
    public void outer() {
        // 内部直接调用,用的是 this,不是代理对象,AOP 不生效
        this.inner();
    }
    
    @Transactional
    public void inner() { ... }
}

原因: 内部调用走的是 this,即原始对象,不经过代理。

解决方案:

  • 自己注入自己(通过 @Autowired@Lazy
  • 把内部方法抽到另一个 Bean
  • 使用 AopContext.currentProxy() 获取当前代理对象(需开启 @EnableAspectJAutoProxy(exposeProxy = true)

2. 代理方式导致的限制

代理方式 失效场景
JDK 动态代理 目标类未实现接口;通过实现类而非接口注入
CGLIB 代理 目标类被 final 修饰;方法是 final/static;通过构造器内部调用