MyBatisPlus 自定义拦截器:全面记录 SQL 执行日志

背景

在使用 MyBatisPlus 时,在面对复杂的动态 SQL 或多个参数的情况下,SQL 语句和其绑定的参数可能不容易直接查看,导致排查问题时非常麻烦。每次遇到 SQL 执行异常,开发人员不得不手动查看 SQL 文件或在代码中打印参数,增加了排查和修复的时间。

解决方法

写一个拦截器,所有 SQL 查询和更新的过程都会自动记录,并且每个占位符 ? 都会被替换为实际的参数值。这样,开发者可以通过日志清晰地看到完整的 SQL 语句和对应的参数,极大地方便了 SQL 调试和错误排查。

项目中使用

自定义拦截器->注册拦截器

自定义拦截器:

import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.sql.SQLException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;

public class MybatisPlusAllSqlLog implements InnerInterceptor {
    // 记录 SQL 日志的日志器
    public static final Logger log = LoggerFactory.getLogger("sys-sql");

    /**
     * 查询前的拦截
     *
     * @param executor      Executor实例
     * @param ms            MappedStatement实例
     * @param parameter     查询参数
     * @param rowBounds     分页信息
     * @param resultHandler 结果处理器
     * @param boundSql      BoundSql实例
     * @throws SQLException SQL异常
     */
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        logInfo(boundSql, ms, parameter);
    }

    /**
     * 更新前的拦截
     *
     * @param executor  Executor实例
     * @param ms        MappedStatement实例
     * @param parameter 更新参数
     * @throws SQLException SQL异常
     */
    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        logInfo(boundSql, ms, parameter);
    }

    /**
     * 日志打印的核心逻辑
     *
     * @param boundSql  BoundSql实例
     * @param ms        MappedStatement实例
     * @param parameter 参数对象
     */
    private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {
        try {
            log.info("parameter = {}", parameter);  // 使用占位符方式更为安全且性能优越
            String sqlId = ms.getId();  // 获取SQL的ID
            log.info("sqlId = {}", sqlId);
            Configuration configuration = ms.getConfiguration();  // 获取配置对象
            String sql = getSql(configuration, boundSql, sqlId);  // 获取最终的SQL语句
            log.info("完整的sql: {}", sql);
        } catch (Exception e) {
            log.error("异常: {}", e.getLocalizedMessage(), e);  // 捕获异常并打印
        }
    }

    /**
     * 封装SQL语句信息,返回SQL节点id和对应的SQL语句
     *
     * @param configuration 配置对象
     * @param boundSql      BoundSql实例
     * @param sqlId         SQL节点ID
     * @return 完整的SQL信息
     */
    public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
        return sqlId + ":" + showSql(configuration, boundSql);  // 返回完整的sql信息
    }

    /**
     * 替换SQL语句中的占位符?为实际的参数值
     *
     * @param configuration 配置对象
     * @param boundSql      BoundSql实例
     * @return 替换后的SQL语句
     */
    public static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();  // 获取参数对象
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  // 获取SQL中所有的参数映射
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");  // 清除SQL中的多余空格

        // 如果存在参数且参数对象不为空,则替换占位符
        if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();  // 获取类型处理器注册器
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);  // 获取MetaObject,用于获取参数属性
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        // 动态SQL中的额外参数
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        // 如果缺少参数,打印“缺失”
                        sql = sql.replaceFirst("\\?", "缺失");
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 将参数对象转化为对应的SQL表示形式(例如:字符串加引号,日期加引号等)
     *
     * @param obj 参数对象
     * @return 转换后的参数值
     */
    private static String getParameterValue(Object obj) {
        if (obj == null) {
            return "NULL";  // 处理null情况
        }

        if (obj instanceof String) {
            return "'" + obj.toString() + "'";  // 字符串加单引号
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            return "'" + formatter.format(obj) + "'";  // 日期格式化后加单引号
        } else {
            return obj.toString();  // 其他类型直接调用toString()
        }
    }
}

注册拦截器:

@Configuration
public class MybatisConfiguration {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new MybatisPlusAllSqlLog());
        return mybatisPlusInterceptor;
    }
}

自定义拦截器主要方法和逻辑

MybatisPlusAllSqlLog 是一个自定义的 MyBatis Plus 拦截器,它实现了 InnerInterceptor 接口。它的主要功能是拦截所有 MyBatis Plus 的 SQL 执行操作。

  • beforeQuerybeforeUpdate 方法:

    • 在查询和更新操作之前调用,拦截 SQL 执行过程。
    • 使用 logInfo 方法打印 SQL 相关信息。
  • logInfo 方法:

    • 获取 SQL 的 ID (sqlId),这有助于定位 SQL 语句对应的 Mapper 方法。
    • 使用 getSql 方法获取最终执行的 SQL 语句,并将其打印出来。
  • getSqlshowSql 方法:

    • getSql:封装 SQL 的 sqlId 和实际的 SQL 语句,输出完整的 SQL 信息。
    • showSql:替换 SQL 语句中的占位符 ? 为实际的参数值。首先检查参数是否为空,然后根据参数类型(如 StringDate)替换为合适的格式。
  • getParameterValue 方法:

    • 将参数对象转换为相应的 SQL 格式,例如将字符串加上单引号,将日期格式化并加上单引号。
    • 处理 null 的情况,输出 "NULL"

自定义拦截器关键点

  1. 日志打印:使用 Logger 打印 SQL 日志,包括 SQL ID、SQL 语句和参数。
  2. 替换占位符:在日志中打印替换后的 SQL 语句,使其更容易理解执行的实际操作。
  3. 适配不同类型的参数:根据参数类型(如字符串、日期等)生成适当的 SQL 字符串表示,确保日志清晰且无歧义。

效果

在这里插入图片描述


❤觉得有用的可以留个关注~~❤

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值