1.创建操作记录实体,并添加记录表到数据库
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("SQL_LOG")
public class SqlLog{
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键")
@TableId(value = "ID", type = IdType.ASSIGN_UUID)
private String id;
@ApiModelProperty(value = "操作类型:INSERT/UPDATE/DELETE")
@TableField("OPERATION_TYPE")
private String operationType;
@ApiModelProperty(value = "执行的SQL")
@TableField("SQL_STATEMENT")
private String sqlStatement;
@ApiModelProperty(value = "参数(JSON)")
@TableField("SQL_PARAMS")
private String sqlParams;
@ApiModelProperty(value = "执行时间")
@TableField("EXECUTE_TIME")
private Date executeTime;
@ApiModelProperty(value = "执行人")
@TableField("EXECUTE_USER")
private String executeUser;
@ApiModelProperty(value = "执行IP")
@TableField("EXECUTE_IP")
private String executeIp;
@ApiModelProperty(value = "SUCCESS/FAIL")
@TableField("EXECUTE_RESULT")
private String executeResult;
@ApiModelProperty(value = "错误信息")
@TableField("ERROR_MSG")
private String errorMsg;
@ApiModelProperty(value = "操作的表名")
@TableField("TABLE_NAME")
private String tableName;
@ApiModelProperty(value = "执行耗时(毫秒)")
@TableField("COST_TIME")
private Long costTime;
}
2.编写对应mapper
@Mapper
public interface SqlLogMapper extends BaseMapper<SqlLog> {
}
3.创建静态注入 Mapper 工具类
/**
* 静态注入 Mapper 工具类
*/
@Component
public class SqlLogMapperStaticHolder {
// 静态变量存储 Mapper
public static SqlLogMapper SQL_LOG_MAPPER;
// Spring 初始化时,将注入的 Mapper 赋值给静态变量
@Autowired
private void setDataSynchronizationMapper(SqlLogMapper mapper) {
SqlLogMapperStaticHolder.SQL_LOG_MAPPER = mapper;
}
}
4.创建SQL 记录拦截器
/**
* 兼容 MyBatis-Plus 的 SQL 记录拦截器
* 仅拦截 Executor.update(增删改),不影响 MP 分页/乐观锁插件
*/
@Intercepts({
// 仅拦截 Executor 的 update 方法(增删改),避免干扰查询/分页
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class MybatisSqlRecordInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(MybatisSqlRecordInterceptor.class);
// 排除 MP 内置 Mapper/日志表 Mapper,避免循环拦截
private static final List<String> EXCLUDE_MAPPER = Arrays.asList("SqlLogMapper", "MybatisMapper");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 延迟加载日志 Mapper,避免循环依赖
private volatile SqlLogMapper sqlLogMapper;
/**
* 核心拦截逻辑:先执行原方法,后解析 SQL(避免破坏 MP 流程)
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取拦截参数(MP 兼容:MappedStatement 是 MP 增强后的对象,可直接使用)
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 2. 过滤:排除 MP 内置 Mapper/日志表 Mapper/非增删改操作
if (!needRecord(ms, parameter)) {
return invocation.proceed(); // 执行原方法,不记录
}
SqlLog sqlLog = new SqlLog();
String realSql = "SQL解析失败";
String realParams = "{}";
long startTime = System.currentTimeMillis();
Object result = null;
try {
// 3. 优先执行原方法(MP 核心:先让分页/乐观锁插件处理,再解析)
result = invocation.proceed();
// 4. 执行后解析 BoundSql(此时 MP 已完成 SQL 拼接,安全无 OGNL 异常)
BoundSql boundSql = safeGetBoundSql(ms, parameter);
if (boundSql != null) {
realSql = parseValidSql(ms.getConfiguration(), boundSql);
realParams = parseValidParams(ms.getConfiguration(), boundSql);
}
// 5. 填充日志(仅读取,不修改任何 MP 核心对象)
sqlLog.setOperationType(ms.getSqlCommandType().name());
sqlLog.setTableName(getTableName(ms));
sqlLog.setSqlStatement(realSql);
sqlLog.setSqlParams(realParams);
sqlLog.setExecuteUser("system");// TODO:更改为系统获取的登录人
sqlLog.setExecuteIp(getClientIp());
sqlLog.setExecuteTime(new Date());
sqlLog.setCostTime(System.currentTimeMillis() - startTime);
sqlLog.setExecuteResult("SUCCESS");
} catch (Exception e) {
// 6. 异常仅记录,不阻断 MP 流程
log.error("SQL 记录失败:{}", e.getMessage());
sqlLog.setExecuteResult("FAIL");
sqlLog.setErrorMsg(e.getMessage());
throw e; // 抛出原异常,保证业务逻辑不受影响
} finally {
// 7. 写入日志(捕获所有异常,避免影响主流程)
writeSqlLog(sqlLog);
}
return result;
}
/**
* 仅包装 Executor 组件(避免包装 MP 依赖的其他组件,如 ParameterHandler)
*/
@Override
public Object plugin(Object target) {
if (target instanceof Executor) { // 仅对 Executor 生成代理
return Plugin.wrap(target, this);
}
return target; // 其他组件直接返回,不干扰 MP
}
@Override
public void setProperties(Properties properties) {
}
// ------------------------------ 辅助方法(兼容 MP)------------------------------
/**
* 判断是否需要记录日志:排除 MP 内置 Mapper/非增删改/参数为空
*/
private boolean needRecord(MappedStatement ms, Object parameter) {
if (parameter == null) return false;
String mapperId = ms.getId();
// 排除 MP 内置 Mapper 和日志表 Mapper
for (String exclude : EXCLUDE_MAPPER) {
if (mapperId.contains(exclude)) return false;
}
// 仅记录增删改
SqlCommandType type = ms.getSqlCommandType();
return type == SqlCommandType.INSERT || type == SqlCommandType.UPDATE || type == SqlCommandType.DELETE;
}
/**
* 安全获取 BoundSql:兼容 MP 动态生成的 SQL
*/
private BoundSql safeGetBoundSql(MappedStatement ms, Object parameter) {
try {
return ms.getBoundSql(parameter);
} catch (Exception e) {
log.warn("解析 BoundSql 失败:{}", e.getMessage());
return null;
}
}
/**
* 解析 SQL:兼容 MP 分页/动态 SQL 拼接后的结果
*/
private String parseValidSql(Configuration config, BoundSql boundSql) {
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
Object paramObj = boundSql.getParameterObject();
List<ParameterMapping> paramMappings = boundSql.getParameterMappings();
if (paramMappings.isEmpty() || paramObj == null) {
return sql;
}
// MP 兼容:使用 MP 增强的 MetaObject 读取参数
MetaObject metaObj = config.newMetaObject(paramObj);
TypeHandlerRegistry typeHandlerRegistry = config.getTypeHandlerRegistry();
for (ParameterMapping pm : paramMappings) {
String paramName = pm.getProperty();
Object paramValue = null;
// 兼容 MP 的 Param 注解和分页参数
if (metaObj.hasGetter(paramName)) {
paramValue = metaObj.getValue(paramName);
} else if (boundSql.hasAdditionalParameter(paramName)) {
paramValue = boundSql.getAdditionalParameter(paramName);
}
// 格式化参数(null → 'null',避免空占位符)
String valueStr = formatParamValue(paramValue);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(valueStr));
}
// 修复 MP 拼接 SQL 可能出现的空占位符
return repairSql(sql);
}
/**
* 格式化参数:兼容 MP 分页参数/日期类型
*/
private String formatParamValue(Object value) {
if (value == null) return "null";
if (value instanceof String) return "'" + ((String) value).replace("'", "''") + "'";
// 兼容 MP 的分页参数(Page 对象)
if (value.getClass().getName().contains("com.baomidou.mybatisplus.extension.plugins.pagination.Page")) {
return "'Page参数'";
}
// 日期类型统一格式化
if (value instanceof Date) {
LocalDateTime ldt = ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
return "'" + DATE_FORMATTER.format(ldt) + "'";
}
if (value instanceof LocalDate || value instanceof LocalDateTime) {
return "'" + "'";
}
return value.toString();
}
/**
* 修复 SQL 语法:兼容 MP 拼接的 SQL
*/
private String repairSql(String sql) {
sql = sql.replaceAll("\\(\\s*,\\s*", "\\( null, ");
sql = sql.replaceAll(",\\s*,\\s*", ", null, ");
sql = sql.replaceAll(",\\s*\\)", ", null )");
return sql;
}
/**
* 解析参数为 JSON:兼容 MP 的参数格式
*/
private String parseValidParams(Configuration config, BoundSql boundSql) {
try {
Object paramObj = boundSql.getParameterObject();
return OBJECT_MAPPER.writeValueAsString(paramObj);
} catch (Exception e) {
return "{\"error\":\"参数解析失败:" + e.getMessage() + "\"}";
}
}
/**
* 获取表名:兼容 MP 的 TableInfo (可选,更精准)
*/
private String getTableName(MappedStatement ms) {
String msId = ms.getId();
// 1. 提取 Mapper 全类名
String mapperFullName = msId.substring(0, msId.lastIndexOf("."));
try {
// 2. 加载 Mapper 类
Class<?> mapperClass = Class.forName(mapperFullName);
// 3. 获取 Mapper 接口的泛型实体类(如 UserMapper extends BaseMapper<User>)
Type genericType = mapperClass.getGenericInterfaces()[0];
ParameterizedType parameterizedType = (ParameterizedType) genericType;
Class<?> entityClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
// 4. 读取 @TableName 注解
TableName tableNameAnnotation = entityClass.getAnnotation(TableName.class);
if (tableNameAnnotation != null && StringUtils.isNotBlank(tableNameAnnotation.value())) {
return tableNameAnnotation.value(); // 返回注解指定的表名(如 t_user)
}
return "unknown_table";
// 方式1:从 Mapper ID 解析(通用)
//String mapperName = ms.getId().substring(ms.getId().lastIndexOf(".") + 1);
//if (mapperName.endsWith("Mapper")) {
// mapperName = mapperName.substring(0, mapperName.length() - 6);
//}
//return StringUtils.camelToUnderline(mapperName);
// 方式2:从 MP 的 TableInfo 获取(更精准,需依赖 MP 核心包)
/*
Class<?> entityClass = ms.getParameterMap().getType();
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
return tableInfo.getTableName();
*/
} catch (Exception e) {
return "unknown_table";
}
}
/**
* 获取客户端 IP:兼容 Web 环境
*/
private String getClientIp() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
} catch (Exception e) {
log.warn("获取 IP 失败:{}", e.getMessage());
}
return "unknown_ip";
}
/**
* 写入日志:安全获取 Mapper,避免循环依赖(兼容 MP 的 Mapper 代理)
*/
private void writeSqlLog(SqlLog sqlLog) {
// 直接使用静态变量
if (SqlLogMapperStaticHolder.SQL_LOG_MAPPER == null) {
log.error("静态注入 Mapper 失败");
return;
}
try {
SqlLogMapperStaticHolder.SQL_LOG_MAPPER.insert(sqlLog);
} catch (Exception e) {
log.error("写入日志表失败:{}", e.getMessage());
}
}
}
5.进行MybatisPlus配置
@Configuration
@Slf4j
public class MybatisPlusConfig {
private MybatisSqlRecordInterceptor mybatisSqlRecordInterceptor = new MybatisSqlRecordInterceptor();
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.addInterceptor(mybatisSqlRecordInterceptor); // 注册拦截器到 MyBatis 配置中
};
}
}

1万+

被折叠的 条评论
为什么被折叠?



