当前位置: 首页 > news >正文

SpringBoot 实战必备:AOP + ThreadLocal 核心知识点(附实战代码)

在 SpringBoot 项目开发中,AOP(面向切面编程)和 ThreadLocal 是高频实用技术,尤其在日志记录、用户上下文传递等场景中不可或缺。本文结合实际项目代码(操作日志切面 + 登录用户ID存储),整理两者核心知识点、实战场景及注意事项,适合新手入门和开发者巩固复习。

一、AOP 核心知识点(实战重点版)

1. 什么是 AOP

AOP(Aspect Oriented Programming,面向切面编程),核心是不修改原有业务代码,通过“切面”对方法进行统一增强(如日志、事务、权限校验),实现“关注点分离”——业务代码只关注业务逻辑,通用逻辑(如日志)抽离到切面中,降低耦合。

底层实现:基于动态代理(JDK 动态代理:针对接口;CGLIB 动态代理:针对类,无接口时使用),SpringBoot 自动集成 AspectJ,无需手动配置代理。

2. 核心概念(必背,结合实战理解)

结合本文实战案例(操作日志切面),用通俗的语言解释 6 个核心概念,拒绝晦涩:

  • 连接点(JoinPoint):所有能被增强的方法(比如你项目中 Controller 里的所有接口方法),是“可能被拦截”的点。

  • 切入点(Pointcut):实际被拦截的方法,通过表达式筛选。比如案例中@Around("@annotation(com.itymy.anno.Olog)"),就是筛选出所有加了@Olog注解的方法。

  • 通知(Advice):增强的逻辑,也是切面的核心代码。常用 5 种,重点掌握环绕通知:

    • @Before:方法执行前执行(如权限校验)

    • @AfterReturning:方法正常返回后执行(如日志记录)

    • @AfterThrowing:方法抛异常后执行(如异常日志)

    • @After:方法最终执行(无论正常/异常,如资源释放)

    • @Around:环绕方法执行(最强大,可控制方法执行前后、是否放行,案例中就是用这个)

  • 切面(Aspect):切入点 + 通知的组合,就是一个完整的切面类(如案例中的OperationLogAspect),用@Aspect注解声明。

  • 目标对象(Target):被拦截、被增强的原始业务对象(如案例中被@Olog注解标记的 Controller 方法所在的类)。

  • 织入(Weaving):Spring 把切面代码嵌入到目标方法中的过程(自动完成,无需手动操作)。

3. 实战核心(结合你的操作日志切面)

你的 AOP 切面是「操作日志自动记录」,核心逻辑对应 AOP 知识点,拆解如下:

// 1. 声明切面(@Aspect)+ 交给Spring管理(@Component) @Slf4j @Aspect @Component public class OperationLogAspect { @Autowired private OperateLogMapper operateLogMapper; // 2. 切入点:筛选所有加了@Olog注解的方法 @Around("@annotation(com.itymy.anno.Olog)") // 3. 环绕通知:ProceedingJoinPoint 代表被拦截的目标方法 public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 4. 放行,执行业务方法(joinPoint.proceed()),并获取返回值 Object result = joinPoint.proceed(); long costTime = System.currentTimeMillis() - startTime; // 5. 增强逻辑:拼装日志、存入数据库 OperateLog operateLog = new OperateLog(); operateLog.setOperateEmpId(CurrentHolder.getCurrentId()); // 从ThreadLocal获取用户ID operateLog.setOperateTime(LocalDateTime.now()); operateLog.setClassName(joinPoint.getTarget().getClass().getName()); // 目标类名 operateLog.setMethodName(joinPoint.getSignature().getName()); // 目标方法名 operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs())); // 方法参数 operateLog.setReturnValue(result.toString()); // 方法返回值 operateLog.setCostTime(costTime); operateLogMapper.insert(operateLog); return result; // 必须返回目标方法的原始返回值,保证接口正常响应 } }

4. 关键注意事项(避坑重点)

  • @Around 环绕通知,必须调用joinPoint.proceed()才能放行,否则业务方法不会执行;且必须返回目标方法的返回值(如案例中的return result),否则前端会接收不到响应。

  • 切入点表达式要精准,避免误拦截(如案例中用自定义注解,只拦截需要记录日志的方法,比拦截所有Controller方法更灵活)。

  • 切面中如果有数据库操作、远程调用等耗时操作,建议异步处理,避免影响主业务接口的响应速度。

  • SpringBoot 中无需手动添加@EnableAspectJAutoProxy,引入spring-boot-starter-aop依赖后,自动开启 AOP 支持。

