当前位置: 首页 > article >正文

使用MyBatisMyBatis Plus实现SQL日志打印与执行监控

使用MyBatis/MyBatis Plus实现SQL日志打印与执行监控

一、背景与价值

在开发过程中,SQL日志的完整输出对于调试和性能优化至关重要。MyBatis默认的日志输出仅显示带占位符的SQL语句,无法直接看到实际参数值,且缺乏执行时间统计。本文将介绍两种实现方案:

  1. 原生配置方案:通过日志框架直接输出基础SQL日志
  2. 增强方案:使用MyBatis拦截器实现完整SQL打印和执行监控

二、原生配置方案(快速上手)

1. 日志框架配置(以Logback为例)

<!-- logback-spring.xml -->
<configuration>
    <logger name="com.zaxxer.hikari" level="INFO"/>
    <logger name="java.sql.Connection" level="INFO"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>
</configuration>

2. 输出示例

DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE name = ?
DEBUG [main] - ==> Parameters: John(String)

3. 局限性

  • 参数值单独显示,无法直接拼接完整SQL
  • 缺乏执行耗时统计
  • 动态SQL处理不够直观

三、增强方案:自定义拦截器实现

1. SQL美化与参数替换

public class MybatisPlusAllSqlLog implements InnerInterceptor {
    public static final Logger log = LoggerFactory.getLogger("sys-sql");

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        logInfo(boundSql, ms, parameter);
    }

    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        logInfo(boundSql, ms, parameter);
    }

    private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {
        try {
            log.info("parameter = " + parameter);
            // 获取到节点的id,即sql语句的id
            String sqlId = ms.getId();
            log.info("sqlId = " + sqlId);
            // 获取节点的配置
            Configuration configuration = ms.getConfiguration();
            // 获取到最终的sql语句
            String sql = getSql(configuration, boundSql, sqlId);
            log.info("完整的sql:{}", sql);
        } catch (Exception e) {
            log.error("异常:{}", e.getLocalizedMessage(), e);
        }
    }

    // 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
    public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
        return sqlId + ":" + showSql(configuration, boundSql);
    }

    // 进行?的替换
    public static String showSql(Configuration configuration, BoundSql boundSql) {
        // 获取参数
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // sql语句中多个空格都用一个空格代替
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
            // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果根据parameterObject.getClass()可以找到对应的类型,则替换
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?",
                        Matcher.quoteReplacement(getParameterValue(parameterObject)));
            } else {
                // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                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;
    }

    // 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理
    private static String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
                    DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }

}

2. 执行耗时监控

@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, CacheKey.class, BoundSql.class})})
public class SqlStatementInterceptor implements Interceptor {

    public static final Logger log = LoggerFactory.getLogger("sys-sql");

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long timeConsuming = System.currentTimeMillis() - startTime;
            log.info("执行SQL:{}ms", timeConsuming);
            if (timeConsuming > 999 && timeConsuming < 5000) {
                log.info("执行SQL大于1s:{}ms", timeConsuming);
            } else if (timeConsuming >= 5000 && timeConsuming < 10000) {
                log.info("执行SQL大于5s:{}ms", timeConsuming);
            } else if (timeConsuming >= 10000) {
                log.info("执行SQL大于10s:{}ms", timeConsuming);
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

自定义拦截器之后,请注意配置该拦截器

3.输出示例

INFO  [http-nio-8080-exec-1] - SQLID: com.example.mapper.UserMapper.selectById
INFO  [http-nio-8080-exec-1] - 完整SQL: SELECT id,name,age FROM user WHERE id=1
INFO  [http-nio-8080-exec-1] - 执行耗时: 48ms
WARN  [http-nio-8080-exec-1] -SQL警告: 执行耗时1204ms

四.总结

通过合理配置SQL日志输出,开发者可以:

  • 快速定位SQL执行问题
  • 直观分析实际执行的SQL语句
  • 有效识别性能瓶颈
  • 提升动态SQL调试效率

http://www.kler.cn/a/542345.html

相关文章:

  • 【Vue中BUG解决】npm error path git
  • 5-R循环
  • ios通过xib创建控件
  • A*寻路详解
  • YOLOv11-ultralytics-8.3.67部分代码阅读笔记-patches.py
  • Linux 实操篇 实用指令
  • 23种设计模式的定义和应用场景-02-结构型模式-C#代码
  • 青少年编程与数学 02-009 Django 5 Web 编程 05课题、数据库与ORM
  • PG高可用学习@2
  • 大模型基本原理(二)——ChatGPT的工作原理
  • BUU35 [DASCTF X GFCTF 2024|四月开启第一局]EasySignin 100 【gopher打mysql】
  • 飞牛 使用docker部署MoviePilot V2自动化影视管理平台教程
  • 剪辑如何剪辑制作视频短视频剪辑学习怎么学,难吗?
  • 安川伺服控制器MP系列优势特点及行业应用
  • Oracle入门精读03_Oracle11g安装目录及子目录的结构简介
  • 计算机毕业设计Tensorflow+LSTM空气质量监测及预测系统 天气预测系统 Spark Hadoop 深度学习 机器学习 人工智能
  • 认识一下redis的分布式锁
  • aspectFill(填充目标区域的同时保持图像的原有宽高比 (aspect ratio)图像不会被拉伸或压缩变形
  • 2.11 sqlite3数据库【数据库的相关操作指令、函数】
  • Java 使用腾讯翻译 API 实现含 HTML 标签文本,json值,精准翻译工具
  • 机器学习怎么学习,还有算法基本的源代码
  • 青少年编程与数学 02-009 Django 5 Web 编程 06课题、模型定义
  • 深度剖析责任链模式
  • 社区版IDEA中配置TomCat(详细版)
  • 【强化学习入门笔记】3.2 策略梯度法:REINFORCE
  • 什么是矩阵账号?如何做矩阵账号运营?