后端技术:mybatis插件原理详解
關注“Java后端技術全?!?br />
回復“面試”獲取全套面試資料
上次發文說到了如何集成分頁插件MyBatis插件原理分析,看完感覺自己better了,今天我們接著來聊mybatis插件的原理。
插件原理分析
mybatis插件涉及到的幾個類:
我將以 Executor 為例,分析 MyBatis 是如何為 Executor 實例植入插件的。Executor 實例是在開啟 SqlSession 時被創建的,因此,我們從源頭進行分析。先來看一下 SqlSession 開啟的過程。
public?SqlSession?openSession()?{return?openSessionFromDataSource(configuration.getDefaultExecutorType(),?null,?false); }private?SqlSession?openSessionFromDataSource(ExecutorType?execType,?TransactionIsolationLevel?level,?boolean?autoCommit)?{Transaction?tx?=?null;try?{//?省略部分邏輯//?創建?Executorfinal?Executor?executor?=?configuration.newExecutor(tx,?execType);return?new?DefaultSqlSession(configuration,?executor,?autoCommit);}?catch?(Exception?e)?{...}?finally?{...} }Executor 的創建過程封裝在 Configuration 中,我們跟進去看看看。
//?Configuration類中 public?Executor?newExecutor(Transaction?transaction,?ExecutorType?executorType)?{executorType?=?executorType?==?null???defaultExecutorType?:?executorType;executorType?=?executorType?==?null???ExecutorType.SIMPLE?:?executorType;Executor?executor;//?根據?executorType?創建相應的?Executor?實例if?(ExecutorType.BATCH?==?executorType)?{...}?else?if?(ExecutorType.REUSE?==?executorType)?{...}?else?{executor?=?new?SimpleExecutor(this,?transaction);}if?(cacheEnabled)?{executor?=?new?CachingExecutor(executor);}//?植入插件executor?=?(Executor)?interceptorChain.pluginAll(executor);return?executor; }如上,newExecutor 方法在創建好 Executor 實例后,緊接著通過攔截器鏈 interceptorChain 為 Executor 實例植入代理邏輯。那下面我們看一下 InterceptorChain 的代碼是怎樣的。
public?class?InterceptorChain?{private?final?List<Interceptor>?interceptors?=?new?ArrayList<Interceptor>();public?Object?pluginAll(Object?target)?{//?遍歷攔截器集合for?(Interceptor?interceptor?:?interceptors)?{//?調用攔截器的?plugin?方法植入相應的插件邏輯target?=?interceptor.plugin(target);}return?target;}/**?添加插件實例到?interceptors?集合中?*/public?void?addInterceptor(Interceptor?interceptor)?{interceptors.add(interceptor);}/**?獲取插件列表?*/public?List<Interceptor>?getInterceptors()?{return?Collections.unmodifiableList(interceptors);} }上面的for循環代表了只要是插件,都會以責任鏈的方式逐一執行(別指望它能跳過某個節點),所謂插件,其實就類似于攔截器。
這里就用到了責任鏈設計模式,責任鏈設計模式就相當于我們在OA系統里發起審批,領導們一層一層進行審批。
以上是 InterceptorChain 的全部代碼,比較簡單。它的 pluginAll 方法會調用具體插件的 plugin 方法植入相應的插件邏輯。如果有多個插件,則會多次調用 plugin 方法,最終生成一個層層嵌套的代理類。形如下面:
當 Executor 的某個方法被調用的時候,插件邏輯會先行執行。執行順序由外而內,比如上圖的執行順序為 plugin3 → plugin2 → Plugin1 → Executor。
plugin 方法是由具體的插件類實現,不過該方法代碼一般比較固定,所以下面找個示例分析一下。
//?TianPlugin類 public?Object?plugin(Object?target)?{return?Plugin.wrap(target,?this); }//Plugin public?static?Object?wrap(Object?target,?Interceptor?interceptor)?{/**?獲取插件類?@Signature 注解內容,并生成相應的映射結構。形如下面:*?{*?????Executor.class?:?[query,?update,?commit],*?????ParameterHandler.class?:?[getParameterObject,?setParameters]*?}*/Map<Class<?>,?Set<Method>>?signatureMap?=?getSignatureMap(interceptor);Class<?>?type?=?target.getClass();//?獲取目標類實現的接口Class<?>[]?interfaces?=?getAllInterfaces(type,?signatureMap);if?(interfaces.length?>?0)?{//?通過?JDK?動態代理為目標類生成代理類return?Proxy.newProxyInstance(type.getClassLoader(),interfaces,new?Plugin(target,?interceptor,?signatureMap));}return?target; }如上,plugin 方法在內部調用了 Plugin 類的 wrap 方法,用于為目標對象生成代理。Plugin 類實現了 InvocationHandler 接口,因此它可以作為參數傳給 Proxy 的 newProxyInstance 方法。
到這里,關于插件植入的邏輯就分析完了。接下來,我們來看看插件邏輯是怎樣執行的。
執行插件邏輯
Plugin 實現了 InvocationHandler 接口,因此它的 invoke 方法會攔截所有的方法調用。invoke 方法會對所攔截的方法進行檢測,以決定是否執行插件邏輯。該方法的邏輯如下:
//在Plugin類中 public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{try?{/**?獲取被攔截方法列表,比如:*????signatureMap.get(Executor.class),可能返回?[query,?update,?commit]*/Set<Method>?methods?=?signatureMap.get(method.getDeclaringClass());//?檢測方法列表是否包含被攔截的方法if?(methods?!=?null?&&?methods.contains(method))?{//?執行插件邏輯return?interceptor.intercept(new?Invocation(target,?method,?args));}//?執行被攔截的方法return?method.invoke(target,?args);}?catch?(Exception?e)?{throw?ExceptionUtil.unwrapThrowable(e);} }invoke 方法的代碼比較少,邏輯不難理解。首先,invoke 方法會檢測被攔截方法是否配置在插件的 @Signature 注解中,若是,則執行插件邏輯,否則執行被攔截方法。插件邏輯封裝在 intercept 中,該方法的參數類型為 Invocation。Invocation 主要用于存儲目標類,方法以及方法參數列表。下面簡單看一下該類的定義。
public?class?Invocation?{private?final?Object?target;private?final?Method?method;private?final?Object[]?args;public?Invocation(Object?target,?Method?method,?Object[]?args)?{this.target?=?target;this.method?=?method;this.args?=?args;}//?省略部分代碼public?Object?proceed()?throws?InvocationTargetException,?IllegalAccessException?{//反射調用被攔截的方法return?method.invoke(target,?args);} }關于插件的執行邏輯就分析到這,整個過程不難理解,大家簡單看看即可。
自定義插件
下面為了讓大家更好的理解Mybatis的插件機制,我們來模擬一個慢sql監控的插件。
/***?慢查詢sql?插件*/ @Intercepts({@Signature(type?=?StatementHandler.class,?method?=?"prepare",?args?=?{Connection.class,?Integer.class})}) public?class?SlowSqlPlugin?implements?Interceptor?{private?long?slowTime;//攔截后需要處理的業務@Overridepublic?Object?intercept(Invocation?invocation)?throws?Throwable?{//通過StatementHandler獲取執行的sqlStatementHandler?statementHandler?=?(StatementHandler)?invocation.getTarget();BoundSql?boundSql?=?statementHandler.getBoundSql();String?sql?=?boundSql.getSql();long?start?=?System.currentTimeMillis();//結束攔截Object?proceed?=?invocation.proceed();long?end?=?System.currentTimeMillis();long?f?=?end?-?start;System.out.println(sql);System.out.println("耗時="?+?f);if?(f?>?slowTime)?{System.out.println("本次數據庫操作是慢查詢,sql是:");System.out.println(sql);}return?proceed;}//獲取到攔截的對象,底層也是通過代理實現的,實際上是拿到一個目標代理對象@Overridepublic?Object?plugin(Object?target)?{//觸發intercept方法return?Plugin.wrap(target,?this);}//設置屬性@Overridepublic?void?setProperties(Properties?properties)?{//獲取我們定義的慢sql的時間閾值slowTimethis.slowTime?=?Long.parseLong(properties.getProperty("slowTime"));} }然后把這個插件類注入到容器中。
然后我們來執行查詢的方法。
耗時28秒的,大于我們定義的10毫秒,那這條SQL就是我們認為的慢SQL。
通過這個插件,我們就能很輕松的理解setProperties()方法是做什么的了。
回顧分頁插件
也是實現mybatis接口Interceptor。
@SuppressWarnings({"rawtypes",?"unchecked"}) @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}),} ) public?class?PageInterceptor?implements?Interceptor?{@Overridepublic?Object?intercept(Invocation?invocation)?throws?Throwable?{...}intercept方法中
//AbstractHelperDialect類中 @Override public?String?getPageSql(MappedStatement?ms,?BoundSql?boundSql,?Object?parameterObject,?RowBounds?rowBounds,?CacheKey?pageKey)?{String?sql?=?boundSql.getSql();Page?page?=?getLocalPage();//支持?order?byString?orderBy?=?page.getOrderBy();if?(StringUtil.isNotEmpty(orderBy))?{pageKey.update(orderBy);sql?=?OrderByParser.converToOrderBySql(sql,?orderBy);}if?(page.isOrderByOnly())?{return?sql;}//獲取分頁sqlreturn?getPageSql(sql,?page,?pageKey);} //模板方法模式中的鉤子方法public?abstract?String?getPageSql(String?sql,?Page?page,?CacheKey?pageKey);AbstractHelperDialect類的實現類有如下(也就是此分頁插件支持的數據庫就以下幾種):
我們用的是MySQL。這里也有與之對應的。
????@Overridepublic?String?getPageSql(String?sql,?Page?page,?CacheKey?pageKey)?{StringBuilder?sqlBuilder?=?new?StringBuilder(sql.length()?+?14);sqlBuilder.append(sql);if?(page.getStartRow()?==?0)?{sqlBuilder.append("?LIMIT???");}?else?{sqlBuilder.append("?LIMIT??,???");}pageKey.update(page.getPageSize());return?sqlBuilder.toString();}到這里我們就知道了,它無非就是在我們執行的SQL上再拼接了Limit罷了。同理,Oracle也就是使用rownum來處理分頁了。下面是Oracle處理分頁
????@Overridepublic?String?getPageSql(String?sql,?Page?page,?CacheKey?pageKey)?{StringBuilder?sqlBuilder?=?new?StringBuilder(sql.length()?+?120);if?(page.getStartRow()?>?0)?{sqlBuilder.append("SELECT?*?FROM?(?");}if?(page.getEndRow()?>?0)?{sqlBuilder.append("?SELECT?TMP_PAGE.*,?ROWNUM?ROW_ID?FROM?(?");}sqlBuilder.append(sql);if?(page.getEndRow()?>?0)?{sqlBuilder.append("?)?TMP_PAGE?WHERE?ROWNUM?<=???");}if?(page.getStartRow()?>?0)?{sqlBuilder.append("?)?WHERE?ROW_ID?>???");}return?sqlBuilder.toString();}其他數據庫分頁操作類似。關于具體原理分析,這里就沒必要贅述了,因為分頁插件源代碼里注釋基本上全是中文。
Mybatis插件應用場景
水平分表
權限控制
數據的加解密
總結
Spring-Boot+Mybatis繼承了分頁插件,以及使用案例、插件的原理分析、源碼分析、如何自定義插件。
涉及到技術點:JDK動態代理、責任鏈設計模式、模板方法模式。
Mybatis插件關鍵對象總結:
Inteceptor接口:自定義攔截必須實現的類。
InterceptorChain:存放插件的容器。
Plugin:h對象,提供創建代理類的方法。
Invocation:對被代理對象的封裝。
IT技術分享社區
個人博客網站:https://programmerblog.xyz
文章推薦程序員效率:畫流程圖常用的工具程序員效率:整理常用的在線筆記軟件遠程辦公:常用的遠程協助軟件,你都知道嗎?51單片機程序下載、ISP及串口基礎知識硬件:斷路器、接觸器、繼電器基礎知識
總結
以上是生活随笔為你收集整理的后端技术:mybatis插件原理详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ligergrid 奇偶行效果_怎么护发
- 下一篇: 项目测试基础:黑盒测试相关知识笔记