Mybatis插件-查看执行SQL
前言
SQL 的执行是通过 Statement 执行的,有的驱动 Statement 实现类有打印执行 SQL 的方法,而有的驱动没有,有打印 SQL 的方法直接执行就可以了,没有就只能手动拼接了。
有打印 SQL 方法
Mysql 的 Statement
有打印 SQL 的方法只需要获取 Statement 再执行对应的方法即可。MyBatis 的插件可以代理 ParameterHandler
、 ResultSetHandler
、 StatementHandler
和 Executor
4 个接口里面的方法,其中 StatementHandler
用于处理 Statement ,可以看到下面两个方法包含 Statement :
int update(Statement statement)
throws SQLException
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
插件代码如下:
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.springframework.stereotype.Component;
import java.sql.PreparedStatement;
import java.sql.Statement;
/**
* @author haibara
*/
@Component
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "update", args = Statement.class),
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
public class CustomInterceptor implements Interceptor {
private final Class<?> clientPreparedStatement;
public CustomInterceptor() throws ClassNotFoundException {
this.clientPreparedStatement = Class.forName("com.mysql.cj.jdbc.ClientPreparedStatement");
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
Object unwrap = ps.unwrap(clientPreparedStatement);
String mysql = unwrap.toString();
String sql = mysql.substring(mysql.indexOf(": ") + 1).trim();
// 控制台打印替换占位符后的 SQL 语句
System.out.println(!mysql.contains("EXCEPTION: ") ? sql : null);
return proceed;
}
}
没有打印 SQL 的方法
没有打印 SQL 的方法就需要获取传给 Statement
的参数和 SQL 语句再手动拼接,获取步骤如下:
- 获取
StatementHandler(BaseStatementHandler)
中的boundSql
。通过boundSql.getSql()
获取包含有占位符的 SQL ,通过boundSql.getParameterMappings()
获取参数。 - 通过 p6spy 转换参数值为对应在数据库当中的格式(自己实现需要将字符串类型替换 ` 为 `` ,还有时间和布尔等特殊类型转换)。
- 替换占位符。
其中第一步参考 ParameterHandler
默认实现类 DefaultParameterHandler
的 setParameters
方法获取参数值,方法中用到的 5 个实例变量通过 Mybatis 的 MetaObject
反射获取。第二步替换占位符参考 p6spy 的 PreparedStatementInformation
的 getSqlWithValues
,p6spy 依赖:
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
插件代码如下:
import com.p6spy.engine.common.Value;
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.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
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.springframework.stereotype.Component;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* @author haibara
*/
@Component
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "update", args = Statement.class),
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
public class CustomInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 反射获取 DefaultParameterHandler setParameters 方法需要的实例变量
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
Configuration configuration = mappedStatement.getConfiguration();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
BoundSql boundSql = statementHandler.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
// 获取准备预处理的 SQL 和用于替换占位符的参数值
final String statementQuery = boundSql.getSql().replaceAll("\\s+", " ");
List<Value> parameterValues = new ArrayList<>();
// 从 BoundSql 中获取参数值,用 p6spy 的 Value 包装参数值再保存到 parameterValues
// 参考 org.apache.ibatis.scripting.defaults.DefaultParameterHandler 的 setParameters 方法
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (ParameterMapping parameterMapping : parameterMappings) {
// 只获取输入的参数
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
// 参数是 <foreach/> 或 <bind/> 标签中的
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// 参数与数据库字段直接映射
value = parameterObject;
} else {
// 参数值需要通过反射从复杂对象中获取
MetaObject mo = configuration.newMetaObject(parameterObject);
value = mo.getValue(propertyName);
}
parameterValues.add(new Value(value));
}
}
}
// 替换 SQL 中的占位符。Java 类型 转为对应的 JdbcType 通过 p6spy Value 的 tosSring 方法
// 参考 com.p6spy.engine.common.PreparedStatementInformation 的 getSqlWithValues 方法
final StringBuilder sb = new StringBuilder();
int currentParameter = 0;
for (int pos = 0; pos < statementQuery.length(); pos++) {
char character = statementQuery.charAt(pos);
if (statementQuery.charAt(pos) == '?' && currentParameter < parameterValues.size()) {
Value value = parameterValues.get(currentParameter);
sb.append(value != null ? value.toString() : new Value().toString());
currentParameter++;
} else {
sb.append(character);
}
}
// 控制台打印替换占位符后的 SQL 语句
System.out.println(sb.toString());
return proceed;
}
}
p6spy 是一个拦截数据库执行记录的框架,通过简单配置就可以打印 SQL,它的原理是代理数据源。不过有一处代码不太懂,PreparedStatementInformation#getSqlWithValues()
里是 currentParameter <= parameterValues.size(),为什么不是 currentParameter < parameterValues.size() 呢(Fix #214 - removed Statement altogether from PreparedStatementInforma… · p6spy/p6spy@aeb861c (github.com))?
自增列
以 Mysql 数据库为例:
-
user 表结构:
create table user ( id int auto_increment primary key, name varchar(20) not null );
-
Mybatis 对应 insert xml:
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.xxxx.User" useGeneratedKeys="true"> insert into `user` (`name`) values (#{name,jdbcType=VARCHAR}) </insert>
在插件的最后添加下面代码:
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyProperties = mappedStatement.getKeyProperties();
String[] keyColumns = mappedStatement.getKeyColumns();
MetaObject metaParam = configuration.newMetaObject(parameterObject);
Object value = metaParam.getValue(keyProperties[0]);
sb.insert(sb.indexOf("(") + 1, "`" + keyColumns[0] + "`, ");
sb.insert(sb.indexOf("(", sb.indexOf(")")) + 1, value + ", ");
}
其他场景还是建议手动拼接 insert 语句。
参考
精尽MyBatis源码分析 - 插件机制 - 月圆吖 - 博客园 (cnblogs.com)
精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下) - 月圆吖 - 博客园 (cnblogs.com)
精尽MyBatis源码分析 - SQL执行过程(二)之 StatementHandler - 月圆吖 - 博客园 (cnblogs.com)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?