捕获Mybatis执行的Sql

使用

java 复制代码
// 开启捕获
SqlCaptureInterceptor.startCapture();
boolean success = SqlHelper.exeBatch(sqlSessionFactory, updateList, OrderItemMapper.class, OrderItemMapper::updateById);
// 获取捕获的sql并且清空
List<String> sqlList = SqlCaptureInterceptor.getCapturedSqlAndClear();
System.out.println(sqlList);

原理

利用 Mybatis 的拦截器来捕获 Sql,然后格式化为完整的 Sql。

源码

SqlParser.class

java 复制代码
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * Sql 解析器
 *
 * @author Jalon
 * @since 2025/9/23 12:59
 **/
public class SqlParser {

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

    /**
     * 从BoundSql中提取参数值列表
     */
    public static List<Object> getParameterValues(Configuration configuration, BoundSql boundSql) {
        List<Object> parameterValues = new ArrayList<>();
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        if (parameterMappings != null && !parameterMappings.isEmpty()) {
            MetaObject metaObject = parameterObject != null ?
                    configuration.newMetaObject(parameterObject) : null;

            for (ParameterMapping mapping : parameterMappings) {
                String property = mapping.getProperty();
                if (boundSql.hasAdditionalParameter(property)) {
                    parameterValues.add(boundSql.getAdditionalParameter(property));
                } else if (parameterObject != null && metaObject.hasGetter(property)) {
                    parameterValues.add(metaObject.getValue(property));
                } else {
                    parameterValues.add(null);
                }
            }
        }

        return parameterValues;
    }

    /**
     * 使用JSQLParser替换SQL中的参数占位符
     */
    public static String replaceParametersWithJsqlparser(String sql, List<Object> parameterValues) {
        try {
            // 解析SQL生成抽象语法树
            Statement statement = CCJSqlParserUtil.parse(sql);

            // 创建参数迭代器
            Iterator<Object> parameterIterator = parameterValues.iterator();

            // 创建字符串缓冲区
            StringBuilder sb = new StringBuilder();

            // 创建表达式解析器,用于替换?占位符
            ExpressionDeParser expressionDeParser = new ExpressionDeParser() {
                @Override
                public void visit(net.sf.jsqlparser.expression.JdbcParameter jdbcParameter) {
                    if (parameterIterator.hasNext()) {
                        Object value = parameterIterator.next();
                        buffer.append(formatParameterValue(value));
                    } else {
                        buffer.append("?"); // 参数不足时保留占位符
                    }
                }
            };

            // 创建SelectDeParser
            SelectDeParser selectDeParser = new SelectDeParser(expressionDeParser, sb);
            expressionDeParser.setSelectVisitor(selectDeParser);
            expressionDeParser.setBuffer(sb);

            // 根据语句类型使用不同的解析方式
            if (statement instanceof Select) {
                // 处理SELECT语句
                ((Select) statement).getSelectBody().accept(selectDeParser);
            } else {
                // 处理非SELECT语句,使用三参数构造函数
                StatementDeParser statementDeParser = new StatementDeParser(
                        expressionDeParser,
                        selectDeParser,
                        sb
                );
                statement.accept(statementDeParser);
            }

            return sb.toString();
        } catch (JSQLParserException e) {
            // 解析失败时返回原始SQL并记录日志
            System.err.println("SQL解析失败: " + e.getMessage() + ", 原始SQL: " + sql);
            return sql;
        }
    }

    /**
     * 格式化参数值为SQL中可用的字符串
     */
    public static String formatParameterValue(Object value) {
        if (value == null) {
            return "NULL";
        }

        // 处理字符串类型
        if (value instanceof String || value instanceof Character) {
            String strValue = value.toString().replace("'", "''"); // 转义单引号
            return "'" + strValue + "'";
        }

        // 处理日期类型
        if (value instanceof java.sql.Date) {
            return "'" + DATE_FORMAT.format(value) + "'";
        }
        if (value instanceof java.util.Date) {
            return "'" + DATETIME_FORMAT.format(value) + "'";
        }

        // 处理布尔类型
        if (value instanceof Boolean) {
            return (Boolean) value ? "1" : "0";
        }

        // 处理数组和集合类型
        if (value.getClass().isArray()) {
            return formatArray((Object[]) value);
        }
        if (value instanceof Collection) {
            return formatCollection((Collection<?>) value);
        }

        // 其他类型直接返回字符串表示
        return value.toString();
    }

