使用Mybatis插件打印SQL詳細內(nèi)容及執(zhí)行時間

引用自官網(wǎng):XML 映射配置文件
MyBatis 允許你在已映射語句執(zhí)行過程中的某一點進行攔截調(diào)用揭蜒。默認情況下厉碟,MyBatis 允許使用插件來攔截的方法調(diào)用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

這些類中方法的細節(jié)可以通過查看每個方法的簽名來發(fā)現(xiàn)毙石,或者直接查看 MyBatis 的發(fā)行包中的源代碼纺座。 假設(shè)你想做的不僅僅是監(jiān)控方法的調(diào)用碌秸,那么你應(yīng)該很好的了解正在重寫的方法的行為撤卢。 因為如果在試圖修改或重寫已有方法的行為的時候素征,你很可能在破壞 MyBatis 的核心模塊集嵌。 這些都是更低層的類和方法,所以使用插件的時候要特別當心御毅。
通過 MyBatis 提供的強大機制根欧,使用插件是非常簡單的,只需實現(xiàn) Interceptor 接口端蛆,并指定了想要攔截的方法簽名即可凤粗。

1. 使用插件與不使用插件(Mybatis自身日志記錄的SQL)打印SQL的區(qū)別

對比.png

以下就是使用Mybatis自身日志記錄SQL所存在的問題

  • SQL中參數(shù)都被占位符"?"替換,無法知道真正執(zhí)行的SQL語句中的參數(shù)是什么
  • 無法記錄SQL執(zhí)行時間今豆,有SQL執(zhí)行時間就可以精準定位到執(zhí)行時間比較慢的SQL

2. 實現(xiàn)SQL攔截器

攔截SQL只需要實現(xiàn)org.apache.ibatis.plugin.Interceptor接口即可:

package com.batatas.framework.mybatis.plugin;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;

@Intercepts(value = {
        @Signature(args = { Statement.class, ResultHandler.class }, method = "query", type = StatementHandler.class),
        @Signature(args = { Statement.class }, method = "update", type = StatementHandler.class),
        @Signature(args = { Statement.class }, method = "batch", type = StatementHandler.class) })
