AOP 学习笔记
一、AOP 基础认知
1. 什么是 AOP
- 定义:AOP(Aspect Oriented Programming)即面向切面编程,核心是面向特定方法编程,将重复逻辑与业务逻辑分离,实现功能增强。
- 本质:不修改原始业务代码,通过动态代理技术对目标方法进行增强,解决代码重复、侵入性强的问题。
2. AOP 核心优势
- 减少重复代码:将日志、计时等共性逻辑抽取到切面类
- 代码无侵入:不修改原始业务方法,降低耦合度
- 提高开发效率:专注业务逻辑开发,共性功能统一实现
- 维护方便:共性功能集中管理,修改时无需改动多个业务类
3. 核心概念
| 概念 | 定义 |
|---|---|
| 连接点(JoinPoint) | 可被 AOP 控制的方法(如业务层所有方法),封装了方法执行时的相关信息 |
| 通知(Advice) | 抽取的重复逻辑(共性功能),体现为具体方法(如计时、日志记录) |
| 切入点(PointCut) | 匹配连接点的条件(通过表达式描述),决定通知应用于哪些方法 |
| 切面(Aspect) | 通知与切入点的对应关系(通知+切入点),所在类为切面类(@Aspect 标识) |
| 目标对象(Target) | 通知所应用的原始业务对象 |
| 代理对象 | Spring AOP 底层通过动态代理生成,用于增强目标对象的方法 |
4. 底层原理
Spring AOP 基于动态代理技术实现:程序运行时自动为目标对象生成代理对象,在代理对象中嵌入通知逻辑,实现对原始方法的增强。
二、AOP 入门实战
1. 需求
统计部门管理业务层方法的执行耗时。
2. 实现步骤
(1)导入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
(2)编写切面类
java
@Component
@Aspect // 标识为切面类
@Slf4j
public class RecordTimeAspect {
// 环绕通知 + 切入点表达式(匹配目标方法)
@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
// 1. 记录开始时间
long begin = System.currentTimeMillis();
// 2. 执行原始业务方法
Object result = pjp.proceed();
// 3. 记录结束时间并计算耗时
long end = System.currentTimeMillis();
log.info("方法执行耗时: {}毫秒", end - begin);
// 4. 返回原始方法结果
return result;
}
}
(3)测试效果
启动服务后,调用业务接口,控制台会输出对应方法的执行耗时。
3. 常见应用场景
- 记录操作日志
- 权限控制
- 事务管理(Spring 事务底层基于 AOP 实现)
- 性能监控(如方法耗时统计)
三、AOP 进阶知识
1. 通知类型
Spring AOP 提供 5 种通知类型,覆盖方法执行的不同阶段:
| 通知注解 | 执行时机 | 注意事项 |
|---|---|---|
| @Around(环绕通知) | 目标方法执行前 + 执行后 | 需调用 proceed() 执行原始方法,必须返回结果 |
| @Before(前置通知) | 目标方法执行前 | 无返回值,不能阻止目标方法执行 |
| @After(后置通知) | 目标方法执行后(无论是否抛出异常) | 无返回值 |
| @AfterReturning | 目标方法正常执行完成后(无异常) | 可获取方法返回值 |
| @AfterThrowing | 目标方法抛出异常后 | 可获取异常信息 |
通知执行顺序(无异常情况)
@Around(前) → @Before → 目标方法 → @Around(后) → @AfterReturning → @After
异常情况
@Around(前) → @Before → 目标方法(抛异常) → @AfterThrowing → @After(@Around 后续逻辑不执行)
2. 切入点表达式
用于描述需要匹配的目标方法,核心有 2 种形式:
(1)execution 表达式(常用)
-
语法 :
execution(访问修饰符? 返回值 包名.类名.?方法名(参数) throws 异常?) -
通配符说明:
*:匹配单个任意符号(如任意返回值、单个包、单个方法名)..:匹配多个连续任意符号(如任意层级包、任意个数/类型的参数)
-
示例:
java// 匹配 com.itheima.service 包下所有类的所有方法 execution(* com.itheima.service.*.*(..)) // 匹配 DeptService 接口中所有以 find 开头的方法 execution(* com.itheima.service.DeptService.find*(..)) // 匹配 delete 方法(参数为 Integer 类型) execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
(2)@annotation 表达式(灵活匹配)
通过自定义注解标记目标方法,适合无规则的方法匹配:
-
定义自定义注解:
java@Target(ElementType.METHOD) // 仅作用于方法 @Retention(RetentionPolicy.RUNTIME) // 运行时生效 public @interface LogOperation {} -
在目标方法上添加注解:
java@Service public class DeptServiceImpl implements DeptService { @Override @LogOperation // 标记需要增强的方法 public void delete(Integer id) { deptMapper.delete(id); } } -
切面类中引用注解:
java@Before("@annotation(com.itheima.anno.LogOperation)") public void before(JoinPoint joinPoint) { log.info("前置通知:记录操作日志"); }
切入点表达式复用
使用 @Pointcut 抽取公共表达式,避免重复编写:
java
@Aspect
@Component
public class MyAspect {
// 抽取公共切入点表达式
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt() {}
// 引用公共表达式
@Before("pt()")
public void before() {
log.info("前置通知...");
}
}
3. 通知顺序控制
当多个切面类匹配同一个目标方法时,通过以下方式控制执行顺序:
-
默认规则:按切面类名的字母顺序排序(前置通知:字母靠前先执行;后置通知:字母靠前后执行)
-
@Order 注解(推荐) :在切面类上添加
@Order(数字),数字越小,优先级越高java@Aspect @Component @Order(1) // 优先级高于 Order(2) 的切面 public class MyAspect1 { ... }
四、AOP 案例:操作日志记录
1. 需求
记录系统中增删改接口的操作日志,包含:操作人、操作时间、类名、方法名、参数、返回值、执行时长,存入数据库。
2. 实现步骤
(1)准备工作
-
数据库表设计:
sqlcreate table operate_log( id int unsigned primary key auto_increment comment 'ID', operate_emp_id int unsigned comment '操作人ID', operate_time datetime comment '操作时间', class_name varchar(100) comment '操作的类名', method_name varchar(100) comment '操作的方法名', method_params varchar(1000) comment '方法参数', return_value varchar(2000) comment '返回值', cost_time int comment '方法执行耗时(ms)' ) comment '操作日志表'; -
实体类
OperateLog+ Mapper 接口(提供insert方法)
(2)编码实现
-
自定义注解
@LogOperation(标记需要记录日志的接口方法) -
切面类实现:
java@Aspect @Component public class OperationLogAspect { @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(com.itheima.anno.LogOperation)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 记录开始时间 long startTime = System.currentTimeMillis(); // 2. 执行原始方法 Object result = joinPoint.proceed(); // 3. 计算耗时 long costTime = System.currentTimeMillis() - startTime; // 4. 构建日志对象 OperateLog log = new OperateLog(); log.setOperateEmpId(CurrentHolder.getCurrentId()); // 从 ThreadLocal 获取当前登录人ID log.setOperateTime(LocalDateTime.now()); log.setClassName(joinPoint.getTarget().getClass().getName()); // 目标类名 log.setMethodName(joinPoint.getSignature().getName()); // 目标方法名 log.setMethodParams(Arrays.toString(joinPoint.getArgs())); // 方法参数 log.setReturnValue(result.toString()); // 返回值 log.setCostTime(costTime); // 5. 保存日志到数据库 operateLogMapper.insert(log); return result; } } -
在 Controller 层增删改方法上添加
@LogOperation注解
3. 关键技术:ThreadLocal 共享登录信息
-
作用:在同一线程(同一请求)中共享数据(如当前登录人ID),实现线程隔离
-
工具类实现 :
javapublic class CurrentHolder { private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>(); // 设置当前登录人ID public static void setCurrentId(Integer empId) { CURRENT_LOCAL.set(empId); } // 获取当前登录人ID public static Integer getCurrentId() { return CURRENT_LOCAL.get(); } // 移除数据(避免内存泄漏) public static void remove() { CURRENT_LOCAL.remove(); } } -
使用场景:在 Token 过滤器中解析登录人ID并存入 ThreadLocal,在 AOP 中直接获取。
四、核心总结
- AOP 核心思想是分离共性逻辑与业务逻辑,通过切面类统一管理增强功能
- 5 种通知类型覆盖方法执行全生命周期,
@Around功能最强大(可控制原始方法执行) - 切入点表达式两种形式:
execution(按方法签名匹配)、@annotation(按注解匹配) - 多切面执行顺序通过
@Order控制,数字越小优先级越高 - 实际开发中常用 AOP 实现日志、权限、事务等非业务功能,提高代码复用性和可维护性