首頁 Java java教程 MyBatis 外掛程式的使用-- 列印 SQL 及執行時間

MyBatis 外掛程式的使用-- 列印 SQL 及執行時間

Jun 25, 2017 am 09:45 AM
mybatis 列印 執行 外掛

Plugins

摘一段来自MyBatis官方文档的文字。

MyBatis允许你在某一点拦截已映射语句执行的调用。默认情况下,MyBatis允许使用插件来拦截方法调用:

  • Executor(update、query、flushStatements、commint、rollback、getTransaction、close、isClosed)

  • ParameterHandler(getParameterObject、setParameters)

  • ResultSetHandler(handleResultSets、handleOutputParameters)

  • StatementHandler(prepare、parameterize、batch、update、query)

这些类中方法的详情可以通过查看每个方法的签名来发现,而且它们的源代码存在于MyBatis发行包中。你应该理解你所覆盖方法的行为,假设你所做的要比监视调用要多。如果你尝试修改或覆盖一个给定的方法,你可能会打破MyBatis的核心。这是低层次的类和方法,要谨慎使用插件。

插件示例:打印每条SQL语句及其执行时间

以下通过代码来演示一下如何使用MyBatis的插件,要演示的场景是:打印每条真正执行的SQL语句及其执行的时间。这是一个非常有用的需求,MyBatis本身的日志可以记录SQL,但是有以下几个问题:

  1. MyBatis日志打印出来的SQL日志,参数都被占位符”?”替换,无法知道真正执行的SQL语句中的参数是什么

  2. MyBatis日志打印出来的SQL日志,有大量的换行符,通常一句SQL语句要通过十几行显示,阅读体验非常差

  3. 无法记录SQL执行时间,有SQL执行时间就可以精准定位到执行时间比较慢的SQL