二、ThreadLocal 核心知识点(实战重点版)

1. 什么是 ThreadLocal

ThreadLocal 是 Java 提供的一种线程本地存储工具,核心作用:为每个线程维护一个独立的变量副本,线程之间的变量互不干扰,实现“线程安全”的变量共享(仅当前线程可见)。

通俗理解:每个线程都有一个“专属小盒子”,ThreadLocal 就是这个盒子的管理者,你可以往盒子里存数据、取数据,其他线程看不到你盒子里的内容,也不会修改你的数据。

核心场景:SpringBoot 中存储当前登录用户信息(如用户ID),在全项目中(AOP、Service、Controller)随时获取,无需手动传参(如你项目中的CurrentHolder工具类)。

2. 核心方法(结合你的 CurrentHolder 工具类)

ThreadLocal 常用 3 个方法,你的工具类全部用到,对应如下:

public class CurrentHolder { // 1. 初始化 ThreadLocal,指定存储类型(这里是当前登录用户ID,Integer类型) private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>(); // 2. 存入数据:登录校验成功后,把用户ID存入ThreadLocal public static void setCurrentId(Integer employeeId) { CURRENT_LOCAL.set(employeeId); } // 3. 取出数据:在AOP、Service等地方,获取当前线程的用户ID public static Integer getCurrentId() { return CURRENT_LOCAL.get(); } // 4. 移除数据:请求结束后,必须删除,防止内存泄漏 public static void remove() { CURRENT_LOCAL.remove(); } }

补充说明 3 个核心方法的细节:

  • set(T value):给当前线程的 ThreadLocal 副本设置值,仅当前线程可见。

  • get():获取当前线程的 ThreadLocal 副本中的值,若未设置,返回 null。

  • remove():删除当前线程的 ThreadLocal 副本中的值,必须调用(重点避坑)。

3. 实战场景(和 AOP 联动)

你的项目中,ThreadLocal 和 AOP 是完美联动的,完整流程如下:

  1. 用户登录:登录接口校验成功后,调用CurrentHolder.setCurrentId(用户ID),把当前登录用户ID存入 ThreadLocal。

  2. 接口请求:用户访问加了@Olog注解的接口,AOP 切面拦截该方法。

  3. 日志记录:AOP 切面中,调用CurrentHolder.getCurrentId(),获取当前登录用户ID,作为操作人存入日志。

  4. 请求结束:在拦截器的afterCompletion方法中,调用CurrentHolder.remove(),删除 ThreadLocal 中的数据,防止内存泄漏。

核心优势:无需在每个接口、每个方法中手动传递用户ID,通过 ThreadLocal 实现“全局共享、线程隔离”,简化代码,提高可维护性。

4. 关键注意事项(避坑重中之重)

  • 内存泄漏问题(最容易踩坑):ThreadLocal 底层依赖 ThreadLocalMap,若不调用remove(),线程结束后(如 Tomcat 线程池复用),ThreadLocal 中的数据不会自动清理,会导致内存泄漏。解决方案:在请求结束后(拦截器 afterCompletion、过滤器 doFilter 最后),必须调用remove()

  • 线程隔离性:ThreadLocal 中的数据仅当前线程可见,不同线程之间的数据互不干扰,适合存储“线程私有”的数据(如当前登录用户、请求ID等)。

  • 静态修饰 ThreadLocal:一般把 ThreadLocal 定义为 static 变量(如你的 CurrentHolder),避免每次创建对象时重复初始化,保证全局只有一个 ThreadLocal 实例管理线程变量。

  • null 处理:调用get()时,可能返回 null(如未登录、未设置值),需做非空判断,避免空指针异常(比如你的 AOP 中,若用户未登录,CurrentHolder.getCurrentId() 会返回 null,存入数据库时需处理)。

三、AOP + ThreadLocal 联动总结(实战核心)

结合你的项目代码,两者的联动是 SpringBoot 开发中的经典用法,总结如下:

  1. ThreadLocal 负责“存储”:存储当前登录用户ID等全局共享、线程私有的数据,供全项目随时获取。

  2. AOP 负责“增强”:拦截指定方法,通过 ThreadLocal 获取所需数据(如用户ID),实现通用逻辑(如操作日志)的统一增强,不侵入业务代码。

