一、AOP 基础概念
1. 什么是 AOP
AOP:面向切面编程(Aspect Oriented Programming) 核心思想:横向抽取通用公共逻辑,不侵入业务代码,实现解耦。 传统 OOP 是纵向继承;AOP 是横向切面,统一拦截增强。
2. 解决什么问题
业务代码中大量重复逻辑:日志、权限校验、事务、接口限流、参数校验、性能监控、异常统一处理。 如果写在每个接口:代码冗余、难维护、污染业务; AOP 统一抽成切面,运行时动态切入目标方法,无侵入。
3. AOP 核心术语
- JoinPoint 连接点 程序中可以被切面拦截的点,Spring 中仅支持方法执行作为连接点(类、字段、构造器不支持)。
- Pointcut 切入点 匹配规则,筛选哪些连接点需要被拦截(表达式匹配方法)。
- Advice 通知 切面要执行的增强逻辑,分 5 种时机:
- @Before:前置通知,方法执行前
- @AfterReturning:正常返回后(无异常)
- @AfterThrowing:抛出异常后
- @After:最终通知,无论是否异常都执行(类似 finally)
- @Around:环绕通知,包裹目标方法,可控制执行、返回值、异常
- Aspect 切面 切面类 = 切入点 + 通知,存放增强逻辑。
- Target 目标对象 被切面代理的原始业务对象。
- Proxy 代理对象 Spring 运行时生成的代理类,调用方法时先走切面逻辑。
- Weaving 织入 将切面逻辑植入目标对象的过程;Spring AOP 是运行时织入。
二、Spring AOP 底层两种代理实现
Spring 会自动选择代理方式,无需手动指定:
1. JDK 动态代理
- 条件:目标类实现了至少一个接口
- 原理:基于接口生成代理类,只能拦截接口定义的方法
- 限制:无法代理类中未在接口声明的方法
2. CGLIB 字节码代理
- 条件:目标类没有实现任何接口
- 原理:继承目标类生成子类作为代理,重写所有非 final 方法
- SpringBoot 2.x+ 默认使用 CGLIB 代理
切换代理方式配置(application.yml)
yaml
spring:
aop:
proxy-target-class: true # 强制全部使用CGLIB
三、注解式 AOP
步骤 1:引入依赖
xml
<!-- Spring AOP 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤 2:开启 AOP
启动类添加注解:
java
运行
@SpringBootApplication
@EnableAspectJAutoProxy // 开启自动代理,支持AOP
public class App {}
步骤 3:完整切面示
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Date;
@Aspect // 标记为切面类
@Component // 交给Spring管理
public class LogAspect {
// 1. 定义切入点:匹配controller包下所有类所有方法
@Pointcut("execution(* com.demo.controller.*.*(..))")
public void pointCut() {}
// 前置通知
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("方法执行前:" + new Date());
// 获取方法信息
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs(); // 获取入参
}
// 正常返回通知,可接收返回值
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturn(Object result) {
System.out.println("方法正常返回,返回值:" + result);
}
// 异常通知,捕获异常
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrow(Exception ex) {
System.out.println("方法抛出异常:" + ex.getMessage());
}
// 最终通知,一定执行
@After("pointCut()")
public void after() {
System.out.println("方法执行完毕(finally)");
}
// 环绕通知(功能最强,可控制目标方法是否执行)
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
// 执行目标方法
Object res = pjp.proceed();
long cost = System.currentTimeMillis() - start;
System.out.println("方法耗时:" + cost + "ms");
return res;
}
}
通知执行顺序
正常无异常: @Around前置 → @Before → 目标方法 → @AfterReturning → @After → @Around后置
出现异常: @Around前置 → @Before → 目标方法抛异常 → @AfterThrowing → @After
四、Pointcut 切入点表达式详解
1. execution 执行表达式
语法: execution(返回值 包.类.方法(参数)) 通配符:
*:任意返回值、任意类、任意方法、单个参数..:任意多层包、任意多个参数
示例:
java
// 匹配com.demo.service下所有类所有方法
execution(* com.demo.service.*.*(..))
// 匹配service包及所有子包
execution(* com.demo.service..*.*(..))
// 仅匹配返回值void的方法
execution(void com.demo.controller.*.*(..))
// 匹配带一个String参数的方法
execution(* com.demo.*.*(String))
2. @annotation 注解匹配
自定义注解:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {}
切面切入点:只拦截加了@LogRecord注解的方法
java
@Pointcut("@annotation(com.demo.annotation.LogRecord)")
3. within 按类匹配
java
// 匹配controller包下所有类
within(com.demo.controller.*)
4. args 按参数类型匹配
java
// 拦截第一个参数为Long的方法
args(Long,..)
五、JoinPoint / ProceedingJoinPoint 常用 API
仅环绕通知使用 ProceedingJoinPoint,其余通知用 JoinPoint
java
// 获取方法签名
Signature signature = joinPoint.getSignature();
MethodSignature methodSig = (MethodSignature) signature;
Method method = methodSig.getMethod();
// 获取方法入参
Object[] args = joinPoint.getArgs();
// 获取目标对象
Object target = joinPoint.getTarget();
// 获取代理对象
Object proxy = joinPoint.getThis();
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 环绕专属:执行目标方法
Object result = pjp.proceed();
// 带参数执行(修改入参)
Object result = pjp.proceed(new Object[]{newParam});
六、经典实战场景
场景 1:自定义注解实现接口日志记录
- 自定义
@LogRecord注解,注解上存模块、操作类型 - AOP 拦截该注解,获取注解属性、请求参数、返回值、耗时存入数据库
场景 2:全局接口权限校验
AOP 拦截 Controller 方法,从 Token 获取用户,判断是否拥有接口权限,无权限直接抛出异常
场景 3:统一接口性能监控
环绕通知记录每个接口执行耗时,超时接口打印告警日志
场景 4:统一异常处理补充
AOP 捕获方法异常,记录错误堆栈,配合 @RestControllerAdvice 全局异常返回统一格式
场景 5:分布式事务、本地事务管理(@Transactional 底层就是 AOP)
Spring 事务本质:AOP 环绕通知,方法前开启事务,正常提交、异常回滚
七、AOP 执行顺序与多切面排序
1. 多个切面执行顺序
使用 @Order(int) 指定优先级,数值越小,优先级越高:
- 进入方法:Order 小的切面先执行
- 退出方法:Order 小的切面后执行
java
@Aspect
@Component
@Order(1) // 优先级高于Order(2)
public class LogAspect {}
2. 同一切面内通知顺序
Around → Before → 方法执行 → AfterReturning/AfterThrowing → After
八、常见坑与面试重点
1. 内部调用 AOP 失效
问题:同一个类中 A方法() 调用本类 B方法(),B 上的切面不执行。 原因:内部调用使用原始this对象调用,不是代理对象,不会走 AOP 拦截。 解决方案:
- 注入自身代理对象:
@Autowired private 当前类 self;,用self.B()调用 - 拆分方法到不同类
- 使用 Spring 上下文获取代理
2. final/static 方法无法被 AOP 拦截
CGLIB 通过继承重写方法,final 不能重写;static 属于类,不属于实例,无法代理。
3. 私有方法不能拦截
Spring AOP 仅拦截 public 方法。
4. @Transactional 失效场景
- 方法 private/static/final
- 同类内部调用
- 异常被 try-catch 捕获,未抛出
- 传播行为配置错误
5. JDK 代理只能拦截接口方法
目标类无接口自动切换 CGLIB;SpringBoot 默认 CGLIB。
九、AOP 与 AspectJ 区别
-
Spring AOP
- 运行时织入,动态代理实现
- 仅支持方法连接点,轻量,无需额外编译
- 开发主流,Spring 内置
-
原生 AspectJ
- 编译期 / 加载期织入,修改字节码
- 支持方法、构造器、字段等全部连接点
- 需要 ajc 编译器,较重,一般项目不用
十、完整开发流程总结
- 引入 aop starter
- 启动类加 @EnableAspectJAutoProxy
- 编写切面类:@Aspect + @Component
- 定义 @Pointcut 切入点表达式
- 编写 5 种通知逻辑,通过 JoinPoint 获取方法信息
- @Order 控制多切面顺序
- 避开内部调用、private、final 等失效场景
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/zgzldnr/article/details/162914302



