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() 呢?
自增列
以 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)