深入理解Mybatis外掛開發
背景
關於Mybatis外掛,大部分人都知道,也都使用過,但很多時候,我們僅僅是停留在表面上,知道Mybatis外掛可以在DAO層進行攔截,如列印執行的SQL語句日誌,做一些許可權控制,分頁等功能;但對其內部實現機制,涉及的軟體設計模式,程式設計思想往往沒有深入的理解。
本篇案例將幫助讀者對Mybatis外掛的使用場景,實現機制,以及其中涉及的程式設計思想進行一個小結,希望對以後的程式設計開發工作有所幫助。
注:本案例以mybatis 3.4.7-SNAPSHOT版本為例。
PS:文章是挺久之前寫的,當時花了一些心思,存到電腦的word裡,今天正好看到,就是裡面的原始碼都是圖片,哈哈哈,湊合著看吧。
Mybatis外掛典型適用場景
分頁功能
mybatis的分頁預設是基於記憶體分頁的(查出所有,再擷取),資料量大的情況下效率較低,不過使用mybatis外掛可以改變該行為,只需要攔截StatementHandler類的prepare方法,改變要執行的SQL語句為分頁語句即可;
公共欄位統一賦值
一般業務系統都會有建立者,建立時間,修改者,修改時間四個欄位,對於這四個欄位的賦值,實際上可以在DAO層統一攔截處理,可以用mybatis外掛攔截Executor類的update方法,對相關引數進行統一賦值即可;
效能監控
對於SQL語句執行的效能監控,可以通過攔截Executor類的update, query等方法,用日誌記錄每個方法執行的時間;
其它
其實mybatis擴充套件性還是很強的,基於外掛機制,基本上可以控制SQL執行的各個階段,如執行階段,引數處理階段,語法構建階段,結果集處理階段,具體可以根據專案業務來實現對應業務邏輯。
Mybatis外掛介紹
什麼是Mybatis外掛
與其稱為Mybatis外掛,不如叫Mybatis攔截器,更加符合其功能定位,實際上它就是一個攔截器,應用代理模式,在方法級別上進行攔截。
支援攔截的方法
- 執行器Executor(update、query、commit、rollback等方法);
- 引數處理器ParameterHandler(getParameterObject、setParameters方法);
- 結果集處理器ResultSetHandler(handleResultSets、handleOutputParameters等方法);
- SQL語法構建器StatementHandler(prepare、parameterize、batch、update、query等方法);
攔截階段
那麼這些類上的方法都是在什麼階段被攔截的呢?為理解這個問題,我們先看段簡單的程式碼(摘自mybatis原始碼中的單元測試SqlSessionTest類),來了解下典型的mybatis執行流程,如下程式碼所示:
以上程式碼主要完成以下功能:
- 讀取mybatis的xml配置檔案資訊
- 通過SqlSessionFactoryBuilder建立SqlSessionFactory物件
- 通過SqlSessionFactory獲取SqlSession物件
- 執行SqlSession物件的selectList方法,查詢結果
- 關閉SqlSession
如下是時序圖,在整個時序圖中,涉及到mybatis外掛部分已標紅,基本上就是體現在上文中提到的四個類上,對這些類上的方法進行攔截。
Mybatis外掛實現機制
外掛配置資訊的載入
先來看下mybatis是如何載入外掛配置的,對應的xml配置資訊如下:
對應的解析程式碼如下,主要做以下工作:
- 根據解析到的類資訊建立Interceptor物件;
- 呼叫setProperties方法設定屬性變數;
- 新增到Configuration的interceptorChain攔截器鏈中;
以上邏輯對應的時序圖如下:
代理物件的生成
Mybatis外掛的實現機制主要是基於動態代理實現的,其中最為關鍵的就是代理物件的生成,所以有必要來了解下這些代理物件是如何生成的。
Executor代理物件
ParameterHandler代理物件
ResultSetHandler代理物件
StatementHandler代理物件
觀察原始碼,發現這些可攔截的類對應的物件生成都是通過InterceptorChain的pluginAll方法來建立的,進一步觀察pluginAll方法,如下:
遍歷所有攔截器,呼叫攔截器的plugin方法生成代理物件,注意生成代理物件重新賦值給target,所以如果有多個攔截器的話,生成的代理物件會被另一個代理物件代理,從而形成一個代理鏈條,執行的時候,依次執行所有攔截器的攔截邏輯程式碼;
接下來看一下我們在編寫攔截器的時候,一個典型的plugin方法實現方式,如下:
再進一步檢視wrap方法,如下:
典型的動態代理實現,呼叫的是Proxy.newProxyInstance方法來生成代理物件。
以上邏輯對應的時序圖如下,這裡我們假設聲明瞭兩個攔截器,那麼在建立target代理物件的時候,最終返回的代理物件proxy2,實際上代理了proxy1,而proxy1又代理了target,:
攔截邏輯的執行
由於真正去執行Executor、ParameterHandler、ResultSetHandler和StatementHandler類中的方法的物件是代理物件(建議將代理物件轉為class檔案,反編譯檢視其結構,幫助理解),所以在執行方法時,首先呼叫的是Plugin類(實現了InvocationHandler介面)的invoke方法,如下:
首先根據執行方法所屬類獲取攔截器中宣告需要攔截的方法集合;
判斷當前方法需不需要執行攔截邏輯,需要的話,執行攔截邏輯方法(即Interceptor介面的intercept方法實現),不需要則直接執行原方法。
可以關注下Interceptor介面的intercept方法實現,一般需要使用者自定義實現邏輯,其中有一個重要引數,即Invocation類,通過改引數我們可以獲取執行物件,執行方法,以及執行方法上的引數,從而進行各種業務邏輯實現,一般在該方法的最後一句程式碼都是invocation.proceed()(內部執行method.invoke方法),否則將無法執行下一個攔截器的intercept方法。
以上邏輯對應的時序圖如下,這裡我們以執行executor物件的query方法為例,且假設有兩個攔截器存在:
Mybatis外掛開發例子
這裡以分頁外掛為例,來了解下一般mybatis外掛的編寫規則,如下所示:
主要需要實現三個方法
- intercept:在此實現自己的攔截邏輯,可從Invocation引數中拿到執行方法的物件,方法,方法引數,從而實現各種業務邏輯, 如下程式碼所示,從invocation中獲取的statementHandler物件即為被代理物件,基於該物件,我們獲取到了執行的原始SQL語句,以及prepare方法上的分頁引數,並更改SQL語句為新的分頁語句,最後呼叫invocation.proceed()返回結果。
- plugin:生成代理物件;
- setProperties:設定一些屬性變數;
小結
簡單的說,mybatis外掛就是對ParameterHandler、ResultSetHandler、StatementHandler、Executor這四個介面上的方法進行攔截,利用JDK動態代理機制,為這些介面的實現類建立代理物件,在執行方法時,先去執行代理物件的方法,從而執行自己編寫的攔截邏輯,所以真正要用好mybatis外掛,主要還是要熟悉這四個介面的方法以及這些方法上的引數的含義;
另外,如果配置了多個攔截器的話,會出現層層代理的情況,即代理物件代理了另外一個代理物件,形成一個代理鏈條,執行的時候,也是層層執行;
關於mybatis外掛涉及到的設計模式和軟體思想如下:
- 設計模式:代理模式、責任鏈模式;
- 軟體思想:AOP程式設計思想,降低模組間的耦合度,使業務模組更加獨立;
一些注意事項:
- 不要定義過多的外掛,代理巢狀過多,執行方法的時候,比較耗效能;
- 攔截器實現類的intercept方法裡最後不要忘了執行invocation.proceed()方法,否則多個攔截器情況下,執行鏈條會斷掉;