基于 Spring AOP 的操作日志记录:以 DeptController 增删接口为例

一、背景

在后台管理系统中,对"部门"这类核心数据的新增、删除、修改操作往往需要有据可查------谁、在什么时间、调用了哪个方法、传入了什么参数、执行耗时多久。如果把这些日志逻辑硬编码进每个 Controller 方法,会造成大量重复代码且难以维护。

本项目采用 Spring AOP(面向切面编程) ,通过自定义注解 @Log 作为"切点标记",在方法执行前后自动织入日志记录逻辑,做到业务代码与日志代码完全解耦

二、整体架构

复制代码
HTTP 请求
   │
   ▼
┌─────────────┐   注解 @Log 标记
│ DeptController│──────────────┐
│  (增/删/改)  │               │
└─────────────┘               │
   │ proceed()                 ▼
   ▼                  ┌──────────────────┐
┌─────────────┐       │  OperateLogAspect │  @Around 环绕通知
│  DeptService │       │  (AOP 切面)       │
└─────────────┘       └──────────────────┘
                          │ 组装 OperateLog
                          ▼
                   ┌────────────────┐
                   │ OperateLogMapper│  → operate_log 表
                   └────────────────┘

三、核心组件逐一拆解

1. 自定义注解 @Log ------ 切点标记

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

这是一个标记型注解 :本身不带任何属性,只用于在目标方法上"打标签"。@Retention(RUNTIME) 保证它在运行时可通过反射被 AOP 识别。

2. 切点声明 ------ 哪里织入?

java 复制代码
@Around("@annotation(com.mlamp.anno.Log)")
public Object recordLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable

@Around + @annotation(...Log) 的组合含义是:所有被 @Log 标注的方法,都作为切点的匹配范围。这样我们只需要给想记录的方法贴注解,非常灵活。

3. 切面逻辑 ------ 记录什么?

OperateLogAspect 在方法执行前后各取一次时间戳,拿到"耗时",再收集上下文信息组装成 OperateLog 对象:

java 复制代码
long begin = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();   // 执行原业务方法
long end = System.currentTimeMillis();

OperateLog olog = new OperateLog();
olog.setOperateEmpId(getCurrentuserId());                       // 操作人
olog.setOperateTime(LocalDateTime.now());                        // 操作时间
olog.setClassName(proceedingJoinPoint.getTarget().getClass().getName()); // 类名
olog.setMethodName(proceedingJoinPoint.getSignature().getName());        // 方法名
olog.setMethodParams(Arrays.toString(proceedingJoinPoint.getArgs()));    // 入参
olog.setReturnValue(result != null ? result.toString() : "void");        // 返回值
olog.setCostTime(end - begin);                                     // 耗时

关键点proceedingJoinPoint.proceed() 是环绕通知的核心------必须调用它,原业务方法(如真正的删除逻辑)才会执行。若不调用,目标方法将被"拦腰截断"。

4. 操作人身份获取 ------ ThreadLocal 传递

java 复制代码
private Integer getCurrentuserId() {
    return Integer.parseInt(ThreadLocalUtil.get().toString());
}

切面本身无法直接拿到 HTTP 请求头里的 JWT,因此由 DemoInterceptor 拦截器 在请求入口处完成身份解析,并把操作人 ID 存入 ThreadLocalUtil

java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String token = request.getHeader("token");
    if (token == null || token.isEmpty()) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return false;                              // token 缺失,拦截
    }
    try {
        Claims claims = JwtUtil.parseToken(token); // 解析 JWT
        Integer empId = Integer.valueOf(claims.get("id").toString());
        ThreadLocalUtil.set(empId);                // ★ 将操作人 ID 存入 ThreadLocal
    } catch (Exception e) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return false;                              // token 非法,拦截
    }
    return HandlerInterceptor.super.preHandle(request, response, handler);
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    ThreadLocalUtil.remove();                       // ★ 请求结束,清理 ThreadLocal,避免内存泄漏
}

流程要点:

  • preHandle:从请求头取 token,用 JwtUtil.parseToken 解析出 Claims,取出其中的 id 作为 empId,调用 ThreadLocalUtil.set(empId) 存入当前线程。
  • postHandle:目标方法执行完毕后调用 ThreadLocalUtil.remove() 清理,防止线程池复用导致数据串用或内存泄漏。

