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("'", "''");
        }
    }
}

相关推荐
cdut_suye13 分钟前
C++11新特性探索:Lambda表达式与函数包装器的实用指南
开发语言·数据库·c++·人工智能·python·机器学习·华为
计算机学姐23 分钟前
基于SSM的宠物领养平台
java·vue.js·spring·maven·intellij-idea·mybatis·宠物
泰山小张只吃荷园33 分钟前
期末Python复习-输入输出
java·前端·spring boot·python·spring cloud·docker·容器
Mr_Xuhhh36 分钟前
程序地址空间
android·java·开发语言·数据库
YSRM42 分钟前
异或-java-leetcode
java·算法·leetcode
大明湖的狗凯.1 小时前
MySQL 中的乐观锁与悲观锁
java·数据库·mysql
真上帝的左手1 小时前
数据库-MySQL-MybatisPlus整合多数据源
数据库·mysql·mybatis
奈斯ing1 小时前
【MySQL篇】持久化和非持久化统计信息的深度剖析(第一篇,总共六篇)
数据库·mysql
真上帝的左手1 小时前
数据库-MySQL-Mybatis源码解析-设计模式角度
数据库·mysql·mybatis
z202305081 小时前
linux之调度管理(13)- wake affine 唤醒特性
java·开发语言