SpringAOP:面向切面编程
大家好,这依旧是一篇个人笔记,关于SpringAOP的笔记
笔记内容来源于黑马Web课程133~139集
本篇笔记由WorkBuddy一起辅助完成,不喜勿喷,谢谢!
一、Spring AOP 整体思路
Spring AOP(Aspect Oriented Programming,面向切面编程)是一种编程思想,核心思想是:在不修改原有业务代码的情况下,为指定的方法统一添加额外的通用功能。
1.1 AOP 核心思想
AOP 的本质是「面向方法编程」,通过「切面」将「额外功能」与「作用范围」结合起来:
- • 切面(Aspect):额外功能 + 作用范围的组合
- • 额外功能:如统计耗时、日志记录、权限校验等通用逻辑
- • 作用范围:通过切入点表达式指定哪些方法需要添加额外功能
总结:AOP编程思想“面向切面”或者“面向方法”编程本质上就是给“你规定的一系列方法”加上“额外功能”
所谓“切面”就是指“额外功能”+“作用范围”(就是我规定的哪些方法)
以上面图片为例:本质上就是给“业务层的方法”添加“计算耗时”这个额外功能
1.2 AOP 执行流程
AOP 的执行基于「动态代理」机制:
步骤 | 说明 |
1. 定义切面类 | 使用 @Aspect 注解标识切面类,@Component 纳入 Spring 管理 |
2. 定义切入点 | 使用 @Pointcut 或直接在通知注解中指定目标方法范围 |
3. 定义通知 | 使用 @Around/@Before 等注解定义额外功能的执行时机 |
4. 生成代理对象 | Spring 为目标对象创建代理对象(JDK 动态代理或 CGLIB) |
5. 调用代理方法 | Controller 实际调用的是代理对象的方法 |
6. 执行通知逻辑 | 代理对象先执行通知逻辑,再调用目标方法 |
二、Spring AOP 基础
2.1 AOP 快速入门
2.1.1 场景需求
统计所有业务层方法的执行耗时,用于性能分析和优化。
2.1.2 实现步骤
步骤一:引入 AOP 依赖(pom.xml)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>步骤二:编写切面类
@Aspect // 标明这是一个 AOP 类 @Component // 纳入 Spring 容器管理 public class RecordTimeAspect { @Around("execution(* com.itheima.service.impl.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { // 1. 记录开始时间 long begin = System.currentTimeMillis(); // 2. 执行目标方法 Object result = pjp.proceed(); // 3. 记录结束时间并计算耗时 long end = System.currentTimeMillis(); log.info("方法执行耗时: {} ms", end - begin); return result;2.2 AOP 核心概念
概念 | 说明 |
连接点 (JoinPoint) | 可以被 AOP 控制的方法,包含方法执行时的相关信息 |
通知 (Advice) | 需要添加的额外功能(共性逻辑),最终体现为一个方法 |
切入点 (PointCut) | 匹配连接点的条件,通知仅在切入点方法执行时被应用 |
切面 (Aspect) | 描述通知与切入点的对应关系(通知 + 切入点) |
目标对象 (Target) | 通知所应用的对象(被代理的原始对象) |
2.3 AOP 的优势
- • 减少重复代码:通用逻辑统一维护
- • 代码无侵入:不修改原有业务代码
- • 提高开发效率:复用通用功能
- • 维护方便:修改一处,全局生效
三、Spring AOP 进阶
3.1 通知类型
根据通知方法执行时机的不同,分为以下五类:
通知类型 | 执行时机 | 特点 |
@Around | 目标方法前、后都执行 | 环绕通知,需手动调用 proceed(),最常用 |
@Before | 目标方法前执行 | 前置通知 |
@After | 目标方法后执行 | 后置通知,无论是否异常都执行 |
@AfterReturning | 目标方法正常返回后执行 | 返回后通知,有异常不执行 |
@AfterThrowing | 目标方法抛出异常后执行 | 异常后通知 |
⚠️ 注意事项:
- • @Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来执行目标方法
- • @Around 环绕通知方法的返回值必须指定为 Object,用于接收目标方法的返回值
3.2 通知顺序
3.2.1 默认顺序
当多个切面的切入点都匹配到目标方法时,默认按照切面类的「类名字母顺序」排序:
- • 目标方法前的通知:字母排名靠前的先执行
- • 目标方法后的通知:字母排名靠前的后执行
3.2.2 自定义顺序(@Order)
使用 @Order(数字) 注解控制执行顺序,数字越小越先执行:
@Order(5) // 先执行 @Aspect @Component public class MyAspect3 {@Before("execution(* com.itheima.service.impl.*.*(..))")public void before(){ log.info("MyAspect3 -> before ..."); } @Order(8) // 后执行 @Aspect @Component public class MyAspect2 {@Before("execution(* com.itheima.service.impl.*.*(..))")public void before(){ log.info("MyAspect3 -> before ..."); }3.3 切入点表达式
3.3.1 execution 表达式
execution 是最常用的切入点表达式,语法如下:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
通配符说明:
通配符 | 含义 |
* | 单个独立的任意符号,可匹配任意返回值、包名、类名、方法名、参数 |
.. | 多个连续的任意符号,可匹配任意层级的包或任意个数、类型的参数 |
常用示例:
// 匹配 service 包下所有类的所有方法 execution(* com.itheima.service.*.*(..)) // 匹配所有以 del 开头的方法 execution(* com.itheima.service.*.del*(..)) // 匹配所有以 e 结尾的方法 execution(* com.itheima.service.*.*e(..)) // 同时匹配多个方法(使用 || 运算符) execution(* com.itheima.service.*.list(..)) || execution(* com.itheima.service.*.delete(..))书写建议:
- • 所有业务方法名命名时尽量规范,方便切入点表达式快速匹配
- • 描述切入点方法通常基于接口描述,增强拓展性
- • 在满足业务需要的前提下,尽量缩小切入点的匹配范围
3.3.2 @annotation 表达式
基于自定义注解标记方法,而非直接写方法路径,更加灵活:
补充一下关于annotation切入点表达式的理解:本质上就是定义了一个自己想要的注解,然后再切面类的通知方法里面绑定归属于哪一个自己定义的注解
步骤一:定义自定义注解
步骤二:切面类绑定注解
步骤三:在目标方法上使用注解
3.3.3 @PointCut 抽取公共切入点
将公共的切入点表达式抽取出来,提高复用性:
(PointCut:对于公共切入点(方法路径)的简化;作用就像是springboot里面RequestMapping公共路径书写的作用)
@Pointcut("execution(* com.itheima.service.impl.*.*(..))") private void pt() {} // private:仅当前切面类可用 @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { // 通知逻辑 }注意:
• private:仅能在当前切面类中引用该表达式
• public:在其他外部的切面类中也可以引用该表达式
3.4 连接点 JoinPoint
就是你要在Aspect类里面执行原方法的内容就必须用到这个JointPoint
在 Spring 中用 JoinPoint 抽象了连接点,可以获得方法执行时的相关信息:
API 方法 | 说明 |
joinPoint.getTarget() | 获取目标对象 |
joinPoint.getTarget().getClass().getName() | 获取目标类全名 |
joinPoint.getSignature().getName() | 获取目标方法名 |
joinPoint.getArgs() | 获取目标方法参数数组 |
joinPoint.proceed() | 执行目标方法(仅 ProceedingJoinPoint 可用) |
注意:
- •对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
- • 对于其他四种通知,获取连接点信息使用 JoinPoint(ProceedingJoinPoint 的父类)
四、Spring AOP 案例
4.1 案例:记录操作日志
将增、删、改相关接口的操作日志记录到数据库表中。
4.1.1 日志信息内容
- • 操作人:当前登录员工的 ID
- • 操作时间:方法执行的时间
- • 执行方法的全类名、方法名
- • 方法运行时参数、返回值
- • 方法执行时长
4.1.2 技术方案
项目 | 选择 |
通知类型 | @Around 环绕通知 |
切入点表达式 | @annotation(com.example.tlias_management.anno.Log) |
4.2 案例:获取当前登录员工
通过 ThreadLocal 在拦截器和切面之间传递当前登录员工信息。
4.2.1 实现思路
输出操作人实现思路:操作的时候需要登陆认证,会产生JWT令牌,之前我们定义的JWT令牌自定义了信息“id”,而这个id可以唯一标识每一个员工,因此获取到这个id就相当于知道操作人是谁了
- • 登录时生成 JWT 令牌,包含员工 ID
- • 拦截器解析 Token,将员工 ID 存入 ThreadLocal
- • 切面类从 ThreadLocal 获取操作人 ID
- • 请求结束后清除 ThreadLocal(防止内存泄漏)
4.2.2 ThreadLocal 工具类
先准备这个线程类,每次请求的数据都会保存在独立的一次线程中,保证了获取到的数据就是那次请求的数据
4.2.3 拦截器设置 ThreadLocal
@Component public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token"); Claims claims = JwtUtils.parseToken(token); Integer employeeId = Integer.valueOf(claims.get("id").toString()); CurrentHolder.setCurrentId(employeeId); // 存入 ThreadLocal return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { CurrentHolder.remove(); // 清除 ThreadLocal } }4.2.4 切面类获取操作人
@Around("@annotation(com.example.tlias_management.anno.Log)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { // 从 ThreadLocal 获取操作人 ID Integer operateEmpId = CurrentHolder.getCurrentId(); // 其他日志信息... LocalDateTime operateTime = LocalDateTime.now(); String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); // 执行目标方法 Object result = joinPoint.proceed(); // 保存日志到数据库... return result;五、知识总结
5.1 核心概念回顾
概念 | 一句话总结 |
AOP | 面向切面编程,不修改原代码给方法加额外功能 |
切面 (Aspect) | 通知 + 切入点的组合 |
通知 (Advice) | 要添加的额外功能 |
切入点 (PointCut) | 指定哪些方法需要添加通知 |
连接点 (JoinPoint) | 可以被 AOP 控制的方法 |
目标对象 (Target) | 被代理的原始对象 |
5.2 常用注解
注解 | 作用 |
@Aspect | 标识这是一个切面类 |
@Component | 将切面类纳入 Spring 容器管理 |
@Around/@Before/@After | 定义通知类型和执行时机 |
@Pointcut | 抽取公共切入点表达式 |
@Order | 控制多个切面的执行顺序 |
5.3 切入点表达式
类型 | 使用场景 |
execution | 按方法路径匹配,适合批量匹配 |
@annotation | 按自定义注解匹配,更加灵活精准 |
5.4 注意事项
- • @Around 必须调用 proceed() 执行目标方法
- • @Around 返回值必须是 Object
- • 使用 ThreadLocal 后一定要在 afterCompletion 中 remove()
- • 切入点表达式尽量缩小匹配范围,提高性能