由于一次 HTTP 请求在同一个线程内执行,AOP 切面随后便能从 ThreadLocal 读取到刚存入的 empId,从而拿到操作人身份,避免参数层层透传。ThreadLocalUtil 提供了 set / get / remove 三个核心方法,与上面拦截器的写入、清理相呼应。

5. 持久化 ------ 写库

OperateLogMapper 用 MyBatis 注解方式把日志记录落库:

java 复制代码
@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, "
      + "method_params, return_value, cost_time) "
      + "values (#{operateEmpId}, #{operateTime},#{className},#{methodName},"
      + "#{methodParams},#{returnValue},#{costTime})")
void insert(OperateLog log);

OperateLog 实体字段与表结构一一对应:operate_emp_idoperate_timeclass_namemethod_namemethod_paramsreturn_valuecost_time

四、DeptController 中的实际应用

DeptController 中,只需在目标方法上添加 @Log 即可接入日志:

java 复制代码
// 删除部门 ------ 标记 @Log
@Log
@DeleteMapping
public Result delete(Integer id){
    deptService.deleteById(id);
    return Result.success();
}

// 新增部门 ------ 标记 @Log
@Log
@PostMapping
public Result add(@RequestBody Dept dept){
    deptService.add(dept);
    return Result.success();
}

注意 list()(查询)、getDeptById()(详情)等方法未加 @Log ,因此不会被记录。这正是注解驱动切面的优势:按需记录,零侵入、可粒度控制

五、一次"删除部门"请求的完整执行流程

  1. 前端 DELETE /depts?id=5 进入 DemoInterceptor.preHandle,解析请求头中的 JWT,取出登录用户 empIdThreadLocalUtil.set(empId);token 缺失或非法则直接返回 401 拦截。
  2. 请求放行到达 DeptController.delete(5)
  3. Spring 发现该方法被 @Log 标记,将控制权交给 OperateLogAspect.recordLog
  4. 切面记录 begin 时间戳。
  5. 调用 proceed() → 真正执行 deptService.deleteById(5) 落库删除。
  6. 切面记录 end 时间戳,算出 costTime
  7. ThreadLocalUtil 取当前用户 ID(即拦截器存入的 empId),组装 OperateLog(类名 DeptController、方法名 delete、参数 [5]、返回值 Result、耗时毫秒)。
  8. operateLogMapper.insert(olog) 写入 operate_log 表,同时 log.info 打印到日志。
  9. 切面返回原方法结果,响应前端。
  10. 请求处理完毕,DemoInterceptor.postHandle 调用 ThreadLocalUtil.remove() 清理当前线程的操作人信息。

整个过程业务方法对日志逻辑完全无感知,新增/修改日志记录也无需改动业务代码。

六、小结与可优化点

优点

  • 解耦:日志逻辑与业务逻辑分离。
  • 灵活:注解驱动,按需标记,粒度可控。
  • 低侵入:业务方法零改动。

可优化点(供思考)

  • 当前 returnValue 直接 toString(),若返回值较大可能拖慢且占库;可限制长度或按需记录。
  • getReturnValueproceed() 之后获取,若业务方法抛异常则不会记录返回值和入库------可考虑补充 @AfterThrowing 记录异常日志。
  • getCurrentuserId() 强转 ThreadLocal,若拦截器未设置会 NPE;建议增加空值兜底。
相关推荐
John jj1 小时前
拆解 Telegram 群组频道收录市场:三类方案,一个可运行的评分模型,目前TG中文人工评分加模型评分机制——LetsTG收录“快速”、“无门槛”
大数据·后端·python·深度学习·搜索引擎·django·全文检索
aqi001 小时前
15天学会AI应用开发(十九)使用LangGraph实现持久记忆功能
人工智能·python·大模型·ai编程·ai应用
月光船幽幽1 小时前
KDTree查表实现观测到动作的统计映射
python
2401_868534781 小时前
论信息系统安全保障规划与设计
python·django
微小冷1 小时前
Optiland近轴光学
python·光学仿真·optiland·几何光学·近轴光学·高斯光学
databook2 小时前
基于模型的重要性评分进行“特征排序”
python·机器学习·scikit-learn
晚安code2 小时前
Java编程规范避坑指南:阿里开发手册15条强制规约实战解析
java·后端
码云骑士2 小时前
94-实战Prompt优化师Agent-迭代优化-效果评分-自动生成Few-Shot
python·prompt