Spring AOP 完整教程(下篇・企业综合实战 + ThreadLocal
承接上篇 AOP 基础、中篇通知与切点语法本篇落地真实企业业务全局增删改接口自动记录操作日志入库配套完整业务需求、两种切点实现方案、ThreadLocal 线程共享工具封装、完整切面可运行代码、项目执行流程覆盖开发中登录人信息透传核心痛点是面试高频综合应用题。一、综合实战需求系统操作日志自动记录1. 业务需求说明拦截项目中所有新增、修改、删除接口save/update/delete接口执行完成后自动将操作信息插入操作日志数据库表完整记录字段操作人 ID当前登录账号从 JWT 令牌解析操作执行时间目标类全限定名执行方法名称接口传入请求参数接口返回结果方法执行耗时毫秒技术选型分析通知类型Around环绕通知唯一可同时拿到入参、返回值、执行耗时可捕获异常切点两种实现方案方案 Aexecution 批量匹配所有 save/update/delete 接口无需修改 Controller全量拦截方案 B自定义Log注解按需标记接口灵活控制哪些接口记录日志方案一execution 批量切点全量拦截匹配所有 Controller 层下新增、更新、删除方法无需修改业务代码全局生效// 切点匹配所有controller下save、update、delete接口 Pointcut(execution(* com.itheima.controller.*.save(..)) || execution(* com.itheima.controller.*.update(..)) || execution(* com.itheima.controller.*.delete(..))) public void logPoint(){}方案二自定义注解切点按需拦截适合仅部分接口需要留痕的场景步骤完整实现自定义日志标记注解import java.lang.annotation.*; Target(ElementType.METHOD) // 仅作用于方法 Retention(RetentionPolicy.RUNTIME) public interface Log { // 自定义操作描述 String value() default ; }Controller 接口添加注解标记PostMapping(/dept) Log(新增部门信息) public ResultDept save(RequestBody Dept dept){ deptService.save(dept); return Result.success(); }切面切点匹配注解Pointcut(annotation(com.itheima.anno.Log)) public void logPoint(){}二、核心配套技术ThreadLocal 线程本地存储2.1 什么是 ThreadLocalThreadLocal 线程局部变量为每一条请求线程分配独立私有存储空间线程之间数据完全隔离、互不干扰。同一 HTTP 请求全程共用一条线程过滤器→Controller→Service→AOP 切面全程共享登录用户 ID无需层层传参。2. 三大核心 APIset(T value)向当前线程存入数据get()读取当前线程存储的数据remove()清空当前线程数据防止内存泄漏必须执行2. 封装全局登录用户工具类public class UserContext { // 静态ThreadLocal存储登录员工Long类型ID private static final ThreadLocalLong USER_THREAD_LOCAL new ThreadLocal(); // 设置当前登录人ID public static void setUserId(Long userId){ USER_THREAD_LOCAL.set(userId); } // 获取当前登录人ID public static Long getUserId(){ return USER_THREAD_LOCAL.get(); } // 释放资源防止内存泄漏 public static void clear(){ USER_THREAD_LOCAL.remove(); } }2. 完整数据透传流程TokenFilter 过滤器拦截请求解析请求头 JWT 令牌提取登录人 ID调用UserContext.setUserId(登录ID)存入当前线程Controller、Service、AOP 切面任意位置通过UserContext.getUserId()获取操作人请求执行完毕过滤器 finally 块调用clear()清除线程数据。三、完整操作日志切面代码实现前置准备数据库创建 operate_log 操作日志表存储需求中全部字段OperateLog 实体类、OperateLogMapper 持久层编写 insert 插入方法引入 JSON 工具包用于参数、返回值序列化存储。完整切面类import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import com.alibaba.fastjson2.JSON; Slf4j Component Aspect public class OperateLogAspect { // 注入日志Mapper插入数据库 Autowired private OperateLogMapper logMapper; // 批量匹配增删改接口切点 Pointcut(execution(* com.itheima.controller.*.save(..)) || execution(* com.itheima.controller.*.update(..)) || execution(* com.itheima.controller.*.delete(..))) public void logPoint(){} Around(logPoint()) public Object recordOperateLog(ProceedingJoinPoint pjp) throws Throwable { // 1. 记录接口开始执行时间 long start System.currentTimeMillis(); // 2. 获取线程内登录操作人 Long operateUserId UserContext.getUserId(); // 3. 提取目标类、方法、请求参数 String className pjp.getTarget().getClass().getName(); String methodName pjp.getSignature().getName(); Object[] args pjp.getArgs(); Object result null; try { // 执行原始接口业务逻辑 result pjp.proceed(); } catch (Throwable throwable) { // 出现异常先抛出日志照常记录 throw throwable; } // 4. 计算接口执行耗时 long costTime System.currentTimeMillis() - start; // 5. 封装日志实体 OperateLog operateLog new OperateLog(); operateLog.setOperateUser(operateUserId); operateLog.setOperateTime(LocalDateTime.now()); operateLog.setClassName(className); operateLog.setMethodName(methodName); operateLog.setMethodParams(JSON.toJSONString(args)); operateLog.setReturnResult(JSON.toJSONString(result)); operateLog.setCostTime(costTime); // 6. 插入数据库持久化日志 logMapper.insert(operateLog); // 返回接口原始结果 return result; } }四、两种切点方案对比选型方案实现方式优点缺点适用场景execution 批量匹配匹配 save/update/delete 方法无需修改 Controller全局自动记录命名必须规范方法改名会失效后台管理系统所有增删改都需要留痕Annotation 注解自定义标记注解灵活控制只需要给目标接口加注解新增接口需手动添加注解仅少量关键业务需要操作日志五、ThreadLocal 开发注意事项内存泄漏风险请求结束必须调用remove()清空线程数据Tomcat 线程池会复用线程旧数据残留会造成登录 ID 错乱仅适用于单线程同一请求内数据共享异步子线程无法获取主线程 ThreadLocal 数据工具类方法统一静态封装项目全局统一调用避免多处创建 ThreadLocal 实例。六、下篇全文完整总结企业核心 AOP 实战使用 Around 环绕通知实现全局操作日志入库两种切点写法execution 批量拦截、自定义 Log 注解按需拦截ThreadLocal 作用同一请求线程共享登录用户 ID解决多层代码传参冗余ThreadLocal 三核心方法set 存入、get 读取、remove 释放完整业务链路过滤器解析 JWT 存入线程 → AOP 读取操作人 → 日志入库开发避坑线程复用必须清除 ThreadLocal否则数据错乱、内存泄漏。下篇拓展实操练习创建操作日志数据表编写实体类与 Mapper 插入方法封装 UserContext 线程工具类在过滤器完成登录 ID 存储编写完整日志切面测试新增、修改、删除接口自动入库日志改造为注解切点模式仅标记接口记录操作日志。下篇面试高频考点记录操作日志为什么优先选择 Around 环绕通知ThreadLocal 作用、使用场景不执行 remove 会有什么问题execution 和注解切点分别适合什么业务场景一条 HTTP 请求中ThreadLocal 数据传递完整流程AOP 日志切面可以获取到哪些接口信息。