基于mybatis拦截器和数据库配置记录操作日志

Posted by William(王明高) Blog on March 2, 2025

基于 MyBatis 拦截器实现操作日志记录的完整方案如下,结合了数据库配置和注解扩展性,适用于需要精准记录 SQL 执行细节的场景:


一、背景需求

  1. 痛点场景

    • 需记录完整 SQL 语句(含参数替换后的可执行 SQL)
    • 需关联业务模块、操作类型、执行结果等元数据
    • 传统 AOP 方案难以获取底层 SQL 执行细节,MyBatis 拦截器可直接拦截 Executorupdate/query 方法
  2. 技术选型

    • 拦截器类实现 org.apache.ibatis.plugin.Interceptor
    • 使用 @Intercepts 注解声明拦截目标(如 Executor.class
    • 结合自定义注解标记需记录的方法

二、日志表结构设计(示例)

CREATE TABLE sys_oper_log (
    id BIGINT PRIMARY KEY COMMENT '主键',
    module VARCHAR(50) COMMENT '业务模块',
    type VARCHAR(20) COMMENT '操作类型(INSERT/UPDATE/DELETE/SELECT)',
    description VARCHAR(255) COMMENT '操作描述',
    sql_text TEXT COMMENT '完整SQL语句',
    params TEXT COMMENT '请求参数',
    result VARCHAR(20) COMMENT '执行结果(SUCCESS/FAIL)',
    operate_time DATETIME COMMENT '操作时间',
    user_id BIGINT COMMENT '操作人ID'
);

三、核心实现代码

1. 自定义注解 @LogOperation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
    String module() default "";    // 业务模块名称
    String type() default "QUERY"; // 操作类型
    boolean recordParams() default true; // 是否记录参数
}

2. MyBatis 拦截器实现

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class OperLogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Method method = getTargetMethod(ms); // 通过反射获取Mapper方法
        
        LogOperation logAnnotation = method.getAnnotation(LogOperation.class);
        if (logAnnotation == null) return invocation.proceed();

        Object result = null;
        OperLog log = new OperLog(); // 日志实体
        try {
            Object parameter = invocation.getArgs()[1];
            BoundSql boundSql = ms.getBoundSql(parameter);
            
            // 记录原始SQL及参数
            log.setSqlText(boundSql.getSql());
            if (logAnnotation.recordParams()) {
                log.setParams(parseParams(boundSql.getParameterObject()));
            }
            
            result = invocation.proceed(); // 执行原操作
            log.setResult("SUCCESS");
        } catch (Exception e) {
            log.setResult("FAIL");
            throw e;
        } finally {
            log.setOperateTime(new Date());
            log.setModule(logAnnotation.module());
            log.setType(logAnnotation.type());
            asyncSaveLog(log); // 异步保存日志
        }
        return result;
    }

    // 其他辅助方法(注册拦截器、参数解析等)
}

四、关键配置步骤

1. 注册拦截器(Spring Boot)

@Configuration
public class MybatisConfig {

    @Bean
    public OperLogInterceptor operLogInterceptor() {
        return new OperLogInterceptor();
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.addInterceptor(operLogInterceptor());
    }
}

2. 业务层使用示例

@LogOperation(module = "用户管理", type = "INSERT")
public int insertUser(User user) {
    return userMapper.insert(user);
}

五、高级特性扩展

  1. 敏感数据脱敏parseParams 方法中对敏感字段(如手机号、身份证)进行掩码处理:

    private String parseParams(Object paramObj) {
        if (paramObj instanceof User) {
            User user = (User) paramObj;
            user.setPassword(null); // 清除敏感字段
            user.setIdCard(DesensitizeUtil.idCard(user.getIdCard())); // 脱敏处理
        }
        return JSON.toJSONString(paramObj);
    }
    
  2. 异步存储优化 使用 @Async 或消息队列异步写入日志,避免影响主业务流程:

    @Async
    public void asyncSaveLog(OperLog log) {
        operLogMapper.insert(log);
    }
    

六、业务实践效果

日志示例输出:

{
  "module": "订单管理",
  "type": "UPDATE",
  "sqlText": "UPDATE order SET status=? WHERE order_no=?",
  "params": {"status":2, "orderNo":"202403020001"},
  "result": "SUCCESS",
  "operateTime": "2025-03-02 18:50:00"
}

注意事项

  1. 拦截器执行顺序 通过 @Order 控制多个拦截器的执行顺序,避免日志记录被其他插件影响
  2. 动态数据源适配 若使用多数据源,需在拦截器中通过 DataSourceHolder 判断当前数据源
  3. 性能监控 建议添加 @Pointcut 的执行耗时统计,确保拦截器不会成为性能瓶颈