捕获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();
    }
}
相关推荐
期待のcode6 小时前
MyBatisX插件
java·数据库·后端·mybatis·springboot
Li.CQ10 小时前
SQL学习笔记(二)
笔记·sql·学习
小二·12 小时前
MyBatis基础入门《十五》分布式事务实战:Seata + MyBatis 实现跨服务数据一致性
分布式·wpf·mybatis
小二·12 小时前
MyBatis基础入门《十四》多租户架构实战:基于 MyBatis 实现 SaaS 系统的动态数据隔离
数据库·架构·mybatis
白衣衬衫 两袖清风13 小时前
SQL联查案例
数据库·sql
晨曦54321016 小时前
MySQL MOD()函数详解与Python对比
sql
甘露s16 小时前
MySQL深入之索引、存储引擎和SQL优化
数据库·sql·mysql
偶遇急雨洗心尘17 小时前
记录一次服务器迁移时,数据库版本不一致导致sql函数报错和系统redirect重定向丢失域名问题
运维·服务器·数据库·sql
Logic10117 小时前
《Mysql数据库应用》 第2版 郭文明 实验5 存储过程与函数的构建与使用核心操作与思路解析
数据库·sql·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学
小二·17 小时前
MyBatis基础入门《十六》企业级插件实战:基于 MyBatis Interceptor 实现 SQL 审计、慢查询监控与数据脱敏
数据库·sql·mybatis