  3. 核心价值:简化代码、降低耦合、提高可维护性,比如你新增一个需要记录日志的接口,只需加一个@Olog注解,无需修改接口代码,AOP 会自动完成日志记录,用户ID也会通过 ThreadLocal 自动获取。

四、补充说明

1. 依赖说明:使用 AOP 需引入 SpringBoot AOP 依赖(可带版本号,避免依赖冲突):

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>3.2.5</version> <!-- 对应SpringBoot 3.x + JDK17,按需调整 --> </dependency>

2. 自定义注解 Olog:需配合 AOP 切入点使用,代码如下(完整度拉满):

@Target(ElementType.METHOD) // 仅作用于方法 @Retention(RetentionPolicy.RUNTIME) // 运行时生效 public @interface Olog { String value() default ""; // 可自定义日志描述,如@Olog("删除部门") }

3. 适用场景:除了操作日志,AOP + ThreadLocal 还可用于:权限校验、接口耗时统计、数据脱敏、请求追踪等场景。

结语:AOP 和 ThreadLocal 是 SpringBoot 实战中不可或缺的两个工具,掌握两者的核心知识点和联动用法,能大幅提升开发效率,减少冗余代码。本文结合实际项目代码拆解,避开常见坑点,适合新手入门和开发者复习,收藏备用~

http://www.jsqmd.com/news/658319/

相关文章:

  • 深度解析MIST显微图像拼接工具:从原理到实战的高效拼接方案
  • 保姆级教程:用Android Studio和Socket实现手机传感器数据实时传输到电脑(附完整代码)
  • 从相机到屏幕:深入解析图形渲染管线中的MVP与视口变换
  • 从手机拍照到AI修图:手把手教你用Python和PyTorch搭建自己的无参考图像质量评估(NR-IQA)模型
  • 别再盲目扩大context window!:用语义蒸馏+调用链图谱+领域实体对齐,将上下文有效利用率提升6.8倍(实测数据)
  • 状态机在自动驾驶中的5个常见设计误区及如何避免
  • 当EPICS遇上物联网:手把手教你用MQTT-CA桥接器打通工业数据流
  • 【TensorRT】—— 动态Batch推理实战:从模型导出到trtexec性能深度解析
  • 【学员故事】源源:从无人听到争相咨询,学习毛丫讲绘本,托育园招生很顺利
  • 节庆体验编排怎样被大模型重做,藏在 ​D​М‌X​Α‌РΙ 之后的运营方法
  • AI 设计工具:不是让 Figma 更好,是重新定义“设计“这件事
  • 云原生死亡报告:Serverless的致命成本陷阱
  • MongoDB备节点无法读取数据怎么解决_rs.slaveOk()与Secondary读取权限
  • GO并发的runtime.Gosched 有什么用(结论:没卵用了)
  • 从超声RF信号到B超图像:MATLAB实战全流程解析与优化
  • 【硬件进阶】DRC零报错却沦为废砖?PCB设计中价值千金的4个“致命雷区”
  • AutoSAR RTE实战:手把手教你配置SWC通信(含S/R与C/S模式对比)
  • 基于R语言的物种气候生态位动态量化与分布特征模拟实践技术
  • 如何用OpenSTA解决复杂芯片设计中的时序收敛难题
  • OpenCV DNN模块实战:5分钟搞定图片风格迁移(附完整代码)
  • 3大零代码平台教你用AI智能体,轻松实现自动化效率提升!
  • 监控通道太多查不过来?国标GB28181视频平台EasyGBS视频质量诊断支持轮询模式,省心太多了
  • 8G显存就能跑的视频抠图工具,发丝级精度,免费开源 | MatAnyone2 完整安装使用教程
  • 告别盲操!深入理解S/4 HANA中MARC、MBEW表的CDS代理视图与增强逻辑
  • 互联网大厂Java面试:Spring Boot/Redis/Kafka/K8s 可观测 + RAG(向量检索/Agent)三轮追问实录
  • RabbitMQ实战:流控机制(Flow Control)全解析——原理、触发、流程与实战
  • 告别AI幻觉:用ReAct模式手把手教你构建一个会‘查资料’的智能问答助手
  • 保姆级教程:在Orange Pi 5 Max上从零配置ROS+PX4无人机仿真环境(Ubuntu 20.04)
  • 多通道热红外辐射计温度系数校准研究
  • 如何快速批量保存小红书无水印内容:XHS-Downloader完整指南