记录MyBatis-Plus使用MyBatis拦截器进行增删改操作记录

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 配置中
        };
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值