使用
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();
}
}