欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用MyBatis攔截器實現(xiàn)SQL的完整打印

 更新時間:2024年07月09日 08:58:24   作者:毅航  
當我們使用Mybatis結(jié)合Mybatis-plus進行開發(fā)時,為了查看執(zhí)行sql的信息通常我們可以通過屬性配置的方式打印出執(zhí)行的sql語句,但這樣的打印出了sql語句常帶有占位符信息,不利于排錯,所以本文介紹了構(gòu)建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的攔截器

InterceptorMyBatis一個非常強大的特性,它允許你攔截執(zhí)行的sql 語句,并在 sql執(zhí)行前后進行自定義處理。從而實現(xiàn)諸如日志記錄、參數(shù)修改、結(jié)果處理、分頁等功能。

通常MyBatis內(nèi)部允許對sql執(zhí)行過程中Executor、ParameterHandler、ResultSetHandler 和 StatementHandler四個關(guān)鍵節(jié)進行攔截。眾所周知,Executorsql執(zhí)行過程的核心組件。Executor會調(diào)用 StatementHandlerParameterHandler 來完成sql的準備和執(zhí)行。

因此,對于Executor攔截可以獲取執(zhí)行sql,并且對于sql 執(zhí)行前后添加自定義邏輯,如緩存邏輯,在查詢語句執(zhí)行前后檢查和添加緩存。

進一步來看,對于Execuotr而言,其還允許在數(shù)據(jù)庫操作的不同階段進行精確的干預(yù)和攔截。例如,如果對Executor中的update方法進行攔截,則其可以獲取sql執(zhí)行中 insert、update、delete三種類型的sql語句。而對Executor# query方法攔截器,其則可以獲取 select 類型的 sql 語句。

知曉了MyBatisInterceptor對于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的攔截器。

在開始寫代碼時,首先來對我們的需求進行再次明確。我們的目標是期待通過MybatisInterceptor來實現(xiàn)完整sql的打印。 而如果要實現(xiàn)這一目標,對Executor進行攔截無疑來說是恰當?shù)倪x擇。因為ExecutorMybatis執(zhí)行sql的一個媒介,其調(diào)用 StatementHandlerParameterHandler 來完成對sql的準備和執(zhí)行。明確了攔截器的切入點后,我們再來看我們要對Executor中的那些方法進行攔截。

正如之前介紹的那樣," 如果攔截 Executor 中的 update 方法,可以捕獲執(zhí)行 insert、updatedelete 三種類型的 SQL 語句。相反,攔截 Executorquery 方法將允許對 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中的queryupdate方法進行攔截,其中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語句,增加可讀性。
  • 準備參數(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
  • 構(gòu)建并返回完整SQL語句:

    • 最后,構(gòu)造一個字符串,包含mappedStatementId和最終的SQL語句,便于日志記錄或調(diào)試。
    • 返回這個字符串作為函數(shù)的結(jié)果。

將上述攔截器注入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ù)問題及解決

    這篇文章主要介紹了mybatis-plus查詢無數(shù)據(jù)問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • Java 多線程死鎖的產(chǎn)生以及如何避免死鎖

    Java 多線程死鎖的產(chǎn)生以及如何避免死鎖

    這篇文章主要介紹了Java 多線程死鎖的產(chǎn)生以及如何避免死鎖,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-09-09
  • Java雪花算法的實現(xiàn)詳解

    Java雪花算法的實現(xiàn)詳解

    雪花算法(Snowflake)是一種分布式唯一ID生成算法,用于生成全局唯一的ID,使用雪花算法生成的ID通常是一個64位的整數(shù),可以根據(jù)需要進行轉(zhuǎn)換和展示,在Java等編程語言中,可以使用相應(yīng)的庫或工具來生成雪花算法的ID,本文給大家介紹了Java雪花算法的實現(xiàn)
    2023-11-11
  • Spring注解開發(fā)@Bean和@ComponentScan使用案例

    Spring注解開發(fā)@Bean和@ComponentScan使用案例

    這篇文章主要介紹了Spring注解開發(fā)@Bean和@ComponentScan使用案例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-09-09
  • 在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程

    在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程

    這篇文章主要介紹了在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程,這樣便是在Linux系統(tǒng)下搭建完整的Java和JSP開發(fā)環(huán)境,需要的朋友可以參考下
    2015-08-08
  • Java實現(xiàn)WebSocket四個步驟

    Java實現(xiàn)WebSocket四個步驟

    這篇文章主要為大家介紹了Java實現(xiàn)WebSocket的方法實例,只需要簡單四個步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2024-01-01
  • java調(diào)用未知類的指定方法簡單實例

    java調(diào)用未知類的指定方法簡單實例

    這篇文章介紹了java調(diào)用未知類的指定方法簡單實例,有需要的朋友可以參考一下
    2013-09-09
  • SpringBoot調(diào)用WebService接口方法示例代碼

    SpringBoot調(diào)用WebService接口方法示例代碼

    這篇文章主要介紹了使用SpringWebServices調(diào)用SOAP?WebService接口的步驟,包括導入依賴、創(chuàng)建請求類和響應(yīng)類、生成ObjectFactory類、配置WebServiceTemplate、調(diào)用WebService接口以及測試代碼,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2025-02-02
  • Spring超詳細講解事務(wù)和事務(wù)傳播機制

    Spring超詳細講解事務(wù)和事務(wù)傳播機制

    Spring事務(wù)的本質(zhì)就是對數(shù)據(jù)庫事務(wù)的支持,沒有數(shù)據(jù)庫事務(wù),Spring是無法提供事務(wù)功能的。Spring只提供統(tǒng)一的事務(wù)管理接口,具體實現(xiàn)都是由數(shù)據(jù)庫自己實現(xiàn)的,Spring會在事務(wù)開始時,根據(jù)當前設(shè)置的隔離級別,調(diào)整數(shù)據(jù)庫的隔離級別,由此保持一致
    2022-06-06
  • java  Iterator接口和LIstIterator接口分析

    java Iterator接口和LIstIterator接口分析

    這篇文章主要介紹了java Iterator接口和LIstIterator接口分析的相關(guān)資料,需要的朋友可以參考下
    2017-05-05

最新評論