Mybatis控制台打印SQL执行信息(执行方法、执行SQL、执行时间)

文章目录


前言

SQL性能 监控是一个程序必要的功能,通常我们可以使用数据库自带的客户端工具进行SQL 性能分析。然而对于一些专业度不高的人员来说,当程序出现卡顿或者响应速度变慢时,排查问题变得困难。当程序出现卡顿,通常通过检查服务器磁盘使用情况、程序内存大小,网络带宽以及数据库I/O 等方面进行问题排查。然而数据库I/O 打高的情况通常是由于SQL 执行效率过低导致的。一般项目制的公司都有属于自己的实施人员,然而要让实施人员去排查具体SQL 执行过慢问题,这显然对于专业度不高的工作人员来说是一种挑战和煎熬。因此本系列文章将介绍如何使用Mybatis 的拦截器功能完成对SQL 执行的时间记录,并通过MQ 推送至SQL记录服务,记录具体的慢SQL信息,后续可以通过页面进行展示。通过可视化的方式让实施人员快速定位到问题所在。

一、基本功能介绍

本章节只实现Mybatis 执行时对执行SQL 进行拦截,控制台打印执行SQL 包括参数、执行方法以及执行时间。大致结构图如下:

对慢SQL 进行发送MQ,记录显示到前端界面的功能,将在本系列文章第二章实现。

1.1本章功能效果预览图:

Mapper Method: 显示该SQL 是由哪个Mapper 方法进行调用执行。

Execute SQL:打印出完整执行的SQL ,自动填充了参数。

Spend Time:记录本次SQL执行花费的时间。

二、可执行源码

2.1 yaml基础配置

需要在yaml 配置文件中配置是否打印SQL 执行信息。当然该配置可以放入Redis中,以方便后续面向微服务时,可以一键开启和关闭,这里就不再演示,后续扩展可有您自主实现。

powershell 复制代码
mybatis-analyze:
  show-log: true #SQL打印到控制台

2.2 MybatisAnalyzeSQLInterceptor实现SQL拦截

源码可直接复制运行!!!!!

java 复制代码
package com.hl.by.common.mybatis.interceptor;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.*;
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.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @Author: DI.YIN
 * @Date: 2024/11/25 16:32
 * @Version: 1.0.0
 * @Description: Mybatis SQL分析插件
 **/
