大家好这依旧是一篇个人笔记关于SpringAOP的笔记笔记内容来源于黑马Web课程133~139集本篇笔记由WorkBuddy一起辅助完成不喜勿喷谢谢一、Spring AOP 整体思路Spring AOPAspect Oriented Programming面向切面编程是一种编程思想核心思想是在不修改原有业务代码的情况下为指定的方法统一添加额外的通用功能。1.1 AOP 核心思想AOP 的本质是「面向方法编程」通过「切面」将「额外功能」与「作用范围」结合起来• 切面Aspect额外功能 作用范围的组合• 额外功能如统计耗时、日志记录、权限校验等通用逻辑• 作用范围通过切入点表达式指定哪些方法需要添加额外功能总结AOP编程思想“面向切面”或者“面向方法”编程本质上就是给“你规定的一系列方法”加上“额外功能”所谓“切面”就是指“额外功能”“作用范围”就是我规定的哪些方法以上面图片为例本质上就是给“业务层的方法”添加“计算耗时”这个额外功能1.2 AOP 执行流程AOP 的执行基于「动态代理」机制步骤说明1. 定义切面类使用 Aspect 注解标识切面类Component 纳入 Spring 管理2. 定义切入点使用 Pointcut 或直接在通知注解中指定目标方法范围3. 定义通知使用 Around/Before 等注解定义额外功能的执行时机4. 生成代理对象Spring 为目标对象创建代理对象JDK 动态代理或 CGLIB5. 调用代理方法Controller 实际调用的是代理对象的方法6. 执行通知逻辑代理对象先执行通知逻辑再调用目标方法二、Spring AOP 基础2.1 AOP 快速入门2.1.1 场景需求统计所有业务层方法的执行耗时用于性能分析和优化。2.1.2 实现步骤步骤一引入 AOP 依赖pom.xmldependency groupIdorg.springframework.boot/groupId artifactIdspring-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• 对于其他四种通知获取连接点信息使用 JoinPointProceedingJoinPoint 的父类四、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 拦截器设置 ThreadLocalComponent 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()• 切入点表达式尽量缩小匹配范围提高性能