談談 MyBatis 的外掛化設計

breezeQian發表於2019-04-16

1.MyBatis 是什麼  

MyBatis 是一個 ORM(Object Relational Mapping,物件 - 關係對映)框架,其中我們寫的 Java 程式碼中的物件資料就是物件模型,關聯式資料庫中的表資料就是關係模型。MyBatis 底層通過 JDBC 介面與關聯式資料庫互動,主要功能是根據對映配置檔案,完成資料在物件模型與關係模型之間的對映,減少了單純用 JDBC 開發時的重複程式碼,只暴露簡單的 API 供開發人員使用。 


 2.什麼是外掛

外掛是一種常見的擴充套件方式,大多數開源框架也都支援使用者通過新增自定義外掛的方式來 擴充套件或改變框架原有的功能。 Mybatis 中也提供了外掛的功能,雖然叫外掛,但是實際上是通過 攔截器( Interceptor)實現的。在 MyBatis 的外掛模組中涉及 責任鏈模式和 JDK 動態代理的知識, 下文會分析責任鏈模式和 JDK 動態代理在 MyBatis 外掛模組中的實踐。


 3.責任鏈模式介紹

 責任鏈模式主要用來處理 ”客戶端發出一個請求,有多個物件都有機會來處理這一個請求,但是客戶端不知道究竟誰會來處理他的請求“ 這樣的情況。也就是需要讓請求者和接收者解耦,這樣就可以動態地切換和組合接收者了。 注意,在責任鏈模式中,請求不一定會被處理,因為可能沒有合適的處理者,請求在責任鏈中從頭傳遞到尾,每個處理物件都判斷不屬於自己處理,最後請求就沒有物件來處理。 責任鏈模式的結構圖如下:

 

談談 MyBatis 的外掛化設計


當有請求進入時,經過 HandlerOne 的 handlerRequest 方法,再把請求傳遞給 HandlerTwo,處理完再把請求傳遞給 HandlerThree,以此類推,形成一個鏈條。鏈條上每一個物件所承擔的責任各不相同,這就是責任鏈模式。 


 4.MyBatis 中的攔截器  

(1)攔截器的作用點

MyBatis允許使用者使用自定義攔截器對 SQL語句執行過程中的某一點進行攔截。 預設情況 下, MyBatis 允許攔截器攔截 Executor 的方法、 ParameterHandler 的方法、 ResultSetHandler 的 方法 以及 StatementHandler 的方法。這裡說明一下,閱讀過 MyBatis 原始碼的同學應該都知道,這裡的 Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler 都是 MyBatis 操作 SQL 過程中的必備的核心層,而我們的攔截器也就是在這些介面的實現類的方法執行中起作用的。 具體可攔截的方法如下 : 

Executor:

         update()、query()、flushStatements()、commit()、rollback() 、 getTransaction()、 close()、 isClosed()方法 。

 ParameterHandler :

         getParameterObject()、 setParameters()方法 。 

ResultSetHandler:

        handleResultSets()、 handleOutputParameters()方法 。

 StatementHandler: 

        prepare()、 parameterize()、 batch()、 update()、 query()方法。

 

(2)攔截器定義

 public interface Interceptor { 

    //執行攔截器方法 
    Object intercept(Invocation invocation) throws Throwable; 

    // 建立interceptor物件,一般用mybatis提供的Plugin.wrap(...)方法 
    // 該方法內部會判斷,當前物件是否匹配當前interceptor,匹配則建立代理物件,不匹配則返回當前物件。 
    Object plugin(Object target); 

    // mybatis載入初始化時,會根據xml或註解的配置,將攔截器中的配置值取出,呼叫這個方法做初始化操作
    void setProperties(Properties properties); 

}複製程式碼

 我們自定義的攔截器需要實現這個介面,並在 MyBatis 中做好配置,即可生效。  


(3)攔截器鏈

 public class InterceptorChain { 

    // 所有已經配置的攔截器 
    private final List<Interceptor> interceptors = new ArrayList<>(); 

    public Object pluginAll(Object target) { 
        for (Interceptor interceptor : interceptors) { 
            target = interceptor.plugin(target); 
        } 
        return target; 
    } 

    // mybatis載入初始化時,會根據xml或註解的配置,建立攔截器,呼叫這個方法存入欄位 interceptors 中 
    public void addInterceptor(Interceptor interceptor) { 
        interceptors.add(interceptor);
    }

    // 獲取所有的攔截器 
    public List<Interceptor> getInterceptors() { 
        return Collections.unmodifiableList(interceptors); 
    } 

} 複製程式碼

MyBatis 在啟動載入配置檔案時將已配置的所有攔截器物件(包括自定義的攔截器物件)都放在 InterceptorChain 類物件的欄位 interceptors 中供後續操作使用。 在 InterceptorChain 的 pluginAll 方法中,迴圈 interceptors,呼叫 interceptor.plugin(target) 方法,這個 plugin 方法內部會判斷這個 target 物件是否匹配當前當前的 interceptor 物件,如果匹配則用 JDK 動態代理建立 target 物件的代理物件,否則返回未被代理的 target 物件。這裡是迴圈 interceptors,所以每個 interceptor 都會去執行一次 plugin 方法,也就是上個 interceptor.plugin(target) 呼叫的返回值是下一個 interceptor.plugin(target) 的入參。最後結果就是 target 可能會被代理多次。如下圖: 


談談 MyBatis 的外掛化設計


結果看到了,最後的 target 物件可能是被多次代理過的物件,也就是圖中的 interceptorProxy1 物件。當 interceptorProxy1 執行 interceptor 方法時,內部會呼叫到 interceptorProxy2 物件的 interceptor 方法, interceptorProxy2 的 interceptor 方法內部繼續往下調直到最終的 target 物件呼叫。 說到這,可以看出 target 物件先是匹配所有適合自己的 interceptor 物件,然後在所有匹配的 interceptor 物件中流轉。這就可以理解成責任鏈模式的設計。


 5.總結

關於 MyBatis 中攔截器的作用點,攔截器定義,以及攔截器的責任鏈模式在這裡做了一定程度的講解,下篇文章會補充攔截器的代理物件是如何建立的,也就是 interceptor.plugin(target); 代理的內部邏輯實現。




相關文章