写MyBatis插件非常简单,只需要实现Interceptor接口即可,我这里将我的Interceptor命名为SqlCostInterceptor:

  1 /**  2  * Sql执行时间记录拦截器 
  3  */  4 @Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),  5     @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),  6     @Signature(type = StatementHandler.class, method = "batch", args = { Statement.class })})  7 public class SqlCostInterceptor implements Interceptor {  8    9     @Override 10     public Object intercept(Invocation invocation) throws Throwable { 11         Object target = invocation.getTarget(); 12           13         long startTime = System.currentTimeMillis(); 14         StatementHandler statementHandler = (StatementHandler)target; 15         try { 16             return invocation.proceed(); 17         } finally { 18             long endTime = System.currentTimeMillis(); 19             long sqlCost = endTime - startTime; 20               21             BoundSql boundSql = statementHandler.getBoundSql(); 22             String sql = boundSql.getSql(); 23             Object parameterObject = boundSql.getParameterObject(); 24             List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings(); 25               26             // 格式化Sql语句,去除换行符,替换参数 27             sql = formatSql(sql, parameterObject, parameterMappingList); 28               29             System.out.println("SQL:[" + sql + "]执行耗时[" + sqlCost + "ms]"); 30         } 31     } 32   33     @Override 34     public Object plugin(Object target) { 35         return Plugin.wrap(target, this); 36     } 37   38     @Override 39     public void setProperties(Properties properties) { 40           41     } 42       43     @SuppressWarnings("unchecked") 44     private String formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) { 45         // 输入sql字符串空判断 46         if (sql == null || sql.length() == 0) { 47             return ""; 48         } 49           50         // 美化sql 51         sql = beautifySql(sql); 52           53         // 不传参数的场景,直接把Sql美化一下返回出去 54         if (parameterObject == null || parameterMappingList == null || parameterMappingList.size() == 0) { 55             return sql; 56         } 57           58         // 定义一个没有替换过占位符的sql,用于出异常时返回 59         String sqlWithoutReplacePlaceholder = sql; 60           61         try { 62             if (parameterMappingList != null) { 63                 Class<?> parameterObjectClass = parameterObject.getClass(); 64   65                 // 如果参数是StrictMap且Value类型为Collection,获取key="list"的属性,这里主要是为了处理<foreach>循环时传入List这种参数的占位符替换 66                 // 例如select * from xxx where id in <foreach collection="list">...</foreach> 67                 if (isStrictMap(parameterObjectClass)) { 68                     StrictMap<Collection<?>> strictMap = (StrictMap<Collection<?>>)parameterObject; 69                       70                     if (isList(strictMap.get("list").getClass())) { 71                         sql = handleListParameter(sql, strictMap.get("list")); 72                     } 73                 } else if (isMap(parameterObjectClass)) { 74                     // 如果参数是Map则直接强转,通过map.get(key)方法获取真正的属性值 75                     // 这里主要是为了处理<insert>、<delete>、<update>、<select>时传入parameterType为map的场景 76                     Map<?, ?> paramMap = (Map<?, ?>) parameterObject; 77                     sql = handleMapParameter(sql, paramMap, parameterMappingList); 78                 } else { 79                     // 通用场景,比如传的是一个自定义的对象或者八种基本数据类型之一或者String 80                     sql = handleCommonParameter(sql, parameterMappingList, parameterObjectClass, parameterObject); 81                 } 82             } 83         } catch (Exception e) { 84             // 占位符替换过程中出现异常,则返回没有替换过占位符但是格式美化过的sql,这样至少保证sql语句比BoundSql中的sql更好看 85             return sqlWithoutReplacePlaceholder; 86         } 87           88         return sql; 89     } 90       91     /** 92      * 美化Sql 93      */ 94     private String beautifySql(String sql) { 95         sql = sql.replace("\n", "").replace("\t", "").replace("  ", " ").replace("( ", "(").replace(" )", ")").replace(" ,", ","); 96           97         return sql; 98     } 99      100     /**101      * 处理参数为List的场景102      */103     private String handleListParameter(String sql, Collection<?> col) {104         if (col != null && col.size() != 0) {105             for (Object obj : col) {106                 String value = null;107                 Class<?> objClass = obj.getClass();108                  109                 // 只处理基本数据类型、基本数据类型的包装类、String这三种110                 // 如果是复合类型也是可以的,不过复杂点且这种场景较少,写代码的时候要判断一下要拿到的是复合类型中的哪个属性111                 if (isPrimitiveOrPrimitiveWrapper(objClass)) {112                     value = obj.toString();113                 } else if (objClass.isAssignableFrom(String.class)) {114                     value = "\"" + obj.toString() + "\""; 
115                 }116                  117                 sql = sql.replaceFirst("\\?", value);118             }119         }120          121         return sql;122     }123      124     /**125      * 处理参数为Map的场景126      */127     private String handleMapParameter(String sql, Map<?, ?> paramMap, List<ParameterMapping> parameterMappingList) {128         for (ParameterMapping parameterMapping : parameterMappingList) {129             Object propertyName = parameterMapping.getProperty();130             Object propertyValue = paramMap.get(propertyName);131             if (propertyValue != null) {132                 if (propertyValue.getClass().isAssignableFrom(String.class)) {133                     propertyValue = "\"" + propertyValue + "\"";134                 }135  136                 sql = sql.replaceFirst("\\?", propertyValue.toString());137             }138         }139          140         return sql;141     }142      143     /**144      * 处理通用的场景145      */146     private String handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList, Class<?> parameterObjectClass, 
147             Object parameterObject) throws Exception {148         for (ParameterMapping parameterMapping : parameterMappingList) {149             String propertyValue = null;150             // 基本数据类型或者基本数据类型的包装类,直接toString即可获取其真正的参数值,其余直接取paramterMapping中的property属性即可151             if (isPrimitiveOrPrimitiveWrapper(parameterObjectClass)) {152                 propertyValue = parameterObject.toString();153             } else {154                 String propertyName = parameterMapping.getProperty();155                  156                 Field field = parameterObjectClass.getDeclaredField(propertyName);157                 // 要获取Field中的属性值,这里必须将私有属性的accessible设置为true158                 field.setAccessible(true);159                 propertyValue = String.valueOf(field.get(parameterObject));160                 if (parameterMapping.getJavaType().isAssignableFrom(String.class)) {161                     propertyValue = "\"" + propertyValue + "\"";162                 }163             }164  165             sql = sql.replaceFirst("\\?", propertyValue);166         }167          168         return sql;169     }170      171     /**172      * 是否基本数据类型或者基本数据类型的包装类173      */174     private boolean isPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) {175         return parameterObjectClass.isPrimitive() || 
176                 (parameterObjectClass.isAssignableFrom(Byte.class) || parameterObjectClass.isAssignableFrom(Short.class) ||177                         parameterObjectClass.isAssignableFrom(Integer.class) || parameterObjectClass.isAssignableFrom(Long.class) ||178                         parameterObjectClass.isAssignableFrom(Double.class) || parameterObjectClass.isAssignableFrom(Float.class) ||179                         parameterObjectClass.isAssignableFrom(Character.class) || parameterObjectClass.isAssignableFrom(Boolean.class));180     }181      182     /**183      * 是否DefaultSqlSession的内部类StrictMap184      */185     private boolean isStrictMap(Class<?> parameterObjectClass) {186         return parameterObjectClass.isAssignableFrom(StrictMap.class);187     }188      189     /**190      * 是否List的实现类191      */192     private boolean isList(Class<?> clazz) {193         Class<?>[] interfaceClasses = clazz.getInterfaces();194         for (Class<?> interfaceClass : interfaceClasses) {195             if (interfaceClass.isAssignableFrom(List.class)) {196                 return true;197             }198         }199          200         return false;201     }202      203     /**204      * 是否Map的实现类205      */206     private boolean isMap(Class<?> parameterObjectClass) {207         Class<?>[] interfaceClasses = parameterObjectClass.getInterfaces();208         for (Class<?> interfaceClass : interfaceClasses) {209             if (interfaceClass.isAssignableFrom(Map.class)) {210                 return true;211             }212         }213          214         return false;215     }216      217 }
登入後複製

