ホームページ Java &#&チュートリアル MyBatisプラグインの詳細な紹介

MyBatisプラグインの詳細な紹介

Jun 25, 2017 pm 01:37 PM
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执行的结果:

<span style="color: #000000">SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "1", "123@sina.com", "个人使用");]执行耗时[1ms]
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "2", "123@qq.com", "企业使用");]执行耗时[1ms]
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "3", "123@sohu.com", "注册账号使用");]执行耗时[0ms]<br></span>
ログイン後にコピー

完全な SQL ステートメントと SQL ステートメントの実行時間が出力されるのを確認してください。

ただし、このプラグインは単なる単純なデモであり、すべてのシナリオを完全にはカバーできないことに注意してください。そのため、このコード スニペットを使用して、実際の SQL とその実行時間は、これに基づいて修正を加える必要がありますが、コードが変更されていなくても、このプラグインは SQL を美しくする役割を果たしており、一部の改行を削除することは問題ありません。

MyBatisプラグインの実装原理については、私の[MyBatisソースコード解析]シリーズ記事で詳しく説明します。記事アドレスは[MyBatisソースコード解析]プラグイン実装原理です。

追記

MyBatis プラグインの仕組みは、うまく使えば、SQL ステートメントの出力や SQL ステートメントの実行時間の記録だけでなく、ページングやテーブル分割など、多くの問題を解決できます。プラグインを介して実行することもできます。プラグインを上手に使用するための鍵は、最初にリストしたものです。これをもう一度示します。 getParameterObject, setParameters)

    ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (準備、パラメータ化、バッチ、更新、クエリ)
  • これら 4 つのインターフェイスと関連メソッドの機能を理解することによってのみ、良いものを書くことができますインターセプター 、期待に応える機能を開発します。

以上がMyBatisプラグインの詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

PyCharm 初心者ガイド: プラグインのインストールを徹底理解! PyCharm 初心者ガイド: プラグインのインストールを徹底理解! Feb 25, 2024 pm 11:57 PM

PyCharm は、開発者がより効率的にコードを記述できるようにする豊富な機能とツールを提供する、強力で人気のある Python 統合開発環境 (IDE) です。 PyCharm のプラグイン機構は、機能を拡張するための強力なツールであり、さまざまなプラグインをインストールすることで、PyCharm にさまざまな機能やカスタマイズ機能を追加できます。したがって、PyCharm の初心者にとって、プラグインのインストールを理解し、習熟することが重要です。この記事では、PyCharm プラグインの完全なインストールについて詳しく説明します。

Chrome プラグイン拡張機能のインストール ディレクトリとは何ですか? Chrome プラグイン拡張機能のインストール ディレクトリとは何ですか? Mar 08, 2024 am 08:55 AM

Chrome プラグイン拡張機能のインストール ディレクトリとは何ですか?通常の状況では、Chrome プラグイン拡張機能のデフォルトのインストール ディレクトリは次のとおりです。 1. WindowsXP での Chrome プラグインのデフォルトのインストール ディレクトリの場所: C:\DocumentsandSettings\username\LocalSettings\ApplicationData\Google\Chrome\UserData\ Default\Extensions2. Windows7 の chrome プラグインのデフォルトのインストール ディレクトリの場所: C:\Users\username\AppData\Local\Google\Chrome\User

MyBatis動的SQLタグのSetタグ機能の詳細説明 MyBatis動的SQLタグのSetタグ機能の詳細説明 Feb 26, 2024 pm 07:48 PM

MyBatis 動的 SQL タグの解釈: Set タグの使用法の詳細な説明 MyBatis は、豊富な動的 SQL タグを提供し、データベース操作ステートメントを柔軟に構築できる優れた永続層フレームワークです。このうち、Set タグは、UPDATE ステートメントで SET 句を生成するために使用され、更新操作でよく使用されます。この記事では、MyBatis での Set タグの使用法を詳細に説明し、特定のコード例を通じてその機能を示します。 SetタグとはMyBatiで使用するSetタグです。

