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 是完美联动的,完整流程如下:
用户登录:登录接口校验成功后,调用
CurrentHolder.setCurrentId(用户ID),把当前登录用户ID存入 ThreadLocal。接口请求:用户访问加了
@Olog注解的接口,AOP 切面拦截该方法。日志记录:AOP 切面中,调用
CurrentHolder.getCurrentId(),获取当前登录用户ID,作为操作人存入日志。请求结束:在拦截器的
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 开发中的经典用法,总结如下:
ThreadLocal 负责“存储”:存储当前登录用户ID等全局共享、线程私有的数据,供全项目随时获取。
AOP 负责“增强”:拦截指定方法,通过 ThreadLocal 获取所需数据(如用户ID),实现通用逻辑(如操作日志)的统一增强,不侵入业务代码。
核心价值:简化代码、降低耦合、提高可维护性,比如你新增一个需要记录日志的接口,只需加一个
@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 实战中不可或缺的两个工具,掌握两者的核心知识点和联动用法,能大幅提升开发效率,减少冗余代码。本文结合实际项目代码拆解,避开常见坑点,适合新手入门和开发者复习,收藏备用~