@Slf4j
@Intercepts(value = {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @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}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@Component
public class MybatisAnalyzeSQLInterceptor implements Interceptor {

    @Value("${mybatis-analyze.show-log:false}")
    private Boolean showLog;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StopWatch startedWatch = StopWatch.createStarted();
        Object returnValue = null;
        Exception proceedSQLException = null;
        try {
            returnValue = invocation.proceed();
        } catch (Exception e) {
            proceedSQLException = e;
        }
        startedWatch.stop();
        long spendTime = startedWatch.getTime(TimeUnit.MILLISECONDS);
        if (invocation.getArgs() == null || !(invocation.getArgs()[0] instanceof MappedStatement)) {
            return returnValue;
        }
        // just handle mappedStatement
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // get BoundSql
        BoundSql boundSql = null;
        for (int i = invocation.getArgs().length - 1; i >= 0; i--) {
            if (invocation.getArgs()[i] instanceof BoundSql) {
                boundSql = (BoundSql) invocation.getArgs()[i];
                break;
            }
        }
        if (invocation.getTarget() instanceof RoutingStatementHandler) {
            RoutingStatementHandler routingStatementHandler = (RoutingStatementHandler) invocation.getTarget();
            boundSql = routingStatementHandler.getBoundSql();
        }
        if (boundSql == null) {
            Object parameter = null;
            if (invocation.getArgs().length > 1) {
                parameter = invocation.getArgs()[1];
            }
            boundSql = mappedStatement.getBoundSql(parameter);
        }
        //
        printProcessedSQL(boundSql, mappedStatement.getConfiguration(), mappedStatement.getId(), spendTime);
        // If an exception occurs during SQL execution,throw exception
        if (proceedSQLException != null) {
            throw proceedSQLException;
        }
        return returnValue;
    }

    /**
     * Parse SQL and Print SQL
     *
     * @param boundSql
     * @param configuration
     * @param statement
     * @param spendTime
     */
    private void printProcessedSQL(BoundSql boundSql, Configuration configuration, String statement, long spendTime) {
        Map<Integer, Object> parameterValueMap = parseParameterValues(configuration, boundSql);
        String finalSQL = fillSqlParams(boundSql.getSql(), parameterValueMap);
        finalSQL = finalSQL.replaceAll("\n", "");
        String printData = "\n===============Start Print SQL===============\n" +
                "Mapper Method: [ " + statement + " ]\n" +
                "Execute SQL: " + finalSQL + " \n" +
                "Spend Time: " + spendTime + " ms \n" +
                "===============End Print SQL===============\n";
        if (showLog) {
            log.info(printData);
        }
    }

    public static String fillSqlParams(String statementQuery, Map<Integer, Object> parameterValues) {
        final StringBuilder sb = new StringBuilder();
        int currentParameter = 0;
        for (int pos = 0; pos < statementQuery.length(); pos++) {
            char character = statementQuery.charAt(pos);
            if (statementQuery.charAt(pos) == '?' && currentParameter <= parameterValues.size()) {
                Object value = parameterValues.get(currentParameter);
                sb.append(value != null ? value.toString() : new MybatisAnalyzeSQLInterceptor.Values().toString());
                currentParameter++;
            } else {
                sb.append(character);
            }
        }
        return sb.toString();
    }

    /**
     * 用于解析参数值
     *
     * @param configuration
     * @param boundSql
     * @return Map<Integer, Object>
     */
    private static Map<Integer, Object> parseParameterValues(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            Map<Integer, Object> parameterValues = new HashMap<>();
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    parameterValues.put(i, new MybatisAnalyzeSQLInterceptor.Values(value));
                }
            }
            return parameterValues;
        }
        return Collections.emptyMap();
    }

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

    @Override
    public void setProperties(Properties properties0) {
    }

    @Setter
    @Getter
    public static class Values {
        public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

        public static final String databaseDialectDateFormat = NORM_DATETIME_PATTERN;
        public static final String databaseDialectTimestampFormat = NORM_DATETIME_PATTERN;
        private Object value;

        public Values(Object valueToSet) {
            this();
            this.value = valueToSet;
        }

        public Values() {
        }

        @Override
        public String toString() {
            return convertToString(this.value);
        }

        public String convertToString(Object value) {
            String result;

            if (value == null) {
                result = "NULL";
            } else {
                if (value instanceof byte[]) {
                    result = new String((byte[]) value);
                } else if (value instanceof Timestamp) {
                    result = new SimpleDateFormat(databaseDialectTimestampFormat).format(value);
                } else if (value instanceof Date) {
                    result = new SimpleDateFormat(databaseDialectDateFormat).format(value);
                } else if (value instanceof Boolean) {
                    result = Boolean.FALSE.equals(value) ? "0" : "1";
                } else {
                    result = value.toString();
                }
                result = quoteIfNeeded(result, value);
            }

            return result;
        }

        private String quoteIfNeeded(String stringValue, Object obj) {
            if (stringValue == null) {
                return null;
            }
            if (Number.class.isAssignableFrom(obj.getClass()) || Boolean.class.isAssignableFrom(obj.getClass())) {
                return stringValue;
            } else {
                return "'" + escape(stringValue) + "'";
            }
        }

        private String escape(String stringValue) {
            return stringValue.replaceAll("'", "''");
        }
    }
}

相关推荐
NiceCloud喜云6 分钟前
腾讯云国际版云数据库选型:MySQL、Redis 怎么按业务架构来判断
数据库·mysql·腾讯云
ew4521815 分钟前
MySQL JOIN 关联语法、差异、标准用法、经典坑点汇总
数据库·mysql
Rain的Java大神之路1 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
Nturmoils1 小时前
给运营导个 CSV,在 ksql 里用 \copy 就够了
数据库
ciqingloveless1 小时前
Oracle打开数据库加密
数据库·oracle
counting money1 小时前
Tomcat 专题:从基础架构到性能调优与 WebSocket 实战
java·websocket·tomcat
m0_462605222 小时前
大模型week02
数据库
DBA_G2 小时前
GBase 8a数据库集群多维度资源管控策略矩阵解析
数据库·oracle
xiaohaiAIgeo2 小时前
【2026年】HG/T 20656-2024化工暖通空调设计规范:新版标准的变化与影响
java·前端·javascript·科普知识
小强库计算机毕业设计2 小时前
基于 Spring Boot + Vue3 的校园管理系统
java·spring boot·后端·项目实战·管理系统·技术分享·校园管理系统实战