1. 背景

经常会遇到需要根据sql排查问题的情况,而mybatis输出的sql日志通常是这样的

Preparing: SELECT COUNT(*) AS total FROM system_notify_message WHERE deleted = 0 AND (read_status = ? AND user_id = ? AND user_type = ?) AND tenant_id = 1
==> Parameters: false(Boolean), 1(Long), 2(Integer)
<==      Total: 1

==>  Preparing: INSERT INTO system_users (username, password, nickname, remark, dept_id, post_ids, email, mobile, sex, status, create_time, update_time, creator, updater, tenant_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
==> Parameters: kk123457(String), $2a$04$fFi.Oou.rsQQLCZvfViiZ.FPm9C.B8QKhkO5puEUPfNyJVoMF8LKu(String), kk123457(String), 123(String), 108(Long), [](String), (String), (String), 2(Integer), 0(Integer), 2024-09-30T15:17:58.856(LocalDateTime), 2024-09-30T15:17:58.856(LocalDateTime), 1(String), 1(String)
<==    Updates: 1

需要自己手动拼接sql,是件很麻烦,且浪费时间的事情,于是就找到了

2. 解决方案一:p6spy集成日志

  • 添加pom依赖,注意该功能需要配合mybatis-plus实现
<dependency>
     <groupId>p6spy</groupId>
     <artifactId>p6spy</artifactId>
     <version>3.8.7</version>
</dependency>
 
<!-- https://mvnrepository.com/artifact/com.github.gavlyukovskiy/p6spy-spring-boot-starter -->
<dependency>
    <groupId>com.github.gavlyukovskiy</groupId>
    <artifactId>p6spy-spring-boot-starter</artifactId>
    <version>1.9.0</version>
</dependency>

<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
				
  • spy.properties配置如下:
#相关的包
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 日志格式
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.p6spy.engine.spy.appender.StdoutLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 开启慢sql
outagedetection=true
# 慢SQL记录标准(单位秒)
outagedetectioninterval=2

sql输出格式

 Consume Time:71 ms 2024-09-30 15:51:01
 Execute SQL:select * from t_heart_beat_record where project_path = 'D:\code\demo\smart-admin' and server_ip = '127.0.0.1;172.17.47.23;2.0.0.1;172.19.224.1;172.23.0.1' and process_no =6968

但这个有局限,如果是以上都用到,可以直接使用。

3. 解决方案二:sql自定义日志输出

  • 自定义拦截器,拦截sql

    打印sql,参数,耗时,默认使用了mybatis为orm,只需要一个拦截器就可以实现该功能,具体代码如下:

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.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Proxy;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 简易版sql日志输出工具
 * 注意:有枚举值需要增加枚举转换
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
@Component
public class SqlLogInterceptor implements Interceptor {
    public static final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private static final Logger log = LoggerFactory.getLogger(SqlLogInterceptor.class);
    /**
     * 获取配置中需要拦截的表,自定义配置,逗号隔开
     */
    @Value("#{'${tmall.sync.tables:}'.split(',')}")
    private List<String> tableNames;

    /**
     * 打印的参数字符串的最大长度
     */
    private final static int MAX_PARAM_LENGTH = 50;

    /**
     * 记录的最大SQL长度
     */
    private final static int MAX_SQL_LENGTH = 500;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        if (CollectionUtils.isEmpty(tableNames)) {
            return invocation.proceed();
        }
        StatementHandler statementHandler = realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        String sql = boundSql.getSql().replaceAll("\\s+", " ").toLowerCase();
        List<ParameterMapping> parameterMappings = new ArrayList<>(boundSql.getParameterMappings());
        Object parameterObject = boundSql.getParameterObject();
        if (parameterMappings.isEmpty() && parameterObject == null) {
            log.warn("parameterMappings is empty or parameterObject is null");
            return invocation.proceed();
        }
        Configuration configuration = mappedStatement.getConfiguration();
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        try {
            String parameter = "null";
            MetaObject newMetaObject = configuration.newMetaObject(parameterObject);
            for (ParameterMapping parameterMapping : parameterMappings) {
                if (parameterMapping.getMode() == ParameterMode.OUT) {
                    continue;
                }
                String propertyName = parameterMapping.getProperty();
                if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    parameter = getParameterValue(parameterObject);
                } else if (newMetaObject.hasGetter(propertyName)) {
                    parameter = getParameterValue(newMetaObject.getValue(propertyName));
                } else if (boundSql.hasAdditionalParameter(propertyName)) {
                    parameter = getParameterValue(boundSql.getAdditionalParameter(propertyName));
                }
                // 转义参数值中的 $ 字符
                String escapedValue = escapeDollarSign(parameter.toString());
                sql = sql.replaceFirst("\\?", escapedValue);
            }
            // 美化sql
            sql = beautifySql(sql);
            if (sql.length() > MAX_SQL_LENGTH) {
                sql= sql.substring(0, MAX_SQL_LENGTH);
            }
            long endTime = System.currentTimeMillis();
            long timeCount = endTime - startTime;
            log.info("耗时{} ms,SQL:{}", timeCount,sql);
            // 将拦截到的sql语句插入日志表中
        } catch (Exception e) {
            log.error(String.format("intercept sql error: [%s]", sql), e);
        }
        return invocation.proceed();
    }

    /**
     * 解决string替换$匹配问题
     * @param value
     * @return
     */
    private String escapeDollarSign(String value) {
        // 转义 $ 字符
        return value.replaceAll("\\$", "\\\\\\$");
    }

    /**
     * 获取参数
     *
     * @param param Object类型参数
     * @return 转换之后的参数
     */
    private static String getParameterValue(Object param) {
        if (param == null) {
            return "null";
        }
        if (param instanceof Number) {
            return param.toString();
        }
        String value = null;
        if (param instanceof String) {
            value = param.toString();
        } else if (param instanceof Date) {
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) param);
//        } else if (param instanceof IEnum) {
//            value = String.valueOf(((IEnum) param).getValue());
        } else {
            value = param.toString();
        }
        String srcStr = String.valueOf(value);
        return value instanceof CharSequence ? escapeString(srcStr) : srcStr;
    }


    @Override
    public Object plugin(Object o) {
        if (o instanceof StatementHandler) {
            return Plugin.wrap(o, this);
        }
        return o;
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 美化sql
     * @param sql sql语句
     */
    private String beautifySql(String sql) {
        sql = sql.replaceAll("[\\s\n ]+", " ");
        return sql;
    }

    public static String escapeString(String escapeStr) {
        if (escapeStr.matches("'(.+)'")) {
            escapeStr = escapeStr.substring(1, escapeStr.length() - 1);
        }

        return "'" + escapeRawString(escapeStr) + "'";
    }


    public static String escapeRawString(String escapeStr) {
        int stringLength = escapeStr.length();
        if (isEscapeNeededForString(escapeStr, stringLength)) {
            StringBuilder buf = new StringBuilder((int)((double)escapeStr.length() * 1.1));

            for(int i = 0; i < stringLength; ++i) {
                char c = escapeStr.charAt(i);
                switch (c) {
                    case '\u0000':
                        buf.append('\\');
                        buf.append('0');
                        break;
                    case '\n':
                        buf.append('\\');
                        buf.append('n');
                        break;
                    case '\r':
                        buf.append('\\');
                        buf.append('r');
                        break;
                    case '\u001a':
                        buf.append('\\');
                        buf.append('Z');
                        break;
                    case '"':
                        buf.append('\\');
                        buf.append('"');
                        break;
                    case '\'':
                        buf.append('\\');
                        buf.append('\'');
                        break;
                    case '\\':
                        buf.append('\\');
                        buf.append('\\');
                        break;
                    default:
                        buf.append(c);
                }
            }

            return buf.toString();
        } else {
            return escapeStr;
        }
    }

    private static boolean isEscapeNeededForString(String str, int len) {
        boolean needsHexEscape = false;

        for(int i = 0; i < len; ++i) {
            char c = str.charAt(i);
            switch (c) {
                case '\u0000':
                    needsHexEscape = true;
                    break;
                case '\n':
                    needsHexEscape = true;
                    break;
                case '\r':
                    needsHexEscape = true;
                    break;
                case '\u001a':
                    needsHexEscape = true;
                    break;
                case '"':
                    needsHexEscape = true;
                    break;
                case '\'':
                    needsHexEscape = true;
                    break;
                case '\\':
                    needsHexEscape = true;
            }

            if (needsHexEscape) {
                break;
            }
        }

        return needsHexEscape;
    }

    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            Plugin plugin = (Plugin)Proxy.getInvocationHandler(target);
            MetaObject metaObject = getMetaObject(plugin);
            return realTarget(metaObject.getValue("target"));
        } else {
            return (T) target;
        }
    }
    public static MetaObject getMetaObject(Object object) {
        return MetaObject.forObject(object, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
    }
}

输出日志:

2024-09-30 15:17:58.861 |  INFO 27772 | http-nio-48080-exec-5 [TID: N/A] c.i.y.server.handler.SqlLogInterceptor   | 耗时1 ms,SQL:insert into system_users (username, password, nickname, remark, dept_id, post_ids, email, mobile, sex, status, create_time, update_time, creator, updater, tenant_id) values ('kk123457', '$2a$04$fFi.Oou.rsQQLCZvfViiZ.FPm9C.B8QKhkO5puEUPfNyJVoMF8LKu', 'kk123457', '123', 108, '[]', '', '', 2, 0, '2024-09-30T15:17:58.856', '2024-09-30T15:17:58.856', '1', '1', 1)

本文由博客一文多发平台 OpenWrite 发布!