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 字节码框架,生成目标类的子类
前置条件 目标类必须实现至少一个接口 目标类不能是 final (final 类无法继承)
代理对象类型 接口类型,强转时必须用接口接收 目标类的子类,可用目标类本身接收
方法拦截范围 只能拦截接口中定义的方法 能拦截目标类中的所有非 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;通过构造器内部调用