《 一生所愛》
從前現在過去了再不來
紅紅落葉長埋塵土內
開始終結總是沒變改
天邊的你飄泊白云外
苦海翻起愛恨
在世間難逃避命運
相親竟不可接近
或我應該相信是緣份
情人別后永遠再不來(消散的情緣)
無言獨坐放眼塵世外(愿來日再續)
鮮花雖會凋謝(只愿)
但會再開(為你)
一生所愛 隱約(守候)
在白云外(期待)
苦海翻起愛恨
在世間難逃避命運
相親竟不可接近
或我應該相信是緣份
苦海翻起愛恨
在世間難逃避命運
相親竟不可接近
或我應該相信是緣份
前言
這是我們分析 mybatis 的第四篇文章,看標題,我們是分析 mybatis 的插件,其實,在前面的三篇文章中,我們已經在剖析源碼的時候多多少少接觸到 mybatis 的插件設計和運行過程了,只是沒有單獨的開一篇文章來講這個,mybatis 的日志系統就是基于插件的。這個在我們之前的源碼剖析里也說過。插件在整個mybatis 中只占很小的一部分,mybatis 不像 Spring ,留了很多的接口給使用者擴展,只留了一個接口給開發者擴展。究其原因還是兩者的目標和工作不同。有了之前三篇文章的基礎,我們今天研究 mybatis 的插件,基本就是一個復習的過程,整體上還是比較輕松的。那么,接下來我們就看看吧!
我們將分為 2 個部分來講述,一個是插件原理,一個是如何應用插件接口并且對比國內流行的插件。
1.插件原理
我們在剖析 mybatis 的時候,就已經發現了 mybatis 的插件在他自己框架身上的應用,我們回顧一下在哪里出現的:
從上面的截圖,可以看到,在mybatis 4大對象的創建過程中,都調用了 interceptorChain.pluginAll 方法,可見該方法的重要性,那么該方法的作用是上面呢?我們首先猜測一下,從該方法的名字可以看出,該方法是攔截器鏈調用插件方法,并傳入了一個對象,最后返回了一個該對象,那么,我們看看該方法是如何實現的:
該類可以說是非常的簡單,所謂大道無形,該類是 mybatis 插件核心,首先有一個插件集合,一個 pluginAll 方法,一個 addInterceptor 方法, 一個getInterceptors 方法,可以看的出來該類就是一個過濾器鏈,類似tomcat 的過濾器和Spring的AOP,我們主要看兩個方法,一個是 pluginAll,一個是 addInterceptor 方法,我們首先看看 addInterceptor 方法,也即使添加過濾器,什么時候添加呢?我們看看該方法的調用棧:
可以看到,從我們的main方法開始,調用了 SqlSessionFactoryBuilder.build 方法,再調用了 XMLConfigBuilder 的 parse 方法,該方法又調用了自身的 parseConfiguration 方法,在 parseConfiguration 方法中調用了 pluginElement 解析 “plugins” 屬性,在該方法中調用了 configuration.addInterceptor 方法,該方法又調用了 interceptorChain.addInterceptor 方法,將插件添加進該集合。也就是說,該方法是在解析XML配置文件的時候調用的,將配置好的插件添加進集合中,以便之后的調用。
那么 pluginAll 方法是什么時候運行的呢?我們同樣看看他的方法調用棧:
我們在方法調用棧圖上看到的最后一層調用了 openSession 方法,也就是我們 sqlSessionFactory.openSession() 方法生成 SqlSession 的時候,該方法會調用 自身的
openSessionFromDataSource 方法,然后調用 configuration.newExecutor 方法插件 Executor,在 newExecutor 方法中,我們上面的圖上也有,調用了 executor = (Executor) interceptorChain.pluginAll(executor) 方法,返回了一個 executor,很顯然,這個對象肯定被處理過了。這里我們只說了 executor 對象,4大對象的其余三個對象也是這么生成的,我們就不一一講了,有興趣的同學可以翻看源碼。
那么,我們就要看看該方法到底是如何實現的,讓 mybatis 的 4 大對象都要調用該方法。
該方法循環了所有的攔截器,并調用了攔截器的 plugin 方法,每次都講返回的 target 對象作為參數作為下一次調用。那么 plugin 方法的內容是什么呢?Interceptor 是個接口,在mybatis 源碼中,只有2個實現類,我們找其中一個 ExamplePlugin 實現類看看源碼實現:
該類實現了 Interceptor 接口,并重寫了3個方法,其中就有我們關注的 plugin 方法,該方法內部很簡單的調用了 Plugin.wrap(target, this) 方法,參數是 目標對象和自身,返回了和目標對象,我們該方法內部是如何實現的呢?
樓主只截取了一部分方法,該類實現類 JDK 動態代理中一個重要的接口 InvocationHandler 接口,而 wrap 方法是一個靜態方法,通過傳入的攔截器和目標對象,生成一個動態代理返回,注意,目標對象一定要實現某個接口,否則返回自身,我們看看代碼實現。
- 調用自身的 getSignatureMap 方法,該方法獲取了 Intercepts 注解上的 key 是 攔截的類型,value 是攔截的方法(多個)數據。并將數據包裝成map返回。
- 獲取目標對象的接口,并講接口放進一個Set中并轉成Class 數組返回。
- 根據上面生成的參數map,攔截器,目標對象,生成一個 puugin對象。
- 將生成 plugin 對象和接口和類加載器創建一個動態代理對象返回。
好了,我們知道了 plugin 方法的作用,也就是說,4 大對象都會調用該方法,都會將這些攔截器把自己包裝起來,最后攔截自己。完成切面工作,比如日志。
那么,既然是實現類 JDK 的 InvocationHandler 接口,那么我們就要看看他的invoke 方法是怎么實現的:
該方法首先從剛剛從攔截器類 Intercepts 注解上取出的參數map中以目標方法的類作為key取出對應的方法集合,如果 invoke 方法和注解上定義的方法匹配,就執行攔截器的 intercept 方法,注意,此時,會創建一個Invocation 對象作為參數傳遞到 intercept 方法中,而這個對象的創建的參數包括 目標對象,代理攔截的方法,代理的參數。
我們回到 mybatis 中的攔截器例子 ExamplePlugin 類中看看 intercept 方法是如何實現的:
該方法只是調用了 invocation 的proceed 方法,那么該方法是如何定義的呢?
該方法只是用反射調用剛剛構造函數中的方法。并沒有執行任何的操作。也就是說,在 Plugin 中的 invoke 方法中,調用了攔截器的 intercept 方法,并傳入了 Invocation 對象,該對象的作用就是將目標對象,目標方法,目標方法參數傳入,讓攔截器可以取出這些參數并做加強工作。注意,需要在執行完加強操作和執行 Invocation 的 proceed 方法。也就是執行目標對象真正的方法。
到這里,我們已經弄懂了 mybatis 的攔截器原理,首先攔截器攔截的是 mybatis 的 4 大對象,我們需要在配置文件中配置攔截器,方便mybaits 添加到攔截器鏈中。mybatis 為我們提供了 Interceptor 接口,我們可以在該接口中實現自己的邏輯,主要需要實現 intercept 方法,在該方法中利用給定的 Invocation 對象來對我們的業務做一些增強。而調用攔截器方法的類就是 JDK 動態代理的接口 InvocationHandler 的實現類 Plugin 的invoke 方法,該方法會根據目標方法是否匹配攔截器注解的值來決定是否調用攔截器的 intercept 方法。并傳入封裝了目標對象,目標方法,目標方法參數的 Invocation 實例。
知道了攔截器的實現原理,那么我們就寫一個例子來體驗一下。
2. 攔截器的應用
首先編寫 mybatis 插件需要遵守幾個約定:
- 實現 Interceptor 接口并實現接口中的方法。
- 在配置文件中配置插件。
-
在實現 Interceptor 接口的類上加上 @Intercepts 注解。該注解如下:
僅有一個 Signature 注解集合,我們看看Signature 注解有哪些內容:
該注解有3個方法,分別代表著攔截的類型,攔截的哪個方法,攔截的方法的參數(因為可能是重載方法)。也就是說,這是一個方簽名注解。
那么我們能攔截哪些類呢?我們前面說,執行 SQL 的是mybatis 4大對象,并且這4大對象也都會調用過濾器鏈,那么他們的調用過程是怎么樣的呢?我們看看他們的方法調用棧:
最上面的是 BaseStatementHandler 抽象類的構造方法,實現類則是PreparedStatementHandler,在該構造器中,會創建2個包含了插件的 parameterHandler 對象和 resultSetHandler 對象。那么這個方法是什么時候調用的呢?實際上,newExecutor 方法,也就是創建 Executor 代理的方法是第一個創建的,然后再執行 doQuery 方法的時候,會創建 StatementHandler 對象,而再創建 StatementHandler 對象的時候,會創建另外 2 個對象 parameterHandler 和 resultSetHandler。由此完成 4 大對象的代理創建。那么 4 大對象的創建調用是什么順序呢?樓主寫了一個例子:
樓主攔截了 4 大對象個一個方法,也就是說,只要執行這 4 個方法都會進入 intercept 方法,都會答應該對象的引用。我們看看運行結果:
可以看到順序,首先執行了 executor 的方法,然后執行了 StatementHandler 的攔截方法, 再執行 ParameterHandler 的方法,再執行 ResultSetHandler 的攔截器,最后執行 executor 真正的查詢方法。
知道了這個順序,對我們開發插件是有幫助的。
看著這里,我們應該有個了解了,我們使用插件的目的大部分都是再運行SQL的時候修改SQL,比如分頁,比如分表,再原有的SQL上做一些修改,那么怎么才能修改呢?重點就在 MappedStatement 的 sqlSource 屬性,該接口的實現類會存儲SQL語句,比如其中一個實現類 :StaticSqlSource,我們看看該類的構造:
其中有一個重要的字段 : sql, 該字段就是存儲 SQL 語句的字符串,那么我們的任務就是修改這個字段,從而達到我們自定義 SQL 的目的。
既然知道了怎么使用插件,那么我們就來寫一個看看:
首先實現攔截器接口:
我們攔截了 StatementHandler 類的 prepare 方法,理論上,我們如果想修改 sql,可以攔截 Executor 和 StatementHandler 都可以。
我們看看 plugin 方法,該方法使用了 mybatis 的 Plugin 的 wrap 方法,基本就是官方默認的寫法,沒什么可修改的。而 setProperties 方法呢?就是可以在配置文件中配置一些參數,可以在運行的時候獲取配置文件的參數。最重要的而是 intercept 方法,該方法步驟如下:
- 獲取Invocation 的目標對象,因為我們攔截的是 StatementHandler 對象,那么就可以強轉成這個對象,如果你攔截了2個對象,就需要進行判斷。
- 打印該對象的 sql 語句。
- 使用反射修改sql。
- 打印修改后的sql 語句。然后運行。
我們看看執行結果:
從結果中可以看到,我們攔截成功,并且成功執行了 sql 語句,返回了空值。如果不攔截,將返回正常的值。
返回了正常結果。
到這里,我們已經知道如何使用mybatis 的插件,雖然這個例子非常的簡單,但市面的分頁插件基本都是這樣設計的。都是通過修改 BoundSql 這個對象來修改Sql,有的可能只修改了這個對象的 Sql 字段,有的直接重新創建一個對象。比如 PageHelper 插件。我們看看該類的關鍵源碼:
@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 {
//緩存count查詢的ms
protected Cache<String, MappedStatement> msCountMap = null;
private Dialect dialect;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
private Field additionalParametersField;
private String countSuffix = "_COUNT";
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于邏輯關系,只會進入一次
if(args.length == 4){
//4 個參數時
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個參數時
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
List resultList;
//調用方法判斷是否需要進行分頁,如果不需要,直接返回結果
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射獲取動態參數
String msId = ms.getId();
Configuration configuration = ms.getConfiguration();
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//判斷是否需要進行 count 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
String countMsId = msId + countSuffix;
Long count;
//先判斷是否存在手寫的 count 查詢
MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
if(countMs != null){
count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自動創建
if (countMs == null) {
//根據當前的 ms 創建一個返回值為 Long 類型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
//處理查詢總數
//返回 true 時繼續分頁查詢,false 時直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當查詢總數為 0 時,直接返回空的結果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//判斷是否需要進行分頁查詢
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分頁的緩存 key
CacheKey pageKey = cacheKey;
//處理參數對象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//調用方言獲取分頁 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
//設置動態參數
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//執行分頁查詢
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不執行分頁的情況下,也不執行內存分頁
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
} else {
//rowBounds用參數值,不使用分頁插件處理時,仍然支持默認的內存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
該類是國內著名插件 PageHelper 的攔截器。該攔截器攔截了 Executor 的兩個重載方法,在 intercept 方法內部,會從 Invocation 對象中取出參數,目標對象,最終會創建一個 pageBoundSql 的 BoundSql 對象,執行 executor 的 query 方法。那么分頁參數放在哪里的呢?放在了 PageHelper 的 ThreadLocal 變量中。然后到這個方法中取出該變量。傳入 sql 語句中。最后執行。
還有一個注意的地方,就是我們之前的簡單 demo 里,只是使用了反射來修改 sql 語句,mybatis 中有一個反射的工具類:MetaObject,他可以快捷的修改某個類的屬性,底層是通過反射,而且支持 OGNL 表達式,非常的強大。我們將我們的例子修改一下:
查看運行結果:
使用 mybatis 的工具類 MetaObject ,使用 OGNL 表達式,修改SQL成功。返回了空值。
總結
我們分析了 mybatis 中常用的插件,知道了他的原理,就是每次創建4大對象的時候,都會將場景封裝到對象中,如果有多個,就層層包裝。這個是通過動態代理的技術實現的。然后在運行的時候會調用實現了動態代理 InvocationHandler 接口的 Plugin 類的 invoke 方法,而該方法會調用攔截器器的 intercept 方法,并傳入封裝了目標對象,目標方法,目標方法參數的 Invocation 供使用者修改或加強。
修改 Sql 有多種方式,最終都是修改 StatementHandler 的 BoundSql 中的 sql 字段,無論是直接修改屬性,還是重新創建一個 BoundSql 對象。還有一個 mybatis 的 MetaObject 類,該類是 mybatis 提供的一個強大的通過反射修改對象屬性的工具類,mybatis 中多次使用該類。
在我們的項目中,通過 mybatis 的攔截器可以實現很多功能,比如分頁插件,再比如 分表插件,因為如果一張表中數據過大,會拆分為多個表,這個時候可以通過一些特定的參數,將表的后綴加上去,起到自動分表的效果。而 XML 中的 SQL 是感知不到的。
總之,mybatis 插件可以實現很多功能。但使用他的時候請一定小心,畢竟這修改了 mybatis 底層的邏輯。
good luck!!!!