分析一下这段代码(这个是改良过的版本,主要是增加了对select * from xxx where id in 这种写法占位符替换为真正参数的支持)。

首先是注解@Intercepts与@Signature,这两个注解是必须的,因为Plugin的wrap方法会取这两个注解里面参数。@Intercepts中可以定义多个@Signature,一个@Signature表示符合如下条件的方法才会被拦截:

  • 接口必须是type定义的类型

  • 方法名必须和method一致

  • 方法形参的Class类型必须和args定义Class类型顺序一致

接着的一个问题是:有四个接口可以拦截,为什么使用StatementHandler去拦截?根据名字来看ParameterHandler和ResultSetHandler,前者处理参数,后者处理结果是不可能使用的,剩下的就是Executor和StatementHandler了。拦截StatementHandler的原因是而不是用Executor的原因是:

  • Executor的update与query方法可能用到MyBatis的一二级缓存从而导致统计的并不是真正的SQL执行时间

  • StatementHandler的update与query方法无论如何都会统计到PreparedStatement的execute方法执行时间,尽管也有一定误差(误差主要来自会将处理结果的时间也算上),但是相差不大

接着讲一下setProperties方法,可以将一些配置属性配置在的子标签中,所有的配置属性会在形参Properties中,setProperties方法可以拿到配置的属性进行需要的处理。

接着讲一下plugin方法,这里是为目标接口生成代理,不需要也没必要自己去写生成代理的方法,MyBatis的Plugin类已经为我们提供了wrap方法(当然如果自己有自己的逻辑也可以在Plugin.wrap方法前后加入,但是最终一定要使用Plugin.wrap方法生成代理),看一下该方法的实现:

 1 public static Object wrap(Object target, Interceptor interceptor) { 2     Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); 3     Class<?> type = target.getClass(); 4     Class<?>[] interfaces = getAllInterfaces(type, signatureMap); 5     if (interfaces.length > 0) { 6       return Proxy.newProxyInstance( 7           type.getClassLoader(), 8           interfaces, 9           new Plugin(target, interceptor, signatureMap));10     }11     return target;12 }
登入後複製