    /**
     * 格式化数组类型参数
     */
    public static String formatArray(Object[] array) {
        StringBuilder sb = new StringBuilder();
        sb.append("(");
        for (int i = 0; i < array.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(formatParameterValue(array[i]));
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * 格式化集合类型参数
     */
    public static String formatCollection(Collection<?> collection) {
        StringBuilder sb = new StringBuilder();
        sb.append("(");
        boolean first = true;
        for (Object item : collection) {
            if (!first) {
                sb.append(", ");
            }
            sb.append(formatParameterValue(item));
            first = false;
        }
        sb.append(")");
        return sb.toString();
    }
}

SqlCaptureInterceptor.class

java 复制代码
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * Sql捕获拦截器
 *
 * @author Jalon
 * @since 2025/9/23 11:28
 **/
@Intercepts({
        @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})
})
public class SqlCaptureInterceptor implements Interceptor {
    // 最大记录
    private static final int DEFAULT_MAX_LEN = 50;
    // 线程局部变量存储捕获的SQL
    private static final ThreadLocal<List<String>> sqlListHolder = ThreadLocal.withInitial(ArrayList::new);
    // 控制是否开启捕获的开关
    private static final ThreadLocal<Boolean> captureEnabled = ThreadLocal.withInitial(() -> false);

    public static int maxLength = DEFAULT_MAX_LEN;

    /**
     * 开启SQL捕获
     */
    public static void startCapture() {
        captureEnabled.set(true);
        // 清空历史数据
        sqlListHolder.get().clear();
    }

    /**
     * 停止SQL捕获
     */
    public static void stopCapture() {
        captureEnabled.set(false);
    }

    /**
     * 获取捕获的SQL列表
     */
    public static List<String> getCapturedSql() {
        return new ArrayList<>(sqlListHolder.get()); // 返回副本,避免外部修改
    }

    /**
     * 获取当前线程执行的SQL列表并清空捕获的SQL数据
     */
    public static List<String> getCapturedSqlAndClear() {
        ArrayList<String> sqls = new ArrayList<>(sqlListHolder.get());
        SqlCaptureInnerInterceptor.clear();
        return sqls;
    }

    /**
     * 清除捕获的SQL数据
     */
    public static void clear() {
        sqlListHolder.get().clear();
        captureEnabled.remove();
        sqlListHolder.remove();
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 仅在开启捕获时记录SQL
        try {
            if (Boolean.TRUE.equals(captureEnabled.get())) {
                if (sqlListHolder.get().size() >= maxLength) {
                    // 超长移除第一个
                    sqlListHolder.get().remove(0);
                }

                MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
                Object parameter = invocation.getArgs().length > 1 ? invocation.getArgs()[1] : null;

                // 获取SQL信息
                BoundSql boundSql = mappedStatement.getBoundSql(parameter);
                Configuration configuration = mappedStatement.getConfiguration();

                // 获取完整的sql
                List<Object> parameterValues = SqlParser.getParameterValues(configuration, boundSql);
                String fullSql = SqlParser.replaceParametersWithJsqlparser(boundSql.getSql(), parameterValues);

                sqlListHolder.get().add(fullSql);
            }
        } catch (Throwable throwable) {
            System.err.println("SQL解析失败: " + throwable.getMessage());
        }

        // 继续执行原方法
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 只对Executor类型进行拦截
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以通过配置文件传递参数
    }
}

配置

对于SpringMVC 项目来说,应该在 mybatis 的 xml 文件中添加 plugin,来注册拦截器。

SpringBoot 项目的话需要再 MyBatisConfig 类中配置,参考:

mybatis.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD SQL MAP Config 3.1//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<plugins>
		<plugin interceptor="com.xx.interceptor.SqlCaptureInterceptor"></plugin>
	</plugins>
</configuration>

MyBatisConfig.class

java 复制代码
import com.fly.ssm.interceptor.SqlCaptureInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class MyBatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);

        // 添加拦截器
        Interceptor[] plugins = new Interceptor[1];
        plugins[0] = new SqlCaptureInterceptor();
        sessionFactory.setPlugins(plugins);

        return sessionFactory.getObject();
    }
}
相关推荐
TDengine (老段)5 小时前
TDengine 聚合函数 SPREAD 用户手册
大数据·数据库·sql·物联网·时序数据库·tdengine·涛思数据
CloudDM4 天前
SQL 审核工具深度体验(一): CloudDM vs Archery vs Yearning vs Bytebase
sql
小小小LIN子5 天前
mybatis升级到mybatis plus后报Parameter not found
mybatis
鸿乃江边鸟8 天前
向量化和列式存储
大数据·sql·向量化
懒虫虫~9 天前
通过内存去重替换SQL中distinct,优化SQL查询效率
java·sql·慢sql治理
逛逛GitHub9 天前
1 个神级智能问数工具,刚开源就 1500 Star 了。
sql·github
Huhbbjs9 天前
SQL 核心概念与实践总结
开发语言·数据库·sql
咋吃都不胖lyh9 天前
SQL-字符串函数、数值函数、日期函数
sql