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;通过构造器内部调用 |