因为这里的target一定是一个接口,因此可以放心使用JDK本身提供的Proxy类,这里相当于就是如果该接口满足方法签名那么就为之生成一个代理。

最后就是intercept方法了,这里就是拦截器的核心代码了,方法的逻辑我就不解释了,可以自己看一下,唯一要注意的一点就是无论如何最终一定要返回invocation.proceed(),保证拦截器的层层调用。

xml文件配置即效果演示

写完了插件,只需要在config.xml文件中进行一次配置即可,非常简单:

1 <plugins>2       <plugin interceptor="org.xrq.mybatis.plugin.SqlCostInterceptor" />3 </plugins>
登入後複製

这里每个子标签代表一个插件,interceptor表示拦截器的完整路径,每个人的不同。

有了类和这段配置,就可以使用SqlCostInterceptor了,SqlCostInterceptor是通用的,但是每个人的CRUD是不同的,我打印一下我这里CRUD执行的结果:

1 SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "1", "123@sina.com", "个人使用");]执行耗时[1ms]2 SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "2", "123@qq.com", "企业使用");]执行耗时[1ms]3 SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "3", "123@sohu.com", "注册账号使用");]执行耗时[0ms]
登入後複製

看到打印了完整的SQl语句以及SQL语句执行时间。

不过要说明一点,这个插件只是一个简单的Demo,我并没有完整测试过,应该是无法覆盖所有场景的,所以如果想用这段代码片段打印真正的SQL及其执行时间的朋友,还需要在这个基础上做修改,不过即使不改代码,这个插件起到美化SQL的作用,去除一些换行符还是没问题的。

至于MyBatis插件的实现原理,会在我【MyBatis源码分析】系列文章中详细解读,文章地址为【MyBatis源码分析】插件实现原理。

后记

MyBatis插件机制非常有用,用得好可以解决很多问题,不只是这里的打印SQL语句以及记录SQL语句执行时间,分页、分表都可以通过插件来实现。用好插件的关键是我开头就列举的,这里再列一次:

  • Executor(update、query、flushStatements、commint、rollback、getTransaction、close、isClosed)

  • ParameterHandler(getParameterObject、setParameters)

  • ResultSetHandler(handleResultSets、handleOutputParameters)

  • StatementHandler(prepare、parameterize、batch、update、query)

  • 只有理解这四个接口及相关方法是干什么的,才能写出好的拦截器,开发出符合预期的功能。


以上是MyBatis 外掛程式的使用-- 列印 SQL 及執行時間的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1666
14
CakePHP 教程
1425
52
Laravel 教程
1325
25
PHP教程
1273
29
C# 教程
1252
24
excel列印表格框線消失怎麼辦 excel列印表格框線消失怎麼辦 Mar 21, 2024 am 09:50 AM

如果在開啟一份需要列印的文件時,在列印預覽裡我們會發現表格框線不知為何消失不見了,遇到這樣的情況,我們就要及時進行處理,如果你的列印文件裡也出現了此類的問題,那麼就和小編一起來學習下邊的課程吧:excel列印表格框線消失怎麼辦? 1.開啟一份需要列印的文件,如下圖所示。  2、選取所有需要的內容區域,如下圖所示。  3、按滑鼠右鍵,選擇「設定儲存格格式」選項,如下圖所示。  4、點選視窗上方的「邊框」選項,如下圖所示。  5、在左側的線條樣式中選擇細實線圖樣,如下圖所示。  6、選擇“外邊框”

PyCharm新手指南:全面了解外掛程式安裝! PyCharm新手指南:全面了解外掛程式安裝! Feb 25, 2024 pm 11:57 PM

PyCharm是一款功能強大且受歡迎的Python整合開發環境(IDE),提供了豐富的功能和工具,讓開發者可以更有效率地編寫程式碼。而PyCharm的插件機制更是其功能擴充的利器,透過安裝不同的插件,可以為PyCharm增加各種功能和客製化的特性。因此,對於PyCharm新手來說,了解並熟練安裝插件是至關重要的。本文將為你詳細介紹PyCharm插件安裝的全

