MyBatis 插件原理與實戰

前言

大家好,我是田螺。

最近在做新項目,基於若依(前後端分離版本)做的,他也使用了常用的分頁插件PageHelper

老規矩,今天文章還是分三步走,先上文章導讀,然後講原理,最後講解源碼案例

最後達到的效果就是希望讀者朋友們在看完我寫的這篇文章後,能夠秒懂別人寫的 MyBatis 插件並且能夠開發出自己的 MyBatis 的插件

文章導讀

MyBatis 插件原理與實戰

什麼是插件?

插件就是在具體的執行流程插一腳(觸發點、攔截器)來實現具體的功能。

一般插件會對執行流程中的上下文有依賴,抽象的說,我們也可以把 MyBatis 看作是 JDBC 的插件,只是功能越來來多,越來越強大,最後我們給了他一個新名字,叫做框架

不管怎樣,JDBC 的那一套還是不會變的,只是做了抽象、封裝、歸類等。

要想知道插件的原理,首先就要對它的執行流程有一定的把控。

執行流程

前邊我們講到,MyBatis 是對 JDBC 的抽象、封裝。

我們首先來回顧一下 JDBC 的執行流程。

JDBC 執行流程

  1. 註冊驅動;

  2. 獲取 Connection 連接;

  3. 執行預編譯;

  4. 執行 SQL;

  5. 封裝結果集;

  6. 釋放資源;

給段僞代碼通透理解下:

// 註冊驅動
Class.forName("com.mysql.jdbc.Driver");
// 獲取鏈接
Connection con = DriverManager.getConnection(url, username, password);
// 執行預編譯
Statement stmt = con.createStatement();
// 執行SQL
ResultSet rs = stmt.executeQuery("SELECT * FROM ...");
// 封裝結果
while (rs.next()) {
  String name = rs.getString("name");
  String pass = rs.getString(1); // 此方法比較高效
}
// 釋放資源
if (rs != null) {  // 關閉記錄集

}
if (stmt != null) {  // 關閉聲明

}
if (conn != null) { // 關閉連接對象

}

上邊的代碼是不是很熟悉,我相信每個入門寫 Java 代碼的人,都寫過這段代碼。

緊接着,我們繼續來了解 MyBatis 的執行流程。

MyBatis 執行流程

  1. 讀取 MyBatis 的核心配置文件;

  2. 加載映射文件;

  3. 構造會話工廠獲取 SqlSessionFactory;

  4. 創建會話對象 SqlSession;

  5. Executor 執行器;

  6. MappedStatement 對象;

  7. 輸入參數映射;

  8. 封裝結果集;

上邊的文字可能不太好理解,我這裏也畫一幅執行流程圖,來方便理解。

MyBatis 執行流程

有沒有覺得 MyBatis 的執行流程和 JDBC 的執行流程主幹也差不多,只是在主幹過程中,把一些配置(mybatis-config.xml)、常用的定義文件單獨抽離出來(mapper.xml)和一些附帶擴展性的攔截點抽離了出來。

下面着重講一講我們的攔截點,因爲插件就是基於我們的攔截點來做的擴展。

攔截點

結合上邊的 MyBatis 執行流程,看下圖的各個攔截點:

MyBatis 攔截點

文字描述,MyBatis 允許使用插件來攔截的方法調用包括:

  1. Executor:

    攔截執行器的方法 (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed),Mybatis 的內部執行器,它負責調用 StatementHandler 操作數據庫,並把結果集通過 ResultSetHandler 進行自動映射,另外,他還處理了二級緩存的操作。從這裏可以看出,我們也是可以通過插件來實現自定義的二級緩存的;

  2. ParameterHandler:

    攔截參數的處理 (getParameterObject, setParameters) ,Mybatis 直接和數據庫執行 sql 腳本的對象。另外它也實現了 Mybatis 的一級緩存。這裏,我們可以使用插件來實現對一級緩存的操作 (禁用等等);

  3. ResultSetHandler:

    攔截結果集的處理 (handleResultSets, handleOutputParameters) ,Mybatis 實現 Sql 入參設置的對象。插件可以改變我們 Sql 的參數默認設置;

  4. StatementHandler:

    攔截 Sql 語法構建的處理 (prepare, parameterize, batch, update, query) ,Mybatis 把 ResultSet 集合映射成 POJO 的接口對象。我們可以定義插件對 Mybatis 的結果集自動映射進行修改。

攔截器爲什麼能夠攔截

org.apache.ibatis.session.Configuration是在 MyBatis 初始化配置的類。

其中的newParameterHandlernewResultSetHandlernewStatementHandlernewExecutor這幾個方法在創建指定的對象(newParameterHandler 創建 ParameterHandler、newResultSetHandler 創建 ResultSetHandler、newStatementHandler 創建 StatementHandler、newExecutor 創建 Executor)對象的時候,都會調用一個統一的方法:

創建對象

這 4 個方法實例化了對應的對象之後,都會調用 interceptorChain 的 pluginAll 方法,那麼下面我們在來看 pluginAll 幹了什麼。

包路徑:org.apache.ibatis.plugin.InterceptorChain

public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }

原來這個 pluginAll 方法就是遍歷所有的攔截器,然後順序執行我們插件的 plugin 方法, 一層一層返回我們原對象 (Executor/ParameterHandler/ResultSetHander/StatementHandler) 的代理對象。當我們調用四大接口對象的方法時候,實際上是調用代理對象的響應方法,代理對象又會調用四大接口對象的實例。

這裏我們看到所有的攔截器 Interceptor,其實它和我們平常寫代碼一樣,也是多態的利用,存在一個攔截器 Interceptor 接口,我們在實現插件的時候,也實現這個接口,就會被調用。

Interceptor 接口

包路徑:org.apache.ibatis.plugin

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    default void setProperties(Properties properties) {
    }
}

這個接口只聲明瞭三個方法:

  1. setProperties 方法是在 Mybatis 進行配置插件的時候可以配置自定義相關屬性,即:接口實現對象的參數配置;

  2. plugin 方法是插件用於封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理,可以決定是否要進行攔截進而決定要返回一個什麼樣的目標對象,官方提供了示例:return Plugin.wrap(target, this);

  3. intercept 方法就是要進行攔截的時候要執行的方法;

編寫簡單的 MyBatis 插件

注:MyBatis 默認沒有一個攔截器接口的實現類,開發者可以實現符合自己需求的攔截器

@Intercepts({@Signature(type= Executor.class, method = "update"args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    public void setProperties(Properties properties) {
    }
}

全局 xml 配置(實例化 bean)

<plugins>
<plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>

這個攔截器攔截 Executor 接口的 update 方法(其實也就是 SqlSession 的新增,刪除,修改操作),所有執行 executor 的 update 方法都會被該攔截器攔截到,就在裏邊做相對應的邏輯處理就可以了。

總結

今天這篇文章到這裏結束了,講解了什麼是插件首先需要了解執行流程,然後回顧我們的 JDBC 流程來推導出 MyBatis 的執行流程,通過初始化的org.apache.ibatis.session.Configuration爲切入點,跟蹤到 interceptorChain 的 pluginAll 方法;最後通過一個簡單的插件來實操了一波。

參考鏈接:

https://zhuanlan.zhihu.com/p/150008843

https://blog.csdn.net/weixin_44046437/article/details/100523028

https://blog.csdn.net/weixin_44046437/article/details/100526643

程序員田螺 專注分享後端面試題,包括計算機網絡、MySql 數據庫、Redis 緩存、操作系統、Java 後端、大廠面試真題等領域。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/avDrr1dM-YjW0igfZ2RH6Q