使用MyBatis攔截器實現(xiàn)SQL的完整打印
當我們使用Mybatis
結(jié)合Mybatis-plus
進行開發(fā)時,為了查看執(zhí)行sql
的信息通常我們可以通過屬性配置的方式打印出執(zhí)行的sql
語句,但這樣的打印出了sql
語句常帶有占位符
信息,不利于排錯。
為了解決這一痛點問題,我們可以通過Mybatis
提供的攔截器,來獲取到真正執(zhí)行的sql
信息,從而避免我們手動替換占位符
的額外操作。
前言
在日常使用Mybatis-plus
開發(fā)時,為了能獲取到執(zhí)行的sql
語句,通??梢栽?code>配置文件進入如下的配置:
mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:mappers/*.xml
通過配置MyBatis-plus
中將log-impl
的日志打印的實現(xiàn)為org.apache.ibatis.logging.stdout.StdOutImpl
,以實現(xiàn)sql
語句在控制臺的打印。
此時,當我們執(zhí)行如下sql
信息時:
<select id="selectByUserName" resultType="com.example.pojo.User"> select user_name userName , age from t_user where user_name = #{name} </select>
可以看到在控制臺會打印出如下內(nèi)容:
不難發(fā)現(xiàn),我們打印出的sql
信息其實是帶有占位符
的。如果我們想在sql
工具中對sql
進行執(zhí)行,則需要我們手動對占位符
進行替換,對于上述這樣的sql
來說這并不是一件難事。但當sql
相關(guān)查詢參數(shù)比較多的時,通過手動對sql
的占位符
進行替換顯然不是一件明智的舉措了。
為了解決這一問題,我們其實可以借助Mybatis
提供的攔截器
來獲取真正執(zhí)行的sql
信息,從而避免手動對占位符
的替換!
Mybatis的攔截器
Interceptor
是MyBatis
一個非常強大的特性,它允許你攔截執(zhí)行的sql
語句,并在 sql
執(zhí)行前后進行自定義處理。從而實現(xiàn)諸如日志記錄、參數(shù)修改、結(jié)果處理、分頁等功能。
通常MyBatis
內(nèi)部允許對sql
執(zhí)行過程中Executor、ParameterHandler、ResultSetHandler 和 StatementHandler
四個關(guān)鍵節(jié)進行攔截。眾所周知,Executor
是sql
執(zhí)行過程的核心組件。Executor
會調(diào)用 StatementHandler
和 ParameterHandler
來完成sql
的準備和執(zhí)行。
因此,對于Executor
攔截可以獲取執(zhí)行sql
,并且對于sql
執(zhí)行前后添加自定義邏輯,如緩存邏輯,在查詢語句執(zhí)行前后檢查和添加緩存。
進一步來看,對于Execuotr
而言,其還允許在數(shù)據(jù)庫操作的不同階段進行精確的干預(yù)和攔截。例如,如果對Executor
中的update
方法進行攔截,則其可以獲取sql
執(zhí)行中 insert、update、delete
三種類型的sql
語句。而對Executor# query
方法攔截器,其則可以獲取 select
類型的 sql
語句。
知曉了MyBatis
中Interceptor
對于Mybatis
核心組件Executor
的攔截邏輯后。接下來,我們將主要介紹如何在Mybatis
中自定義一個自己的Interceptor
。
事實上,如果要在MyBatis
中編寫一個攔截器,則首先需要實現(xiàn) Interceptor
接口,該接口主要包含如下方法:
intercept
方法
intercept
方法接收一個 Invocation
對象,代表被攔截的方法調(diào)用。這個方法可以在方法調(diào)用前后執(zhí)行自定義邏輯,并決定是否繼續(xù)執(zhí)行原方法。
@Override public Object intercept(Invocation invocation) throws Throwable { // 在這里編寫攔截邏輯 return invocation.proceed(); // 繼續(xù)執(zhí)行原方法 }
plugin
方法
plugin
方法用于生成目標對象的代理。如果目標對象是需要攔截的類型,返回代理對象;否則直接返回目標對象。
@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
setProperties
方法
setProperties
方法用于接收在配置文件中定義的屬性,這些屬性可以用來配置攔截器的行為。
@Override public void setProperties(Properties properties) { // 讀取配置屬性 }
如下是Interceptor
的一個統(tǒng)計sql
執(zhí)行時長的示例代碼:
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)過上述分析,相信大家對Mybatis
中的Interceptor
已經(jīng)有了比較整體的認識。接下來,我們便來分析該如何構(gòu)建一個打印完整sql
的攔截器。
在開始寫代碼時,首先來對我們的需求進行再次明確。我們的目標是期待通過Mybatis
的Interceptor
來實現(xiàn)完整sql
的打印。 而如果要實現(xiàn)這一目標,對Executor
進行攔截無疑來說是恰當?shù)倪x擇。因為Executor
是Mybatis
執(zhí)行sql
的一個媒介,其調(diào)用 StatementHandler
和 ParameterHandler
來完成對sql
的準備和執(zhí)行。明確了攔截器的切入點后,我們再來看我們要對Executor
中的那些方法進行攔截。
正如之前介紹的那樣," 如果攔截 Executor
中的 update
方法,可以捕獲執(zhí)行 insert
、update
和 delete
三種類型的 SQL 語句。相反,攔截 Executor
的 query
方法將允許對 select
類型的 SQL 語句進行捕獲。"
因此,在構(gòu)建攔截器時我們的@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}) })
我們對Executor
中的query
和update
方法進行攔截,其中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 { /** * 默認替換字符 */ 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信息出錯,異常信息 ",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ù)組中第一個參數(shù)即為MappedStatement對象 MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; // invocation中的Args數(shù)組中第二個參數(shù)為sql語句所需要的參數(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語句 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ù)完成對占位符的替換處理 for (int i = 0 ; i < parameterMappings.size() ; i++) { String replacePlaceHolder = String.format(SQL_PLACEHOLDER,i); sql = sql.replaceFirst("\?",replacePlaceHolder); } // MetaObject主要是封裝了originalObject對象,提供了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)) { // 處理動態(tài)sql標簽信息 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 查詢中加入不必要的單引號,從而保持查詢的正確性。 if (obj == null) { return ""; } String stringValue = obj.toString(); // 對于非空字符串,我們添加單引號以滿足以滿足參數(shù)優(yōu)化的需求。 return "'" + stringValue + "'"; }
為了讀者能快速理解上述攔截器的原理,小編在此上述代碼中的generateCompleteSql
的處理邏輯進行簡單的分析。
首先,generateCompleteSql
方法的主要目的是生成一個完整的、可讀性高的Sql
語句,其它接收兩個參數(shù):MappedStatement
對象和parameter
(參數(shù)對象)。其內(nèi)部邏輯如下:
獲取SQL語句和基本信息:
mappedStatementId
存儲了MappedStatement
的ID,這通常與MyBatis中的映射語句相關(guān)聯(lián)。- 通過
mappedStatement.getBoundSql(parameter)
獲取BoundSql
對象,其中包含了未解析的SQL語句和參數(shù)映射信息。
格式化SQL語句:
- 調(diào)用
SqlFormatter.format()
方法來格式化SQL語句,增加可讀性。
- 調(diào)用
準備參數(shù)信息:
- 從
BoundSql
中提取參數(shù)映射列表parameterMappings
和參數(shù)對象parameterObject
。 - 檢查參數(shù)映射列表是否非空且參數(shù)對象非空,這是進行參數(shù)替換的前提。
- 從
參數(shù)替換:
- 遍歷參數(shù)映射列表,使用正則表達式和字符串操作,將SQL語句中的
?
占位符替換為特定的占位符(如#{param0}
)。 - 利用
configuration.newMetaObject(parameterObject)
創(chuàng)建MetaObject
,用于訪問參數(shù)對象的屬性。 - 對于每個參數(shù)映射,嘗試通過
MetaObject
獲取屬性值或通過BoundSql
的附加參數(shù)信息獲取值,然后將這些值轉(zhuǎn)換為字符串形式,再替換到SQL語句中。 - 如果屬性值無法通過上述方式獲取,則將占位符替換為預(yù)定義的未知標識符
UNKNOWN
。
- 遍歷參數(shù)映射列表,使用正則表達式和字符串操作,將SQL語句中的
構(gòu)建并返回完整SQL語句:
- 最后,構(gòu)造一個字符串,包含
mappedStatementId
和最終的SQL語句,便于日志記錄或調(diào)試。 - 返回這個字符串作為函數(shù)的結(jié)果。
- 最后,構(gòu)造一個字符串,包含
將上述攔截器
注入Spring
容器,
@Configuration public class MybatisConfigBean { @Bean public SqlInterceptor addMybatisInterceptor() { return new SqlInterceptor(); } }
啟動SpringBoot
應(yīng)用,然后執(zhí)行相關(guān)sql
時,可以看到控制臺有如下輸出:
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
對外暴露出的Interceptor
接口,手動實現(xiàn)一個能優(yōu)雅地打印完整sql
日志的攔截器!
總結(jié)
本文首先對Mybatis
內(nèi)置sql
打印機制進行了分析,深入闡述了其所面臨痛點,然后對Mybatis
的攔截器機制進行了深入介紹,并借助攔截器截止,實現(xiàn)了一款可以完整打印sql
的攔截器!
以上就是使用MyBatis攔截器實現(xiàn)SQL的完整打印的詳細內(nèi)容,更多關(guān)于MyBatis攔截器SQL打印的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis-plus查詢無數(shù)據(jù)問題及解決
這篇文章主要介紹了mybatis-plus查詢無數(shù)據(jù)問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12Spring注解開發(fā)@Bean和@ComponentScan使用案例
這篇文章主要介紹了Spring注解開發(fā)@Bean和@ComponentScan使用案例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程
這篇文章主要介紹了在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程,這樣便是在Linux系統(tǒng)下搭建完整的Java和JSP開發(fā)環(huán)境,需要的朋友可以參考下2015-08-08SpringBoot調(diào)用WebService接口方法示例代碼
這篇文章主要介紹了使用SpringWebServices調(diào)用SOAP?WebService接口的步驟,包括導入依賴、創(chuàng)建請求類和響應(yīng)類、生成ObjectFactory類、配置WebServiceTemplate、調(diào)用WebService接口以及測試代碼,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2025-02-02java Iterator接口和LIstIterator接口分析
這篇文章主要介紹了java Iterator接口和LIstIterator接口分析的相關(guān)資料,需要的朋友可以參考下2017-05-05