分享Edge瀏覽器不支援此外掛程式的三種解決方法 分享Edge瀏覽器不支援此外掛程式的三種解決方法 Mar 13, 2024 pm 04:34 PM

  用戶使用Edge瀏覽器的過程中可能會添加一些插件來滿足自己更多的使用需求。但是在添加插件時顯示不支援此插件,這該如何解決?今日小編就來給大家分享三種解決方法,快來試試看。  方法一:嘗試用其他的瀏覽器。  方法二:瀏覽器上的FlashPlayer可能過時或遺失,導致此外掛程式不受支援狀態,可在官網下載最新版本。  方法三:同時按下「Ctrl+Shift+Delete」鍵。  點選“清除資料”,重新開啟瀏覽器即可。

詳解MyBatis動態SQL標籤中的Set標籤功能 詳解MyBatis動態SQL標籤中的Set標籤功能 Feb 26, 2024 pm 07:48 PM

MyBatis動態SQL標籤解讀:Set標籤用法詳解MyBatis是一個優秀的持久層框架,它提供了豐富的動態SQL標籤,可以靈活地建構資料庫操作語句。其中,Set標籤是用來產生UPDATE語句中SET子句的標籤,在更新作業中非常常用。本文將詳細解讀MyBatis中Set標籤的用法,以及透過具體的程式碼範例來示範其功能。什麼是Set標籤Set標籤用於MyBati

Chrome的插件擴充功能安裝目錄是什麼 Chrome的插件擴充功能安裝目錄是什麼 Mar 08, 2024 am 08:55 AM

Chrome的插件擴充功能安裝目錄是什麼?正常情況下,Chrome外掛程式擴充功能的預設安裝目錄如下:1、windowsxp中chrome外掛程式預設安裝目錄位置:C:\DocumentsandSettings\使用者名稱\LocalSettings\ApplicationData\Google\Chrome\UserData\Default\Extensions2、windows7中chrome插件預設安裝目錄位置:C:\Users\使用者名稱\AppData\Local\Google\Chrome\User

MyBatis快取機制詳解:一文讀懂快取儲存原理 MyBatis快取機制詳解:一文讀懂快取儲存原理 Feb 23, 2024 pm 04:09 PM

MyBatis快取機制詳解:一文讀懂快取儲存原理引言在使用MyBatis進行資料庫存取時,快取是一個非常重要的機制,能夠有效減少對資料庫的訪問,提高系統效能。本文將詳細介紹MyBatis的快取機制,包括快取的分類、儲存原理和具體的程式碼範例。一、快取的分類MyBatis的快取主要分為一級快取和二級快取兩種。一級緩存一級緩存是SqlSession級別的緩存,當在

你知道怎麼列印ppt 一頁6張雙面的設定方法嗎 你知道怎麼列印ppt 一頁6張雙面的設定方法嗎 Mar 20, 2024 pm 06:36 PM

我們有時候在使用PPT時常常會需要列印出來。可是,我們都知道PPT中的頁數比較多,如果是一張一張的列印,實在是浪費呢?所以,本人親測試過,一頁紙上放6張PPT頁面,然後雙面列印是很OK的。既不浪費紙,排版內容也看的很清楚。那麼,你知道怎麼印PPT一頁6張雙面呢?接下來,就叫你設定的方法,有興趣的認真看喔!步驟詳情:1、首先,我們在電腦上找到需要列印的PPT,然後雙擊開啟。點選頁面左側上方按鈕旁的倒三角形符號,在下拉式選單中找到【檔案】的按鈕,點選一下;接著,在出現的訊息中點選【列印】。 2、點擊

MyBatis Generator配置參數解讀及最佳實踐 MyBatis Generator配置參數解讀及最佳實踐 Feb 23, 2024 am 09:51 AM

MyBatisGenerator是MyBatis官方提供的程式碼產生工具,可以幫助開發人員快速產生符合資料庫表結構的JavaBean、Mapper介面以及XML映射檔。在使用MyBatisGenerator進行程式碼產生的過程中,配置參數的設定是至關重要的。本文將從配置參數的角度出發,深入探討MyBatisGenerator的

See all articles