MyBatis-Plus SQL日志

1,163 阅读3分钟

MyBatis-Plus 日志拦截器

EnhancedSqlLogInterceptor -> 拦截日志
SqlLogHandler -> 拓展功能:实现接口自定义处理日志

import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import lombok.extern.slf4j.Slf4j;
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.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Array;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 增强版 SQL 日志打印拦截器(显示完整参数)
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, org.apache.ibatis.session.ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class EnhancedSqlLogInterceptor implements Interceptor {

    // 慢SQL阈值(毫秒)
    private static final long SLOW_SQL_THRESHOLD = 1000;

    // 需要隐藏的参数值(如密码等敏感信息)
    private static final Set<String> HIDE_PARAMS = new HashSet<>(Arrays.asList(
            "password", "pwd", "secret", "token", "creditCard", "cvv"
    ));

    // 日期格式化
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    // SQL 中的 ? 占位符正则
    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("(?<!\\)\?(?!\?)");

    private static final int MAX_LINE_LENGTH = 120;

    @Autowired(required = false)
    private List<SqlLogHandler> sqlLogHandlers;

    // 使用正则表达式匹配 ? 占位符(确保是独立的问号)

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

        // 记录开始时间
        long startTime = SystemClock.now();

        try {
            // 执行SQL
            return invocation.proceed();
        } finally {
            // 计算执行时间
            long endTime = SystemClock.now();
            long costTime = endTime - startTime;


            //如果开启了debug=true,则打印SQL
            if (log.isDebugEnabled() || costTime > SLOW_SQL_THRESHOLD || (sqlLogHandlers != null && !sqlLogHandlers.isEmpty())) {
                // 获取执行方法名
                String methodName = invocation.getMethod().getName();

                // 获取StatementHandler
                StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

                // 获取BoundSql
                BoundSql boundSql = statementHandler.getBoundSql();

                // 获取原始SQL(带 ? 占位符)
                String sqlWithPlaceholders = boundSql.getSql().trim();

                // 获取完整SQL(带实际参数值)
                String completeSql = getCompleteSql(statementHandler, boundSql, sqlWithPlaceholders);
                // 构建日志信息
                String logMsg = buildLogMessage(methodName, completeSql, costTime);

                if(costTime > SLOW_SQL_THRESHOLD){
                    log.warn(logMsg);
                }else {
                    if(log.isDebugEnabled()){
                        log.debug(logMsg);
                    }
                }
                if(sqlLogHandlers != null && !sqlLogHandlers.isEmpty()){
                    for (SqlLogHandler sqlLogHandler : sqlLogHandlers) {
                        sqlLogHandler.handle(methodName, completeSql, costTime);
                    }
                }
            }
        }
    }

    /**
     * 获取完整SQL(替换 ? 为实际参数值)
     */
    private String getCompleteSql(StatementHandler statementHandler, BoundSql boundSql, String sqlWithPlaceholders) {
        try {
            // 获取参数映射
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            if (parameterMappings == null || parameterMappings.isEmpty()) {
                return sqlWithPlaceholders;
            }

            // 获取配置信息
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
            MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
            Configuration configuration = mappedStatement.getConfiguration();
//            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

            // 替换占位符
//            String completeSql = sqlWithPlaceholders;
            Object parameterObject = boundSql.getParameterObject();
            List<Object> paramValues = getParameterValues(boundSql, configuration, parameterObject);

            Matcher matcher = PLACEHOLDER_PATTERN.matcher(sqlWithPlaceholders);
            StringBuffer sb = new StringBuffer();
            int paramIndex = 0;
            while (matcher.find() && paramIndex < paramValues.size()) {
                String replacement = formatParameter(paramValues.get(paramIndex++));
                matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
            }
            matcher.appendTail(sb);
            return sb.toString();

//            for (Object value : paramValues) {
//                int index = completeSql.indexOf('?');
//                if (index == -1) break;
//
//                String replacement = formatParameter(value);
//                completeSql = completeSql.substring(0, index) + replacement + completeSql.substring(index + 1);
//            }
//
//            return completeSql;
        } catch (Exception e) {
            log.error("获取完整SQL失败,将返回原始SQL", e);
            return sqlWithPlaceholders;
        }
    }

    /**
     * 获取参数值列表
     */
    private List<Object> getParameterValues(BoundSql boundSql, Configuration configuration, Object parameterObject) {
        List<Object> paramValues = new ArrayList<>();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        if (parameterMappings != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            MetaObject metaObject = parameterObject == null ? null :
                    configuration.newMetaObject(parameterObject);

            for (ParameterMapping parameterMapping : parameterMappings) {
                String propertyName = parameterMapping.getProperty();

                // 处理敏感参数
                if (isSensitiveParam(propertyName)) {
                    paramValues.add("******");
                    continue;
                }

                if (metaObject != null && typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    // 基本类型参数
                    paramValues.add(parameterObject);
                } else if (boundSql.hasAdditionalParameter(propertyName)) {
                    // 动态参数
                    Object value = boundSql.getAdditionalParameter(propertyName);
                    paramValues.add(value);
                } else if (parameterObject == null) {
                    paramValues.add(null);
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    paramValues.add(parameterObject);
                } else if (metaObject != null) {
                    // 对象属性
                    Object value = metaObject.getValue(propertyName);
                    paramValues.add(value);
                } else {
                    paramValues.add(null);
                }
            }
        }
        return paramValues;
    }

    /**
     * 格式化参数值
     */
    private String formatParameter(Object param) {
        if (param == null) {
            return "NULL";
        }

        if (param instanceof String) {
            return "'" + param + "'";
        }

        if (param instanceof Date) {
            return "'" + DATE_FORMAT.format((Date) param) + "'";
        }

        if (param instanceof Number) {
            return param.toString();
        }

        if (param instanceof Boolean) {
            return Boolean.TRUE.equals(param) ? "1" : "0";
        }

        if (param instanceof byte[]) {
            return "[BINARY]";
        }

        if (param instanceof Collection) {
            return formatCollection((Collection<?>) param);
        }

        if (param.getClass().isArray()) {
            return formatArray(param);
        }

        return "'" + param.toString() + "'";
    }

    /**
     * 格式化集合参数
     */
    private String formatCollection(Collection<?> collection) {
        StringBuilder sb = new StringBuilder();
        for (Object item : collection) {
            if (sb.length() > 0) sb.append(",");
            sb.append(formatParameter(item));
        }
        return sb.toString();
    }

    /**
     * 格式化数组参数
     */
    private String formatArray(Object array) {
        StringBuilder sb = new StringBuilder();
        int length = Array.getLength(array);
        for (int i = 0; i < length; i++) {
            if (sb.length() > 0) sb.append(",");
            sb.append(formatParameter(Array.get(array, i)));
        }
        return sb.toString();
    }

    /**
     * 判断是否是敏感参数
     */
    private boolean isSensitiveParam(String paramName) {
        if (paramName == null) return false;

        String lowerParamName = paramName.toLowerCase();
        return HIDE_PARAMS.stream()
                .anyMatch(hideParam -> lowerParamName.contains(hideParam.toLowerCase()));
    }

    /**
     * 构建日志消息
     */
    private String buildLogMessage(String method, String sql, long costTime) {
        return String.format("\n" +
                        "┌───────────────────────────────────────────────────────────────────────────────\n" +
                        "│ SQL 方法: %s\n" +
                        "│ 执行时间: %d ms\n" +
                        "│ 完整 SQL: \n" +
                        "%s" +
                        "└───────────────────────────────────────────────────────────────────────────────",
                method, costTime, formatSqlForDisplay(sql)).replace("│        \n","");
    }

    /**
     * 优化版SQL格式化显示(解决换行问题)
     */
    private String formatSqlForDisplay(String sql) {
        // 1. 对长SQL进行智能换行
        List<String> formattedLines = new ArrayList<>();
        StringBuilder currentLine = new StringBuilder();

        // 按空格分割SQL单词
        String[] tokens = sql.split("\s+");

        for (String token : tokens) {
            // 检查添加该单词后是否超过最大行长度
            if (currentLine.length() + token.length() + 1 > MAX_LINE_LENGTH) {
                // 保存当前行并开始新行
                formattedLines.add(currentLine.toString());
                currentLine = new StringBuilder();
            }

            // 添加单词到当前行
            if (currentLine.length() > 0) {
                currentLine.append(" ");
            }
            currentLine.append(token);

            // 特殊处理:遇到特定关键字后强制换行
            if (isKeyword(token)) {
                formattedLines.add(currentLine.toString());
                currentLine = new StringBuilder();
            }
        }

        // 添加最后一行
        if (currentLine.length() > 0) {
            formattedLines.add(currentLine.toString());
        }

        // 2. 为每行添加前缀
        return formattedLines.stream()
                .map(line -> "│        " + line)
                .collect(Collectors.joining("\n")) + "\n";
    }

    /**
     * 判断是否是SQL关键字(需要换行的)
     */
    private boolean isKeyword(String token) {
        if (token == null || token.isEmpty()) {
            return false;
        }

        String upperToken = token.toUpperCase();
        return upperToken.equals("SELECT") ||
                upperToken.equals("FROM") ||
                upperToken.equals("WHERE") ||
                upperToken.equals("JOIN") ||
                upperToken.equals("INNER") ||
                upperToken.equals("LEFT") ||
                upperToken.equals("RIGHT") ||
                upperToken.equals("GROUP") ||
                upperToken.equals("ORDER") ||
                upperToken.equals("HAVING") ||
                upperToken.equals("LIMIT") ||
                upperToken.equals("OFFSET") ||
                upperToken.equals("UNION") ||
                upperToken.equals("VALUES");
    }

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

    @Override
    public void setProperties(Properties properties) {
        // 可配置属性
    }
}

SqlLogHandler 接口

public interface SqlLogHandler {

    void handle(String methodName, String completeSql, long costTime);
}

SqlLogHandler 实现类demo

import org.slf4j.Logger;
import org.springframework.stereotype.Component;


@Component
public class DBLogHandler implements SqlLogHandler {

    private static final Logger logger = org.slf4j.LoggerFactory.getLogger(DBLogHandler.class);

    @Override
    public void handle(String methodName, String completeSql, long costTime) {
        logger.info("当前操作用户 : {}", SecurityUtils.getCurrentUserId());
        logger.info("DBLogHandler{}: {} costTime: {}", methodName, completeSql, costTime);
        AsyncExecutor.execute(() -> logger.info("异步执行器DBLogHandler{}: {} costTime: {}", methodName, completeSql, costTime));
    }
}