使用MyBatis攔截器實(shí)現(xiàn)SQL的完整打印
當(dāng)我們使用Mybatis結(jié)合Mybatis-plus進(jìn)行開發(fā)時(shí),為了查看執(zhí)行sql的信息通常我們可以通過(guò)屬性配置的方式打印出執(zhí)行的sql語(yǔ)句,但這樣的打印出了sql語(yǔ)句常帶有占位符信息,不利于排錯(cuò)。
為了解決這一痛點(diǎn)問(wèn)題,我們可以通過(guò)Mybatis提供的攔截器,來(lái)獲取到真正執(zhí)行的sql信息,從而避免我們手動(dòng)替換占位符的額外操作。
前言
在日常使用Mybatis-plus開發(fā)時(shí),為了能獲取到執(zhí)行的sql語(yǔ)句,通常可以在配置文件進(jìn)入如下的配置:
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mappers/*.xml
通過(guò)配置MyBatis-plus中將log-impl的日志打印的實(shí)現(xiàn)為org.apache.ibatis.logging.stdout.StdOutImpl ,以實(shí)現(xiàn)sql語(yǔ)句在控制臺(tái)的打印。
此時(shí),當(dāng)我們執(zhí)行如下sql信息時(shí):
<select id="selectByUserName" resultType="com.example.pojo.User">
select user_name userName , age from t_user where user_name = #{name}
</select>
可以看到在控制臺(tái)會(huì)打印出如下內(nèi)容:

不難發(fā)現(xiàn),我們打印出的sql信息其實(shí)是帶有占位符的。如果我們想在sql工具中對(duì)sql進(jìn)行執(zhí)行,則需要我們手動(dòng)對(duì)占位符進(jìn)行替換,對(duì)于上述這樣的sql來(lái)說(shuō)這并不是一件難事。但當(dāng)sql相關(guān)查詢參數(shù)比較多的時(shí),通過(guò)手動(dòng)對(duì)sql的占位符進(jìn)行替換顯然不是一件明智的舉措了。
為了解決這一問(wèn)題,我們其實(shí)可以借助Mybatis提供的攔截器來(lái)獲取真正執(zhí)行的sql信息,從而避免手動(dòng)對(duì)占位符的替換!
Mybatis的攔截器
Interceptor是MyBatis一個(gè)非常強(qiáng)大的特性,它允許你攔截執(zhí)行的sql 語(yǔ)句,并在 sql執(zhí)行前后進(jìn)行自定義處理。從而實(shí)現(xiàn)諸如日志記錄、參數(shù)修改、結(jié)果處理、分頁(yè)等功能。
通常MyBatis內(nèi)部允許對(duì)sql執(zhí)行過(guò)程中Executor、ParameterHandler、ResultSetHandler 和 StatementHandler四個(gè)關(guān)鍵節(jié)進(jìn)行攔截。眾所周知,Executor是sql執(zhí)行過(guò)程的核心組件。Executor會(huì)調(diào)用 StatementHandler 和 ParameterHandler 來(lái)完成sql的準(zhǔn)備和執(zhí)行。
因此,對(duì)于Executor攔截可以獲取執(zhí)行sql,并且對(duì)于sql 執(zhí)行前后添加自定義邏輯,如緩存邏輯,在查詢語(yǔ)句執(zhí)行前后檢查和添加緩存。
進(jìn)一步來(lái)看,對(duì)于Execuotr而言,其還允許在數(shù)據(jù)庫(kù)操作的不同階段進(jìn)行精確的干預(yù)和攔截。例如,如果對(duì)Executor中的update方法進(jìn)行攔截,則其可以獲取sql執(zhí)行中 insert、update、delete三種類型的sql語(yǔ)句。而對(duì)Executor# query方法攔截器,其則可以獲取 select 類型的 sql 語(yǔ)句。
知曉了MyBatis中Interceptor對(duì)于Mybatis核心組件Executor的攔截邏輯后。接下來(lái),我們將主要介紹如何在Mybatis中自定義一個(gè)自己的Interceptor。
事實(shí)上,如果要在MyBatis 中編寫一個(gè)攔截器,則首先需要實(shí)現(xiàn) Interceptor 接口,該接口主要包含如下方法:
intercept方法
intercept 方法接收一個(gè) Invocation 對(duì)象,代表被攔截的方法調(diào)用。這個(gè)方法可以在方法調(diào)用前后執(zhí)行自定義邏輯,并決定是否繼續(xù)執(zhí)行原方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 在這里編寫攔截邏輯
return invocation.proceed(); // 繼續(xù)執(zhí)行原方法
}
plugin方法
plugin 方法用于生成目標(biāo)對(duì)象的代理。如果目標(biāo)對(duì)象是需要攔截的類型,返回代理對(duì)象;否則直接返回目標(biāo)對(duì)象。
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
setProperties方法
setProperties 方法用于接收在配置文件中定義的屬性,這些屬性可以用來(lái)配置攔截器的行為。
@Override
public void setProperties(Properties properties) {
// 讀取配置屬性
}
如下是Interceptor的一個(gè)統(tǒng)計(jì)sql執(zhí)行時(shí)長(zhǎng)的示例代碼:
package com.example.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlPrintInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
String sql = statementHandler.getBoundSql().getSql();
long startTime = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long endTime = System.currentTimeMillis();
System.out.println("SQL: " + sql);
System.out.println("Execution Time: " + (endTime - startTime) + "ms");
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
sql打印攔截器
經(jīng)過(guò)上述分析,相信大家對(duì)Mybatis中的Interceptor已經(jīng)有了比較整體的認(rèn)識(shí)。接下來(lái),我們便來(lái)分析該如何構(gòu)建一個(gè)打印完整sql的攔截器。
在開始寫代碼時(shí),首先來(lái)對(duì)我們的需求進(jìn)行再次明確。我們的目標(biāo)是期待通過(guò)Mybatis的Interceptor來(lái)實(shí)現(xiàn)完整sql的打印。 而如果要實(shí)現(xiàn)這一目標(biāo),對(duì)Executor進(jìn)行攔截?zé)o疑來(lái)說(shuō)是恰當(dāng)?shù)倪x擇。因?yàn)?code>Executor是Mybatis執(zhí)行sql的一個(gè)媒介,其調(diào)用 StatementHandler 和 ParameterHandler 來(lái)完成對(duì)sql的準(zhǔn)備和執(zhí)行。明確了攔截器的切入點(diǎn)后,我們?cè)賮?lái)看我們要對(duì)Executor中的那些方法進(jìn)行攔截。
正如之前介紹的那樣," 如果攔截 Executor 中的 update 方法,可以捕獲執(zhí)行 insert、update 和 delete 三種類型的 SQL 語(yǔ)句。相反,攔截 Executor 的 query 方法將允許對(duì) select 類型的 SQL 語(yǔ)句進(jìn)行捕獲。"
因此,在構(gòu)建攔截器時(shí)我們的@Signature內(nèi)容如下:
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
我們對(duì)Executor中的query和update方法進(jìn)行攔截,其中args表示的是方法入?yún)⑿畔?。由?code>Executor中的query方法存在方法的重載,所以出現(xiàn)兩次!
在此基礎(chǔ)上,我們構(gòu)建出的攔截器如下:
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
@Slf4j
public class SqlInterceptor implements Interceptor {
/**
* 默認(rèn)替換字符
*/
public static final String UNKNOWN = "UNKNOWN";
/**
* 替換sql中的?占位符
*/
public static final String SQL_PLACEHOLDER = "#{%s}";
@Override
public Object intercept(Invocation invocation) throws Throwable {
String completeSql = "";
try {
completeSql = getCompleteSqlInfo(invocation);
}catch (RuntimeException e) {
log.error("獲取sql信息出錯(cuò),異常信息 ",e);
}finally {
log.info("sql執(zhí)行信息:[{}] ",completeSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 獲取完整的sql信息
* @param invocation
* @return
*/
private String getCompleteSqlInfo(Invocation invocation) {
// invocation中的Args數(shù)組中第一個(gè)參數(shù)即為MappedStatement對(duì)象
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// invocation中的Args數(shù)組中第二個(gè)參數(shù)為sql語(yǔ)句所需要的參數(shù)
Object parameter = null;
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
return generateCompleteSql(mappedStatement, parameter);
}
private String generateCompleteSql(MappedStatement mappedStatement, Object parameter) {
// 獲取sql語(yǔ)句
String mappedStatementId = mappedStatement.getId();
// BoundSql就是封裝myBatis最終產(chǎn)生的sql類
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
// 格式化sql信息
String sql = SqlFormatter.format(boundSql.getSql());
// 獲取參數(shù)列表
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
Object parameterObject = boundSql.getParameterObject();
Configuration configuration = mappedStatement.getConfiguration();
if (!CollUtil.isEmpty(parameterMappings) && parameterObject != null) {
// 遍歷參數(shù)完成對(duì)占位符的替換處理
for (int i = 0 ; i < parameterMappings.size() ; i++) {
String replacePlaceHolder = String.format(SQL_PLACEHOLDER,i);
sql = sql.replaceFirst("\?",replacePlaceHolder);
}
// MetaObject主要是封裝了originalObject對(duì)象,提供了get和set的方法用于獲取和設(shè)置originalObject的屬性值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (int i = 0 ; i < parameterMappings.size() ; i ++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
String replacePlaceHolder = String.format(SQL_PLACEHOLDER,i);
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst(Pattern.quote(replacePlaceHolder),
Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 處理動(dòng)態(tài)sql標(biāo)簽信息
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst(Pattern.quote(replacePlaceHolder),
Matcher.quoteReplacement(getParameterValue(obj)));
} else {
// 未知參數(shù),替換?為特定字符
sql = sql.replaceFirst(Pattern.quote(replacePlaceHolder), UNKNOWN);
}
}
}
StringBuilder formatSql = new StringBuilder()
.append(" mappedStatementId - ID:").append(mappedStatementId)
.append(StringPool.NEWLINE).append("Execute SQL:").append(sql);
return formatSql.toString();
}
/**
*
* @author 毅航
* @date 2024/7/7 9:14
*/
private static String getParameterValue(Object obj) {
// 直接返回空字符串將避免在 SQL 查詢中加入不必要的單引號(hào),從而保持查詢的正確性。
if (obj == null) {
return "";
}
String stringValue = obj.toString();
// 對(duì)于非空字符串,我們添加單引號(hào)以滿足以滿足參數(shù)優(yōu)化的需求。
return "'" + stringValue + "'";
}
為了讀者能快速理解上述攔截器的原理,小編在此上述代碼中的generateCompleteSql的處理邏輯進(jìn)行簡(jiǎn)單的分析。
首先,generateCompleteSql方法的主要目的是生成一個(gè)完整的、可讀性高的Sql語(yǔ)句,其它接收兩個(gè)參數(shù):MappedStatement對(duì)象和parameter(參數(shù)對(duì)象)。其內(nèi)部邏輯如下:
獲取SQL語(yǔ)句和基本信息:
mappedStatementId存儲(chǔ)了MappedStatement的ID,這通常與MyBatis中的映射語(yǔ)句相關(guān)聯(lián)。- 通過(guò)
mappedStatement.getBoundSql(parameter)獲取BoundSql對(duì)象,其中包含了未解析的SQL語(yǔ)句和參數(shù)映射信息。
格式化SQL語(yǔ)句:
- 調(diào)用
SqlFormatter.format()方法來(lái)格式化SQL語(yǔ)句,增加可讀性。
- 調(diào)用
準(zhǔn)備參數(shù)信息:
- 從
BoundSql中提取參數(shù)映射列表parameterMappings和參數(shù)對(duì)象parameterObject。 - 檢查參數(shù)映射列表是否非空且參數(shù)對(duì)象非空,這是進(jìn)行參數(shù)替換的前提。
- 從
參數(shù)替換:
- 遍歷參數(shù)映射列表,使用正則表達(dá)式和字符串操作,將SQL語(yǔ)句中的
?占位符替換為特定的占位符(如#{param0})。 - 利用
configuration.newMetaObject(parameterObject)創(chuàng)建MetaObject,用于訪問(wèn)參數(shù)對(duì)象的屬性。 - 對(duì)于每個(gè)參數(shù)映射,嘗試通過(guò)
MetaObject獲取屬性值或通過(guò)BoundSql的附加參數(shù)信息獲取值,然后將這些值轉(zhuǎn)換為字符串形式,再替換到SQL語(yǔ)句中。 - 如果屬性值無(wú)法通過(guò)上述方式獲取,則將占位符替換為預(yù)定義的未知標(biāo)識(shí)符
UNKNOWN。
- 遍歷參數(shù)映射列表,使用正則表達(dá)式和字符串操作,將SQL語(yǔ)句中的
構(gòu)建并返回完整SQL語(yǔ)句:
- 最后,構(gòu)造一個(gè)字符串,包含
mappedStatementId和最終的SQL語(yǔ)句,便于日志記錄或調(diào)試。 - 返回這個(gè)字符串作為函數(shù)的結(jié)果。
- 最后,構(gòu)造一個(gè)字符串,包含

將上述攔截器注入Spring容器,
@Configuration
public class MybatisConfigBean {
@Bean
public SqlInterceptor addMybatisInterceptor() {
return new SqlInterceptor();
}
}
啟動(dòng)SpringBoot應(yīng)用,然后執(zhí)行相關(guān)sql時(shí),可以看到控制臺(tái)有如下輸出:
2024-07-07 10:11:46.407 INFO 19076 --- [nio-8080-exec-9] com.example.Interceptor.SqlInterceptor
: sql執(zhí)行信息:[ mappedStatementId - ID:com.example.dao.UserMapper.selectByUserName
Execute SQL:select
user_name userName ,
age
from
t_user
where
user_name = 'zhangSan?'
and remark = 'test1']
至此,我們就利用MyBatis對(duì)外暴露出的Interceptor接口,手動(dòng)實(shí)現(xiàn)一個(gè)能優(yōu)雅地打印完整sql日志的攔截器!
總結(jié)
本文首先對(duì)Mybatis內(nèi)置sql打印機(jī)制進(jìn)行了分析,深入闡述了其所面臨痛點(diǎn),然后對(duì)Mybatis的攔截器機(jī)制進(jìn)行了深入介紹,并借助攔截器截止,實(shí)現(xiàn)了一款可以完整打印sql的攔截器!
以上就是使用MyBatis攔截器實(shí)現(xiàn)SQL的完整打印的詳細(xì)內(nèi)容,更多關(guān)于MyBatis攔截器SQL打印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis-plus查詢無(wú)數(shù)據(jù)問(wèn)題及解決
這篇文章主要介紹了mybatis-plus查詢無(wú)數(shù)據(jù)問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
Spring注解開發(fā)@Bean和@ComponentScan使用案例
這篇文章主要介紹了Spring注解開發(fā)@Bean和@ComponentScan使用案例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程
這篇文章主要介紹了在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程,這樣便是在Linux系統(tǒng)下搭建完整的Java和JSP開發(fā)環(huán)境,需要的朋友可以參考下2015-08-08
Java實(shí)現(xiàn)WebSocket四個(gè)步驟
這篇文章主要為大家介紹了Java實(shí)現(xiàn)WebSocket的方法實(shí)例,只需要簡(jiǎn)單四個(gè)步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
java調(diào)用未知類的指定方法簡(jiǎn)單實(shí)例
這篇文章介紹了java調(diào)用未知類的指定方法簡(jiǎn)單實(shí)例,有需要的朋友可以參考一下2013-09-09
SpringBoot調(diào)用WebService接口方法示例代碼
這篇文章主要介紹了使用SpringWebServices調(diào)用SOAP?WebService接口的步驟,包括導(dǎo)入依賴、創(chuàng)建請(qǐng)求類和響應(yīng)類、生成ObjectFactory類、配置WebServiceTemplate、調(diào)用WebService接口以及測(cè)試代碼,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-02-02
Spring超詳細(xì)講解事務(wù)和事務(wù)傳播機(jī)制
Spring事務(wù)的本質(zhì)就是對(duì)數(shù)據(jù)庫(kù)事務(wù)的支持,沒(méi)有數(shù)據(jù)庫(kù)事務(wù),Spring是無(wú)法提供事務(wù)功能的。Spring只提供統(tǒng)一的事務(wù)管理接口,具體實(shí)現(xiàn)都是由數(shù)據(jù)庫(kù)自己實(shí)現(xiàn)的,Spring會(huì)在事務(wù)開始時(shí),根據(jù)當(dāng)前設(shè)置的隔離級(jí)別,調(diào)整數(shù)據(jù)庫(kù)的隔離級(jí)別,由此保持一致2022-06-06
java Iterator接口和LIstIterator接口分析
這篇文章主要介紹了java Iterator接口和LIstIterator接口分析的相關(guān)資料,需要的朋友可以參考下2017-05-05