Edge ブラウザがこのプラグインをサポートしない理由に対する 3 つの解決策を共有します Edge ブラウザがこのプラグインをサポートしない理由に対する 3 つの解決策を共有します Mar 13, 2024 pm 04:34 PM

ユーザーが Edge ブラウザを使用する場合、より多くのニーズを満たすためにいくつかのプラグインを追加する場合があります。しかし、プラグインを追加すると、このプラグインはサポートされていないと表示されます。この問題を解決するにはどうすればよいですか?今日は編集者が 3 つの解決策を紹介しますので、ぜひ試してみてください。方法 1: 別のブラウザを使用してみてください。方法 2: ブラウザ上の Flash Player が古いか見つからないため、プラグインがサポートされていない可能性があり、公式 Web サイトから最新バージョンをダウンロードできます。方法3:「Ctrl+Shift+Delete」キーを同時に押します。 「データを消去」をクリックしてブラウザを再度開きます。

Go 言語のインデントの仕様と例 Go 言語のインデントの仕様と例 Mar 22, 2024 pm 09:33 PM

Go 言語のインデント仕様と例 Go 言語は Google によって開発されたプログラミング言語であり、その簡潔で明確な構文で知られており、インデント仕様はコードの読みやすさと美しさに重要な役割を果たします。この記事ではGo言語のインデントの仕様を紹介し、具体的なコード例を通して詳しく解説します。インデントの仕様 Go 言語では、スペースの代わりにタブがインデントに使用されます。インデントの各レベルは 1 つのタブで、通常はスペース 4 個の幅に設定されます。このような仕様により、コーディング スタイルが統一され、チームが協力してコンパイルできるようになります。

Oracle DECODE関数の詳細説明と使用例 Oracle DECODE関数の詳細説明と使用例 Mar 08, 2024 pm 03:51 PM

Oracle の DECODE 関数は、クエリ ステートメントのさまざまな条件に基づいてさまざまな結果を返すためによく使用される条件式です。この記事ではDECODE関数の構文・使い方・サンプルコードを詳しく紹介します。 1. DECODE 関数の構文 DECODE(expr,search1,result1[,search2,result2,...,default]) expr: 比較する式またはフィールド。検索1、

MyBatis 1次キャッシュの詳細解説:データアクセス効率を高めるには? MyBatis 1次キャッシュの詳細解説:データアクセス効率を高めるには? Feb 23, 2024 pm 08:13 PM

MyBatis 1次キャッシュの詳細解説:データアクセス効率を高めるには?開発プロセス中、効率的なデータ アクセスは常にプログラマーの焦点の 1 つでした。 MyBatis のような永続層フレームワークの場合、キャッシュはデータ アクセス効率を向上させるための重要な方法の 1 つです。 MyBatis は、一次キャッシュと二次キャッシュという 2 つのキャッシュ メカニズムを提供しており、一次キャッシュはデフォルトで有効になっています。この記事では、MyBatis の 1 次キャッシュのメカニズムを詳細に紹介し、読者の理解を深めるために具体的なコード例を示します。

MyBatisのキャッシュ機構を解析:一次キャッシュと二次キャッシュの特徴と使い方を比較 MyBatisのキャッシュ機構を解析:一次キャッシュと二次キャッシュの特徴と使い方を比較 Feb 25, 2024 pm 12:30 PM

MyBatis のキャッシュ メカニズムの分析: 1 次キャッシュと 2 次キャッシュの違いと応用 MyBatis フレームワークでは、キャッシュはデータベース操作のパフォーマンスを効果的に向上させることができる非常に重要な機能です。そのうち、一次キャッシュと二次キャッシュは、MyBatis でよく使用される 2 つのキャッシュ メカニズムです。この記事では、一次キャッシュと二次キャッシュの違いと用途を詳細に分析し、具体的なコード例を示して説明します。 1. レベル 1 キャッシュ レベル 1 キャッシュはローカル キャッシュとも呼ばれ、デフォルトで有効になっており、オフにすることはできません。 1次キャッシュはSqlSesです

See all articles