Java AOP完全指南:从原理到实战(全套知识点+场景总结)
AOP(Aspect-Oriented Programming,面向切面编程)是Java开发中核心的编程思想之一,与OOP(面向对象编程)互补------OOP通过"类"封装核心业务逻辑,AOP则通过"切面"封装通用横切逻辑(如日志、权限、事务),实现"核心业务与通用逻辑解耦"。
下面我会从「核心概念→底层原理→开发实战→常见场景」,把AOP的全部知识点讲透,让你不仅懂"是什么",还会用"怎么用",更能理解"为什么这么设计"。
一、先搞懂:AOP到底是什么?(核心原理+核心概念)
1. AOP的本质:解耦"核心业务"与"横切逻辑"
我们先看一个痛点场景:如果多个接口都需要"记录日志+权限校验",按传统OOP写法,会出现大量重复代码:
java
// 接口1:新增用户
@PostMapping("/add")
public Result addUser(UserParam param) {
// 重复的横切逻辑:权限校验
if (!hasPermission()) return Result.fail("无权限");
// 重复的横切逻辑:记录日志(前置)
log.info("开始新增用户,入参:{}", param);
// 核心业务逻辑
userService.save(param);
// 重复的横切逻辑:记录日志(后置)
log.info("新增用户成功");
return Result.success();
}
// 接口2:修改用户(同样要写权限校验+日志记录,重复代码!)
@PostMapping("/update")
public Result updateUser(UserParam param) {
if (!hasPermission()) return Result.fail("无权限");
log.info("开始修改用户,入参:{}", param);
userService.update(param);
log.info("修改用户成功");
return Result.success();
}
问题:权限校验、日志记录是"横切逻辑"(横跨多个核心业务),重复编码导致维护成本高(改日志格式要改所有接口)、核心业务不纯粹。
AOP的解决方案:把"权限校验、日志记录"封装成"切面",通过配置指定"哪些接口需要执行这些逻辑",AOP框架会自动在接口执行时插入横切逻辑------核心业务代码只关注自身,横切逻辑集中维护。
优化后核心业务代码(无重复逻辑):
java
// 接口1:新增用户(仅保留核心业务)
@PostMapping("/add")
@OperationLog // 注解标记:需要记录日志
@RequiresPermission // 注解标记:需要权限校验
public Result addUser(UserParam param) {
userService.save(param);
return Result.success();
}
// 接口2:修改用户(同样无重复逻辑)
@PostMapping("/update")
@OperationLog
@RequiresPermission
public Result updateUser(UserParam param) {
userService.update(param);
return Result.success();
}
横切逻辑集中在"切面"中,一次编写、多处复用------这就是AOP的核心价值:解耦、复用、无侵入。
2. AOP核心概念(必须熟记,否则看不懂后续)
AOP的术语较多,但都是围绕"如何精准插入横切逻辑"设计的,用"白话文+实例"帮你理解:
| 核心概念 | 白话文解释 | 对应实例(日志场景) |
|---|---|---|
| 切面(Aspect) | 封装横切逻辑的"类"(比如日志切面、权限切面),是AOP的核心载体 | LogAspect类(包含日志记录的所有逻辑) |
| 连接点(JoinPoint) | 程序执行过程中"可以插入横切逻辑"的点(如方法执行前、执行后、抛出异常时) | 接口方法addUser执行前、执行后、抛出异常时,都是连接点 |
| 切入点(Pointcut) | 从所有连接点中"筛选出需要插入横切逻辑的点"(即"哪些方法需要被拦截") | 通过表达式筛选:controller包下所有带@OperationLog注解的方法 |
| 通知(Advice) | 切面中"具体的横切逻辑",且指定"在连接点的哪个时机执行"(如前置、后置) | LogAspect中的beforeLog()(方法执行前记录日志)、afterLog()(方法执行后记录日志) |
| 目标对象(Target) | 被AOP拦截的"原始对象"(即核心业务对象,如UserController) |
UserController实例 |
| 代理对象(Proxy) | AOP框架动态生成的"代理对象",包装目标对象,负责插入横切逻辑 | Spring通过JDK动态代理/CGLIB生成的UserController代理对象,调用代理对象方法时会执行切面逻辑 |
| 织入(Weaving) | 把切面的横切逻辑"插入"到目标对象连接点的过程(由AOP框架自动完成) | Spring容器启动时,自动将LogAspect的逻辑织入到UserController的addUser方法中 |
核心关系 :切面(Aspect)= 切入点(Pointcut)+ 通知(Advice)
(简单说:切面="哪些方法要拦截"+"拦截后执行什么逻辑+什么时候执行")
3. AOP的5种通知类型(时机+用法)
通知是横切逻辑的具体实现,核心区别是"执行时机",5种类型覆盖所有场景:
| 通知类型 | 执行时机 | 核心特点 | 示例场景 |
|---|---|---|---|
| 前置通知(@Before) | 目标方法执行前执行 | 不能阻止目标方法执行(除非抛异常),可获取入参 | 权限校验、日志记录(前置) |
| 后置通知(@After) | 目标方法执行后执行(无论成功/失败,都会执行) | 不关心目标方法结果,仅做收尾操作(如释放资源) | 清理临时文件、关闭连接 |
| 返回通知(@AfterReturning) | 目标方法成功执行后执行 | 可获取目标方法的返回值,仅在无异常时执行 | 日志记录(后置,需返回值)、数据统计 |
| 异常通知(@AfterThrowing) | 目标方法抛出异常后执行 | 可获取异常信息,仅在方法抛出异常时执行 | 异常日志记录、报警通知 |
| 环绕通知(@Around) | 目标方法执行前后都执行(最灵活,可控制目标方法是否执行、修改入参/返回值) | 拥有所有通知的能力,是实际开发中最常用的通知类型 | 日志记录、限流、事务控制 |
环绕通知示例(最常用):
java
// 环绕通知可以完整控制目标方法的执行流程
@Around("logPointcut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 前置逻辑:方法执行前
log.info("环绕通知-前置:入参={}", JSONUtil.toJsonStr(joinPoint.getArgs()));
// 2. 执行目标方法(核心!不调用则目标方法不会执行)
Object result = joinPoint.proceed(); // 相当于调用 target.addUser(param)
// 3. 后置逻辑:方法执行后
log.info("环绕通知-后置:返回值={}", JSONUtil.toJsonStr(result));
return result; // 返回目标方法的结果(可修改)
}
二、AOP底层原理:动态代理(JDK动态代理 vs CGLIB)
AOP的"织入"过程依赖动态代理------AOP框架不会修改目标对象的代码,而是在运行时动态生成一个"代理对象",代理对象包装目标对象,在调用目标方法时插入切面逻辑。
Java中AOP的动态代理有两种实现方式,Spring AOP会自动选择:
1. JDK动态代理(默认,优先使用)
原理:
- 基于Java的
java.lang.reflect.Proxy类和InvocationHandler接口实现; - 要求目标对象必须实现接口(代理对象是接口的实现类);
- 核心是"代理接口",而非代理类。
简单实现示例:
java
// 1. 目标接口(JDK动态代理必须有接口)
public interface UserService {
void addUser(String name);
}
// 2. 目标对象(实现接口)
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("核心业务:新增用户" + name);
}
}
// 3. 代理处理器(实现InvocationHandler,封装横切逻辑)
public class LogInvocationHandler implements InvocationHandler {
private Object target; // 目标对象(被代理的对象)
public LogInvocationHandler(Object target) {
this.target = target;
}
// 代理对象的所有方法调用,都会触发invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置横切逻辑:日志记录
System.out.println("JDK代理-前置:执行" + method.getName() + "方法,入参=" + Arrays.toString(args));
// 执行目标方法(反射调用)
Object result = method.invoke(target, args);
// 后置横切逻辑:日志记录
System.out.println("JDK代理-后置:" + method.getName() + "方法执行完成");
return result;
}
}
// 4. 测试:生成代理对象并调用
public class JdkProxyTest {
public static void main(String[] args) {
// 目标对象
UserService target = new UserServiceImpl();
// 生成代理对象(Proxy.newProxyInstance动态生成)
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 目标对象实现的接口
new LogInvocationHandler(target) // 代理处理器
);
// 调用代理对象的方法(会执行切面逻辑+目标方法)
proxy.addUser("张三");
}
}
输出结果:
JDK代理-前置:执行addUser方法,入参=[张三]
核心业务:新增用户张三
JDK代理-后置:addUser方法执行完成
2. CGLIB动态代理(备选,无接口时使用)
原理:
- 基于CGLIB(Code Generation Library)字节码增强框架实现;
- 不要求目标对象实现接口(通过"继承目标类"生成代理对象);
- 核心是"代理类",通过修改字节码生成目标类的子类,重写目标方法并插入切面逻辑。
核心特点:
- 依赖第三方库(Spring已内置CGLIB,无需额外导入);
- 因为是继承,目标类不能是
final(final类不能被继承),目标方法也不能是final(final方法不能被重写); - 性能略优于JDK动态代理(直接操作字节码)。
3. Spring AOP的代理选择策略
Spring AOP会自动根据目标对象选择代理方式:
- 目标对象实现了接口:优先使用JDK动态代理;
- 目标对象未实现接口:使用CGLIB动态代理;
- 可通过配置强制使用CGLIB(Spring Boot 2.x后默认支持)。
4. 织入时机(AOP何时插入切面逻辑)
织入是"切面逻辑插入目标对象"的过程,Spring AOP的织入时机是运行时(动态织入):
- 编译时织入:编译期修改字节码(如AspectJ),需要特殊编译器;
- 类加载时织入:类加载到JVM时修改字节码;
- 运行时织入:Spring容器启动时,动态生成代理对象并织入切面(最灵活,无需额外工具)。
三、Spring AOP开发实战(从0到1实现日志切面)
Spring AOP是Java开发中最常用的AOP实现(基于动态代理+注解驱动),下面通过"日志记录"场景,手把手教你实现AOP开发。
1. 开发步骤(Spring Boot环境)
第一步:导入依赖(Spring Boot已内置AOP,无需额外导入)
如果是纯Spring项目,需导入以下依赖(Maven):
xml
<!-- Spring AOP核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.20</version>
</dependency>
<!-- AspectJ依赖(用于注解解析和切入点表达式) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
第二步:定义切面类(核心,包含切入点+通知)
java
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
// 1. @Aspect:标记为切面类(必须)
@Aspect
// 2. @Component:交给Spring容器管理(必须,否则Spring无法扫描到切面)
@Component
// 3. @Slf4j:Lombok日志注解(可选)
@Slf4j
public class LogAspect {
// --------------- 第一步:定义切入点(Pointcut):筛选需要拦截的方法 ---------------
/**
* 切入点表达式:@annotation(com.example.annotation.OperationLog)
* 含义:拦截所有带 @OperationLog 注解的方法(自定义注解,用于标记需要记录日志的接口)
*/
@Pointcut("@annotation(com.example.annotation.OperationLog)")
public void logPointcut() {
// 切入点方法:无实际逻辑,仅用于承载@Pointcut注解
}
// --------------- 第二步:定义通知(Advice):横切逻辑+执行时机 ---------------
/**
* 环绕通知(最常用):方法执行前后都执行
* 注:环绕通知必须有返回值(Object),且要调用 joinPoint.proceed() 执行目标方法
*/
@Around("logPointcut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 前置逻辑:方法执行前记录日志(入参、方法名、执行时间)
long startTime = System.currentTimeMillis(); // 记录开始时间
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取目标方法
String methodName = method.getName(); // 方法名
Object[] args = joinPoint.getArgs(); // 入参
log.info("【环绕通知-前置】方法名:{},入参:{}", methodName, JSONUtil.toJsonStr(args));
// 2. 执行目标方法(核心!不调用则目标方法不会执行)
Object result; // 目标方法返回值
try {
result = joinPoint.proceed(); // 调用目标方法(如 UserController.addUser())
} catch (Exception e) {
// 3. 异常逻辑:目标方法抛出异常时执行(也可以用 @AfterThrowing 单独定义)
log.error("【环绕通知-异常】方法名:{},异常信息:{}", methodName, e.getMessage(), e);
throw e; // 抛出异常,不影响业务层异常处理
}
// 4. 后置逻辑:方法执行后记录日志(返回值、执行耗时)
long costTime = System.currentTimeMillis() - startTime; // 计算耗时
log.info("【环绕通知-后置】方法名:{},返回值:{},耗时:{}ms", methodName, JSONUtil.toJsonStr(result), costTime);
return result; // 返回目标方法的返回值(给前端)
}
/**
* 前置通知(单独使用):方法执行前执行(仅演示,实际用环绕通知即可覆盖)
*/
@Before("logPointcut()")
public void beforeLog(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("【前置通知】方法名:{},开始执行", methodName);
}
/**
* 返回通知(单独使用):方法成功执行后执行(仅演示)
*/
@AfterReturning(value = "logPointcut()", returning = "result")
public void afterReturningLog(ProceedingJoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
log.info("【返回通知】方法名:{},执行成功,返回值:{}", methodName, JSONUtil.toJsonStr(result));
}
/**
* 异常通知(单独使用):方法抛出异常后执行(仅演示)
*/
@AfterThrowing(value = "logPointcut()", throwing = "e")
public void afterThrowingLog(ProceedingJoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
log.error("【异常通知】方法名:{},执行失败,异常信息:{}", methodName, e.getMessage(), e);
}
/**
* 后置通知(单独使用):方法执行后执行(无论成功/失败)(仅演示)
*/
@After("logPointcut()")
public void afterLog(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("【后置通知】方法名:{},执行结束", methodName);
}
}
第三步:定义自定义注解(用于标记需要拦截的方法)
java
import java.lang.annotation.*;
// 1. @Target(ElementType.METHOD):仅作用于方法
@Target(ElementType.METHOD)
// 2. @Retention(RetentionPolicy.RUNTIME):运行时保留(AOP需要反射获取)
@Retention(RetentionPolicy.RUNTIME)
// 3. @Documented:生成JavaDoc时包含该注解
@Documented
public @interface OperationLog {
// 可选参数:日志描述(如"新增用户""修改订单")
String desc() default "";
}
第四步:在目标方法上添加注解(标记需要记录日志)
java
import com.example.annotation.OperationLog;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
// 添加 @OperationLog 注解:标记该方法需要执行日志切面逻辑
@OperationLog(desc = "新增用户")
@PostMapping("/user/add")
public Result addUser(@RequestBody UserParam param) {
// 核心业务逻辑(无任何日志代码,AOP自动插入)
userService.save(param);
return Result.success("新增成功");
}
@OperationLog(desc = "修改用户")
@PostMapping("/user/update")
public Result updateUser(@RequestBody UserParam param) {
userService.update(param);
return Result.success("修改成功");
}
}
第五步:测试效果
调用 /user/add 接口,控制台输出日志(按通知执行顺序):
【前置通知】方法名:addUser,开始执行
【环绕通知-前置】方法名:addUser,入参:{"name":"张三","age":25}
【环绕通知-后置】方法名:addUser,返回值:{"code":200,"msg":"新增成功","data":null},耗时:10ms
【返回通知】方法名:addUser,执行成功,返回值:{"code":200,"msg":"新增成功","data":null}
【后置通知】方法名:addUser,执行结束
2. 切入点表达式(关键:精准筛选拦截方法)
切入点表达式是AOP的核心,用于"精准筛选需要拦截的方法",Spring AOP支持多种表达式语法,最常用的有3种:
(1)按注解拦截(最灵活,推荐)
java
// 拦截所有带 @OperationLog 注解的方法(自定义注解)
@Pointcut("@annotation(com.example.annotation.OperationLog)")
// 拦截所有带 @RequestMapping 注解的方法(Spring内置注解)
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
(2)按包路径拦截(批量拦截)
java
// 拦截 com.example.controller 包下所有类的所有方法(包括子包)
@Pointcut("execution(* com.example.controller..*(..))")
表达式解析:
*:匹配任意返回值类型;com.example.controller..:匹配该包及所有子包;*:匹配任意类;(..):匹配任意参数(无参、单参、多参)。
(3)按方法签名拦截(精准到方法)
java
// 拦截 com.example.controller.UserController 类的 addUser 方法(参数为 UserParam)
@Pointcut("execution(public * com.example.controller.UserController.addUser(com.example.param.UserParam))")
3. 通知执行顺序(必记,避免逻辑混乱)
当一个切面有多个通知时,执行顺序如下(以环绕通知为例):
@Before(前置通知) → 环绕通知前置逻辑 → 目标方法执行 → 环绕通知后置逻辑 → @AfterReturning(返回通知)/ @AfterThrowing(异常通知) → @After(后置通知)
- 正常情况(无异常):
@Before→ 环绕前置 → 目标方法 → 环绕后置 →@AfterReturning→@After; - 异常情况(抛异常):
@Before→ 环绕前置 → 目标方法 → 环绕异常逻辑 →@AfterThrowing→@After。
四、AOP高级知识点(框架开发必备)
1. 切面优先级(@Order注解)
当多个切面同时拦截同一个方法时,需要指定切面的执行顺序,用@Order(n)注解(n越小,优先级越高):
java
// 日志切面(优先级1,先执行)
@Aspect
@Component
@Order(1)
public class LogAspect { ... }
// 权限切面(优先级2,后执行)
@Aspect
@Component
@Order(2)
public class PermissionAspect { ... }
执行顺序:LogAspect的通知 → PermissionAspect的通知 → 目标方法 → PermissionAspect的后置通知 → LogAspect的后置通知。
2. 获取目标方法的注解参数
在切面中可以通过反射获取目标方法上的注解参数(如@OperationLog(desc="新增用户")的desc值):
java
@Around("logPointcut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 2. 获取注解实例
OperationLog operationLog = method.getAnnotation(OperationLog.class);
// 3. 获取注解参数
String desc = operationLog.desc();
log.info("操作描述:{}", desc); // 输出:操作描述:新增用户
// 执行目标方法
return joinPoint.proceed();
}
3. 环绕通知修改入参/返回值
环绕通知的灵活性在于可以修改目标方法的入参和返回值:
java
@Around("logPointcut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 修改入参(比如给入参的name字段添加前缀)
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0 && args[0] instanceof UserParam) {
UserParam param = (UserParam) args[0];
param.setName("AOP修改-" + param.getName()); // 入参修改
}
// 2. 执行目标方法
Object result = joinPoint.proceed(args); // 传入修改后的入参
// 3. 修改返回值(比如给返回消息添加后缀)
if (result instanceof Result) {
Result res = (Result) result;
res.setMsg(res.getMsg() + "(AOP修改返回值)");
}
return result;
}
4. 静态切入点 vs 动态切入点
- 静态切入点:基于方法签名(如包名、方法名、注解),在Spring容器启动时就确定,性能高(大部分场景用静态切入点);
- 动态切入点:基于方法运行时的参数、返回值、异常等动态条件(如"入参id>100的方法才拦截"),性能较低(少用)。
五、Java开发中AOP的常见使用场景(实战价值)
AOP的核心价值是"通用横切逻辑的复用与解耦",以下是实际开发中最常用的场景,覆盖80%的业务需求:
1. 日志记录(最常用)
- 场景:记录接口的入参、返回值、执行耗时、操作人、操作时间等(审计日志、访问日志);
- 实现:用环绕通知,在方法执行前后记录日志,异步保存到数据库(避免影响接口性能);
- 示例:Spring Cloud的Sleuth链路追踪、自定义操作日志框架。
2. 权限校验
- 场景:接口访问前校验用户是否登录、是否有操作权限(如"只有管理员能删除用户");
- 实现:用前置通知或环绕通知,在方法执行前获取当前用户权限,无权限则抛异常;
- 示例:Spring Security的
@PreAuthorize注解、Shiro的权限拦截。
3. 接口限流
- 场景:限制接口的访问频率(如"每秒最多10次请求"),防止接口被刷爆;
- 实现:用环绕通知,结合Redis计数,在方法执行前判断访问次数,超过阈值则拒绝请求;
- 示例:自定义
@RateLimit注解+AOP+Redis实现限流。
4. 事务控制
- 场景:保证核心业务的原子性(如"下单→扣库存→减余额"要么全成功,要么全失败);
- 实现:Spring的
@Transactional注解底层就是AOP,用环绕通知管理事务(执行前开启事务,成功提交,失败回滚); - 示例:Service层方法添加
@Transactional注解,AOP自动织入事务逻辑。
5. 异常统一处理
- 场景:接口抛出异常后,统一格式化返回结果(如
{"code":500,"msg":"服务器内部错误"}),避免返回原始异常栈; - 实现:用异常通知(
@AfterThrowing)或Spring的@RestControllerAdvice(底层是AOP); - 示例:全局异常处理器
GlobalExceptionHandler。
6. 数据脱敏
- 场景:接口返回的敏感数据(手机号、身份证号)进行脱敏处理(如"138****1234");
- 实现:用返回通知(
@AfterReturning)或环绕通知,在方法返回后修改返回值,对敏感字段脱敏; - 示例:自定义
@Sensitive注解,标记需要脱敏的字段,AOP自动处理。
7. 缓存控制
- 场景:接口查询结果缓存(如"查询商品详情",首次查询数据库,后续查询缓存);
- 实现:用环绕通知,方法执行前先查缓存,缓存存在则直接返回,不存在则执行业务方法并缓存结果;
- 示例:Spring的
@Cacheable、@CacheEvict注解(底层是AOP)。
8. 性能监控
- 场景:统计核心接口的执行耗时,排查性能瓶颈;
- 实现:用环绕通知,记录方法开始和结束时间,计算耗时并上报监控平台(如Prometheus);
- 示例:微服务的性能监控组件(SkyWalking、Pinpoint)底层依赖AOP采集耗时数据。
六、AOP的优缺点(避坑指南)
1. 优点
- 解耦:核心业务与通用逻辑分离,代码更简洁,维护成本低;
- 复用:横切逻辑一次编写,多处复用(如日志切面可作用于所有接口);
- 无侵入:核心业务代码无需修改,仅通过注解或配置即可启用AOP;
- 灵活:可通过切入点表达式精准控制拦截范围,支持动态开关。
2. 缺点
- 调试难度高:AOP动态生成代理对象,代码执行流程比OOP复杂,调试时需要关注切面逻辑;
- 性能开销:动态代理和反射会带来轻微性能损耗(高频接口需注意优化,如缓存反射对象);
- 学习成本:AOP术语较多,切入点表达式、通知顺序等需要熟练掌握;
- 过度使用风险:简单场景(如单个方法需要日志)用AOP会增加复杂度,不如直接写代码。
七、总结:AOP核心知识点图谱
AOP核心 → 解耦横切逻辑与核心业务
├─ 核心概念:切面(Aspect)= 切入点(Pointcut)+ 通知(Advice)
│ ├─ 切入点:筛选拦截方法(表达式:注解/包路径/方法签名)
│ └─ 通知:5种类型(前置/后置/返回/异常/环绕)
├─ 底层原理:动态代理(JDK代理:接口优先;CGLIB:无接口)
├─ 开发流程:导入依赖→定义切面→定义切入点→定义通知→标记目标方法
├─ 高级特性:切面优先级(@Order)、修改入参/返回值、获取注解参数
├─ 常见场景:日志、权限、限流、事务、异常处理、缓存、脱敏、性能监控
└─ 避坑指南:避免过度使用、缓存反射对象、注意通知执行顺序
AOP是Java进阶的核心知识点,也是框架开发的基石(Spring、MyBatis等框架都重度依赖AOP)。建议先从"日志记录""权限校验"等简单场景入手,动手实现一个切面,再逐步深入复杂场景(如事务、缓存)。
如果遇到具体问题(如AOP拦截不到方法、通知顺序混乱),可以从以下几点排查:
- 切面类是否加了
@Aspect和@Component注解; - 切入点表达式是否正确(包路径、注解全类名是否写错);
- 目标对象是否被Spring容器管理(是否加了
@Controller/@Service等注解); - 是否是final类/方法(CGLIB无法代理final类)。