public class SqlCostInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();

        long startTime = System.currentTimeMillis();
        StatementHandler statementHandler = (StatementHandler) target;
        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            long sqlCost = endTime - startTime;
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
            Object parameterObject = boundSql.getParameterObject();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();

            // 格式化Sql語句嫌拣,去除換行符柔袁,替換參數(shù)
            sql = formatSql(sql, parameterObject, parameterMappingList);

            System.out.println("SQL:[" + sql + "]執(zhí)行耗時[" + sqlCost + "ms]");

        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private String formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {
        // 輸入判斷是否為空
        if (sql == "" || sql.length() == 0) {
            return "";
        }
        // 美化sql
        sql = beautifySql(sql);

        // 不傳參數(shù)的場景,直接把Sql美化一下返回出去
        if (parameterObject == null || parameterMappingList == null || parameterMappingList.size() == 0) {
            return sql;
        }

        // 定義一個沒有替換過占位符的sql亭罪,用于出異常時返回
        String sqlWithoutReplacePlaceholder = sql;
        try {
            if (parameterMappingList != null) {
                Class<?> parameterObjectClass = parameterObject.getClass();

                // 如果參數(shù)是StrictMap且Value類型為Collection瘦馍,獲取key="list"的屬性,這里主要是為了處理<foreach>循環(huán)時傳入List這種參數(shù)的占位符替換
                // 例如select * from xxx where id in <foreach
                // collection="list">...</foreach>
                if (isStrictMap(parameterObjectClass)) {
                    StrictMap<Collection<?>> strictMap = (StrictMap<Collection<?>>) parameterObject;

                    if (isList(strictMap.get("list").getClass())) {
                        sql = handleListParameter(sql, strictMap.get("list"));
                    }
                } else if (isMap(parameterObjectClass)) {
                    // 如果參數(shù)是Map則直接強轉(zhuǎn)应役,通過map.get(key)方法獲取真正的屬性值
                    // 這里主要是為了處理<insert>情组、<delete>、<update>箩祥、<select>時傳入parameterType為map的場景
                    Map<?, ?> paramMap = (Map<?, ?>) parameterObject;
                    sql = handleMapParameter(sql, paramMap, parameterMappingList);
                } else {
                    // 通用場景院崇,比如傳的是一個自定義的對象或者八種基本數(shù)據(jù)類型之一或者String
                    sql = handleCommonParameter(sql, parameterMappingList, parameterObjectClass, parameterObject);
                }
            }
        } catch (Exception e) {
            // 占位符替換過程中出現(xiàn)異常,則返回沒有替換過占位符但是格式美化過的sql袍祖,這樣至少保證sql語句比BoundSql中的sql更好看
            return sqlWithoutReplacePlaceholder;
        }

        return sql;
    }

    /**
     * 處理通用場景
     * 
     * @throws SecurityException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     */
    private String handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList,
            Class<?> parameterObjectClass, Object parameterObject) throws Exception {
        for (ParameterMapping parameterMapping : parameterMappingList) {
            String propertyValue = null;
            // 基本數(shù)據(jù)類型或者基本數(shù)據(jù)類型的包裝類底瓣,直接toString即可獲取其真正的參數(shù)值,其余直接取paramterMapping中的property屬性即可
            if (isPrimitiveOrPrimitiveWrapper(parameterObjectClass)) {
                propertyValue = parameterObject.toString();
            } else {
                String propertyName = parameterMapping.getProperty();

                Field field = parameterObjectClass.getDeclaredField(propertyName);
                // 要獲取Field中的屬性值蕉陋,這里必須將私有屬性的accessible設(shè)置為true
                field.setAccessible(true);
                propertyValue = String.valueOf(field.get(parameterObject));
                if (parameterMapping.getJavaType().isAssignableFrom(String.class)) {
                    propertyValue = "\"" + propertyValue + "\"";
                }
            }

            sql = sql.replaceFirst("\\?", propertyValue);
        }

        return sql;
    }

    /**
     * 處理Map場景
     */
    private String handleMapParameter(String sql, Map<?, ?> paramMap, List<ParameterMapping> parameterMappingList) {
        for (ParameterMapping parameterMapping : parameterMappingList) {
            Object propertyName = parameterMapping.getProperty();
            Object propertyValue = paramMap.get(propertyName);
            if (propertyValue != null) {
                if (propertyValue.getClass().isAssignableFrom(String.class)) {
                    propertyValue = "\"" + propertyValue + "\"";
                }

                sql = sql.replaceFirst("\\?", propertyValue.toString());
            }
        }
        return sql;
    }

    /**
     * @Description: 處理List場景
     * @param sql
     * @param collection
     */
    private String handleListParameter(String sql, Collection<?> col) {
        if (col != null && col.size() != 0) {
            for (Object obj : col) {
                String value = null;
                Class<?> objClass = obj.getClass();

                // 只處理基本數(shù)據(jù)類型捐凭、基本數(shù)據(jù)類型的包裝類、String這三種
                // 如果是復(fù)合類型也是可以的凳鬓,不過復(fù)雜點且這種場景較少茁肠,寫代碼的時候要判斷一下要拿到的是復(fù)合類型中的哪個屬性
                if (isPrimitiveOrPrimitiveWrapper(objClass)) {
                    value = obj.toString();
                } else if (objClass.isAssignableFrom(String.class)) {
                    value = "\"" + obj.toString() + "\"";
                }

                sql = sql.replaceFirst("\\?", value);
            }
        }

        return sql;
    }

    private String beautifySql(String sql) {
        // sql = sql.replace("\n", "").replace("\t", "").replace(" ", "
        // ").replace("( ", "(").replace(" )", ")").replace(" ,", ",");
        sql = sql.replaceAll("[\\s\n ]+", " ");
        return sql;
    }

    /**
     * 是否基本數(shù)據(jù)類型或者基本數(shù)據(jù)類型的包裝類
     */
    private boolean isPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) {
        return parameterObjectClass.isPrimitive() || (parameterObjectClass.isAssignableFrom(Byte.class)
                || parameterObjectClass.isAssignableFrom(Short.class)
                || parameterObjectClass.isAssignableFrom(Integer.class)
                || parameterObjectClass.isAssignableFrom(Long.class)
                || parameterObjectClass.isAssignableFrom(Double.class)
                || parameterObjectClass.isAssignableFrom(Float.class)
                || parameterObjectClass.isAssignableFrom(Character.class)
                || parameterObjectClass.isAssignableFrom(Boolean.class));
    }

    /**
     * 是否DefaultSqlSession的內(nèi)部類StrictMap
     */
    private boolean isStrictMap(Class<?> parameterObjectClass) {
        return parameterObjectClass.isAssignableFrom(StrictMap.class);
    }

    /**
     * 是否List的實現(xiàn)類
     */
    private boolean isList(Class<?> clazz) {
        Class<?>[] interfaceClasses = clazz.getInterfaces();
        for (Class<?> interfaceClass : interfaceClasses) {
            if (interfaceClass.isAssignableFrom(List.class)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 是否Map的實現(xiàn)類
     */
    private boolean isMap(Class<?> parameterObjectClass) {
        Class<?>[] interfaceClasses = parameterObjectClass.getInterfaces();
        for (Class<?> interfaceClass : interfaceClasses) {
            if (interfaceClass.isAssignableFrom(Map.class)) {
                return true;
            }
        }

        return false;
    }

}

3. 配置插件

配置插件,只需要在mybatis-config.xml文件中添加插件即可:

<!-- mybatis-config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
       <plugin interceptor="com.batatas.framework.mybatis.plugin.SqlCostInterceptor">
      <!--      <property name="prop1" value="prop1"/>
           <property name="prop2" value="prop2"/> -->
       </plugin>
    </plugins>
</configuration>

4. 測試

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缩举,一起剝皮案震驚了整個濱河市垦梆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仅孩,老刑警劉巖托猩,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異辽慕,居然都是意外死亡京腥,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門溅蛉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绞旅,“玉大人,你說我怎么就攤上這事温艇。” “怎么了堕汞?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵勺爱,是天一觀的道長。 經(jīng)常有香客問我讯检,道長琐鲁,這世上最難降的妖魔是什么卫旱? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮围段,結(jié)果婚禮上顾翼,老公的妹妹穿的比我還像新娘。我一直安慰自己奈泪,他們只是感情好适贸,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涝桅,像睡著了一般拜姿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冯遂,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天蕊肥,我揣著相機與錄音,去河邊找鬼蛤肌。 笑死壁却,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的裸准。 我是一名探鬼主播展东,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼狼速!你這毒婦竟也來了琅锻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤向胡,失蹤者是張志新(化名)和其女友劉穎恼蓬,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體僵芹,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡处硬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拇派。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荷辕。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖件豌,靈堂內(nèi)的尸體忽然破棺而出疮方,到底是詐尸還是另有隱情,我是刑警寧澤茧彤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布骡显,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏惫谤。R本人自食惡果不足惜壁顶,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望溜歪。 院中可真熱鬧若专,春花似錦、人聲如沸蝴猪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拯腮。三九已至窖式,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間动壤,已是汗流浹背萝喘。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留琼懊,地道東北人阁簸。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像哼丈,于是被迫代替她去往敵國和親启妹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1. 簡介 1.1 什么是 MyBatis 醉旦? MyBatis 是支持定制化 SQL饶米、存儲過程以及高級映射的優(yōu)秀的...
    笨鳥慢飛閱讀 5,510評論 0 4
  • 1 引言# 本文主要講解JDBC怎么演變到Mybatis的漸變過程,重點講解了為什么要將JDBC封裝成Mybait...
    七寸知架構(gòu)閱讀 76,460評論 36 980
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理车胡,服務(wù)發(fā)現(xiàn)檬输,斷路器,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • 官方文檔 簡介 入門 XML配置 XML映射文件 動態(tài)SQL Java API SQL語句構(gòu)建器 日志 一匈棘、 JD...
    拾壹北閱讀 3,544評論 0 52
  • 今天下班后去雙語給朋友的兒子送東西丧慈,到學(xué)校門口還沒下課就在那等了會,再去托輔接兒子時已經(jīng)6:40多了主卫,兒子...
    三年級五班劉佳銘閱讀 234評論 0 0