基于 MyBatis 拦截器实现操作日志记录的完整方案如下,结合了数据库配置和注解扩展性,适用于需要精准记录 SQL 执行细节的场景:
一、背景需求
-
痛点场景
- 需记录完整 SQL 语句(含参数替换后的可执行 SQL)
- 需关联业务模块、操作类型、执行结果等元数据
- 传统 AOP 方案难以获取底层 SQL 执行细节,MyBatis 拦截器可直接拦截
Executor
的update/query
方法
-
技术选型
- 拦截器类实现
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);
}
五、高级特性扩展
-
敏感数据脱敏 在
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); }
-
异步存储优化 使用
@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"
}
注意事项
- 拦截器执行顺序
通过
@Order
控制多个拦截器的执行顺序,避免日志记录被其他插件影响 - 动态数据源适配
若使用多数据源,需在拦截器中通过
DataSourceHolder
判断当前数据源 - 性能监控
建议添加
@Pointcut
的执行耗时统计,确保拦截器不会成为性能瓶颈