Skip to content

查看SQL的执行时间 ​

使用注意

下面的两种方案中,方案1和方案2不能共存,否则会优先执行MyBatisplus自定义的SQL耗时查询拦截器。

方案1.MyBatis 自定义拦截器 ​

原生 mybatis 日志只会打印 SQL,不带耗时,自己写一个 Interceptor 拦截 StatementHandler,拿到开始结束时间,打印毫秒耗时:

java
package com.cghbh.handler;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.util.Properties;

/**
 * MyBatis SQL耗时拦截器,开发调试使用
 */
@Component
@Intercepts({
        @Signature(type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}),
        @Signature(type = StatementHandler.class,
                method = "query",
                args = {java.sql.Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class,
                method = "update",
                args = {java.sql.Statement.class})
})
public class SqlCostInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(SqlCostInterceptor.class);

    // 慢SQL阈值,毫秒,超过会黄色警告
    private static final long SLOW_SQL_THRESHOLD = 500;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        Object proceed;
        try {
            proceed = invocation.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            // 获取 StatementHandler
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
            MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql().replaceAll("\\s+", " ");
            String mapperId = mappedStatement.getId();

            if (cost > SLOW_SQL_THRESHOLD) {
                log.warn("\n【慢SQL】mapperId:{} \n耗时: {} ms \nSQL: {}", mapperId, cost, sql);
            } else {
                log.info("\n【SQL】mapperId:{} \n耗时: {} ms \nSQL: {}", mapperId, cost, sql);
            }
        }
        return proceed;
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}

当有请求过来之后,SQL执行和耗时效果如下:

使用总结

✅优点

  1. 纯代码,不改数据源,切换环境方便
  2. 可控,可以自己定制日志格式、过滤不需要打印的 SQL
  3. 不想引入第三方包的时候用

❌ 缺点

  1. 打印的 SQL 还是带 ?,参数是分开的,不能直接复制执行(硬伤!)
  2. 自己写要考虑边界(批量、存储过程等),容易有小 bug

👉 适合:只单纯看耗时,不经常复制 SQL 去数据库执行;不想引入额外依赖。

方案 2.P6Spy ​

P6Spy 是 JDBC 代理,能打印填充完参数的完整 SQL + 执行耗时,适合 MyBatis 开发调试脚本之家。

引入依赖 ​

在qk-management包中引入这个依赖。

xml
<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>

修改application.yml配置 ​

修改 yml 数据源:driver 换成 com.p6spy.engine.spy.P6SpyDriver,url 开头加 p6spy。

java
#数据库连接信息
spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver 
    url: jdbc:p6spy:mysql://localhost:3306/qk
    username: root
    password: Xianyu@666
    # driver-class-name: com.mysql.cj.jdbc.Driver
    # url: jdbc:mysql://localhost:3306/qk


#mybatis-plus配置
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true  # 开启驼峰命名
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印sql
  global-config:
    db-config:
      id-type: auto # 全局主键策略 数据库自增
  mapper-locations: classpath*:/mappers/*.xml # 映射文件位置

增加配置文件 ​

创建spy.properties文件,放到 src/main/resources 下面:

yaml
# 模块:日志模块 + 慢SQL检测模块
modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory

# 使用自定义单行模板(重点:executionTime 就是耗时ms)
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=【P6Spy】时间:%(currentTime) | 耗时:%(executionTime) ms | 类型:%(category) | 连接ID:%(connectionId) | SQL:%(sql)

# 输出到控制台(开发首选)
appender=com.p6spy.engine.spy.appender.StdoutLogger
# 如果想交给slf4j/logback统一管理,换成下面这行
# appender=com.p6spy.engine.spy.appender.Slf4JLogger

# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss.SSS

# 真实驱动类(mysql8用 com.mysql.cj.jdbc.Driver)
driverlist=com.mysql.cj.jdbc.Driver

# 注销原有驱动
deregisterdrivers=true
useprefix=true

# 排除不需要打印的类别,减少垃圾日志
# 可选值:error,info,batch,debug,statement,commit,rollback,result,resultset
excludecategories=info,debug,result,resultset

# 慢SQL检测开关
outagedetection=true
# 超过 500ms 标记为慢SQL,单位毫秒,不同版本可能不一样,有的是秒,有的是毫秒
outagedetectioninterval=500

使用的效果如下,输出补全参数后的完整 SQL(不是 ?,直接复制就能丢 Navicat 执行!这个是最大优势):

使用总结

✅ 优点

  1. 输出补全参数后的完整 SQL(不是 ?,直接复制就能丢 Navicat 执行!这个是最大优势)
  2. 自带 SQL 执行耗时,格式清晰
  3. 不用写代码,只改依赖 + 数据源配置,侵入很小
  4. 还能打印执行栈、连接信息

❌ 缺点

  1. 绝对不能上生产:有性能损耗
  2. 改数据源 url、driver,切换的时候要小心忘记切回来,容易线上炸
  3. 依赖多一层 JDBC 代理

👉 适合:写 xml、调分页、联表查询,想直接拿到完整 SQL + 耗时,复制去数据库测试。