精盡MyBatis源碼分析 – 插件機制

該系列文檔是本人在學習 Mybatis 的源碼過程中總結下來的,可能對讀者不太友好,請結合我的源碼注釋(Mybatis源碼分析 GitHub 地址Mybatis-Spring 源碼分析 GitHub 地址Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀

MyBatis 版本:3.5.2

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

插件機制

開源框架一般都會提供插件或其他形式的擴展點,供開發者自行擴展,增加框架的靈活性

當然,MyBatis 也提供了插件機制,基於它開發者可以進行擴展,對 MyBatis 的功能進行增強,例如實現分頁、SQL分析、監控等功能,本文會對 MyBatis 插件機制的原理以及如何實現一個自定義的插件來進行講述

我們在編寫插件時,除了需要讓插件類實現 org.apache.ibatis.plugin.Interceptor 介面,還需要通過註解標註該插件的攔截點,也就是插件需要增強的方法,MyBatis 只提供下面這些類中定義的方法能夠被增強:

  • Executor:執行器

  • ParameterHandler:參數處理器

  • ResultSetHandler:結果集處理器

  • StatementHandler:Statement 處理器

植入插件邏輯

《MyBatis的SQL執行過程》一系列文檔中,有講到在創建Executor、ParameterHandler、ResultSetHandler和StatementHandler對象時,會調用InterceptorChainpluginAll方法,遍歷所有的插件,調用Interceptor插件的plugin方法植入相應的插件邏輯,所以在 MyBatis 中只有上面的四個對象中的方法可以被增強

程式碼如下:

// Configuration.java

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // <1> 獲得執行器類型
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    // <2> 創建對應實現的 Executor 對象
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    // <3> 如果開啟快取,創建 CachingExecutor 對象,進行包裝
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // <4> 應用插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
        BoundSql boundSql) {
    // 創建 ParameterHandler 對象
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 應用插件
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
        ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
	// 創建 DefaultResultSetHandler 對象
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, 
                                                                    parameterHandler, resultHandler, boundSql, rowBounds);
	// 應用插件
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, 
                                                                    parameterObject, rowBounds, resultHandler, boundSql);
    // 將 Configuration 全局配置中的所有插件應用在 StatementHandler 上面
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

分頁插件示例

我們先來看一個簡單的插件示例,程式碼如下:

@Intercepts({
  @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
  )
})
public class ExamplePlugin implements Interceptor {

  // Executor的查詢方法:
  // public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    RowBounds rowBounds = (RowBounds) args[2];
    if (rowBounds == RowBounds.DEFAULT) { // 無需分頁
      return invocation.proceed();
    }
    /*
     * 將query方法的 RowBounds 入參設置為空對象
     * 也就是關閉 MyBatis 內部實現的分頁(邏輯分頁,在拿到查詢結果後再進行分頁的,而不是物理分頁)
     */
    args[2] = RowBounds.DEFAULT;

    MappedStatement mappedStatement = (MappedStatement) args[0];
    BoundSql boundSql = mappedStatement.getBoundSql(args[1]);

    // 獲取 SQL 語句,拼接 limit 語句
    String sql = boundSql.getSql();
    String limit = String.format("LIMIT %d,%d", rowBounds.getOffset(), rowBounds.getLimit());
    sql = sql + " " + limit;

    // 創建一個 StaticSqlSource 對象
    SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings());

    // 通過反射獲取並設置 MappedStatement 的 sqlSource 欄位
    Field field = MappedStatement.class.getDeclaredField("sqlSource");
    field.setAccessible(true);
    field.set(mappedStatement, sqlSource);

    // 執行被攔截方法
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    // default impl
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    // default nop
  }
}

在上面的分頁插件中,@Intercepts@Signature兩個註解指定了增強的方法是Executor.query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler),也就是我們使用到的 Executor 執行資料庫查詢操作的方法

在實現的 intercept 方法中,通過 RowBounds 參數獲取分頁資訊,並生成相應的 SQL(拼接了 limit) ,並使用該 SQL 作為參數重新創建一個 StaticSqlSource 對象,最後通過反射替換 MappedStatement 對象中的 sqlSource 欄位,這樣就實現了一個簡單的分頁插件

上面只是一個簡單的示例,實際場景中慎用

Interceptor

org.apache.ibatis.plugin.Interceptor:攔截器介面,程式碼如下:

public interface Interceptor {
  /**
   * 攔截方法
   *
   * @param invocation 調用資訊
   * @return 調用結果
   * @throws Throwable 若發生異常
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * 應用插件。如應用成功,則會創建目標對象的代理對象
   *
   * @param target 目標對象
   * @return 應用的結果對象,可以是代理對象,也可以是 target 對象,也可以是任意對象。具體的,看程式碼實現
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  /**
   * 設置攔截器屬性
   *
   * @param properties 屬性
   */
  default void setProperties(Properties properties) {
    // NOP
  }
}
  • intercept方法:攔截方法,插件的增強邏輯
  • plugin方法:應用插件,往目標對象中植入相應的插件邏輯,如果應用成功則返回一個代理對象(JDK動態代理),否則返回原始對象,默認調用Pluginwrap方法
  • setProperties方法:設置攔截器屬性

Invocation

org.apache.ibatis.plugin.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;
	}
    // 省略 getter setter 方法
}

Plugin

org.apache.ibatis.plugin.Plugin:實現InvocationHandler介面,用於對攔截的對象進行,一方面提供創建動態代理對象的方法,另一方面實現對指定類的指定方法的攔截處理,MyBatis插件機制的核心類

構造方法

public class Plugin implements InvocationHandler {
	/**
	 * 目標對象
	 */
	private final Object target;
	/**
	 * 攔截器
	 */
	private final Interceptor interceptor;
	/**
	 * 攔截的方法映射
	 *
	 * KEY:類
	 * VALUE:方法集合
	 */
	private final Map<Class<?>, Set<Method>> signatureMap;

	private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
		this.target = target;
		this.interceptor = interceptor;
		this.signatureMap = signatureMap;
	}
}

wrap方法

wrap(Object target, Interceptor interceptor)方法,創建目標類的代理對象,方法如下:

public static Object wrap(Object target, Interceptor interceptor) {
    // <1> 獲得攔截器中需要攔截的類的方法集合
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // <2> 獲得目標對象的 Class 對象
    Class<?> type = target.getClass();
    // <3> 獲得目標對象所有需要被攔截的 Class 對象(父類或者介面)
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // <4> 若存在需要被攔截的,則為目標對象的創建一個動態代理對象(JDK 動態代理),代理類為 Plugin 對象
    if (interfaces.length > 0) {
        // 因為 Plugin 實現了 InvocationHandler 介面,所以可以作為 JDK 動態代理的調用處理器
        return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
    }
    // <5> 如果沒有,則返回原始的目標對象
    return target;
}
  1. 調用getSignatureMap方法,獲得攔截器中需要攔截的類的方法集合,有就是通過@Intercepts@Signature兩個註解指定的增強的方法
  2. 獲得目標對象的 Class 對象(父類或者介面)
  3. 獲得目標對象所有需要被攔截的 Class 對象
  4. 如果需要被攔截,則為目標對象的創建一個動態代理對象(JDK 動態代理),代理類為 Plugin 對象,並返回該動態代理對象
  5. 否則返回原始的目標對象

getSignatureMap方法

getSignatureMap(Interceptor interceptor)方法,獲取插件需要增強的方法,方法如下:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // 獲取 @Intercepts 註解
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
        throw new PluginException( "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    // 獲取 @Intercepts 註解中的 @Signature 註解
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      // 為 @Signature 註解中定義類名創建一個方法數組
        Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
        try {
          // 獲取 @Signature 註解中定義的方法對象
            Method method = sig.type().getMethod(sig.method(), sig.args());
            methods.add(method);
        } catch (NoSuchMethodException e) {
            throw new PluginException(
                    "Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
        }
    }
    return signatureMap;
}
  • 通過該插件上面的@Intercepts@Signature註解,獲取到所有需要被攔截的對象中的需要增強的方法

getAllInterfaces方法

getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap)方法,判斷目標對象是否需要被插件應用,方法如下:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    // 介面的集合
    Set<Class<?>> interfaces = new HashSet<>();
    // 循環遞歸 type 類,機器父類
    while (type != null) {
        // 遍歷介面集合,若在 signatureMap 中,則添加到 interfaces 中
        for (Class<?> c : type.getInterfaces()) {
            if (signatureMap.containsKey(c)) {
                interfaces.add(c);
            }
        }
        // 獲得父類
        type = type.getSuperclass();
    }
    // 創建介面的數組
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}
  • 入參signatureMap就是getSignatureMap方法返回的該插件需要增強的方法
  • 返回存在於signatureMap集合中所有目標對象的父類或者介面

invoke方法

invoke(Object proxy, Method method, Object[] args)方法,動態代理對象的攔截方法,方法如下:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 獲得目標方法所在的類需要被攔截的方法
        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);
    }
}
  1. 獲得目標方法所在的類需要被攔截的方法
  2. 如果被攔截的方法包含當前方法,則將當前方法封裝成Invocation對象,調用Interceptor插件的intercept方法,執行插件邏輯
  3. 否則執行原有方法

這樣一來,當你調用了目標對象的對應方法時,則會進入該插件的intercept方法,執行插件邏輯,擴展功能

InterceptorChain

org.apache.ibatis.plugin.InterceptorChain:攔截器鏈,用於將所有的攔截器按順序將插件邏輯植入目標對象,程式碼如下:

public class InterceptorChain {

	private final List<Interceptor> interceptors = new ArrayList<>();

	public Object pluginAll(Object target) {
    	// 遍歷攔截器集合
		for (Interceptor interceptor : interceptors) {
      		// 調用攔截器的 plugin 方法植入相應的插件邏輯
			target = interceptor.plugin(target);
		}
		return target;
	}

	public void addInterceptor(Interceptor interceptor) {
		interceptors.add(interceptor);
	}

	public List<Interceptor> getInterceptors() {
		return Collections.unmodifiableList(interceptors);
	}
}

配置MyBatis插件都會保存在interceptors集合中,可以回顧到《初始化(一)之載入mybatis-config.xml》XMLConfigBuilder小節的pluginElement方法,會將解析到的依次全部添加到ConfigurationInterceptorChain對象中,程式碼如下:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        // 遍歷 <plugins /> 標籤
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            // <1> 創建 Interceptor 對象,並設置屬性
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            // <2> 添加到 configuration 中
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

總結

本文分析了 MyBatis 中插件機制,總體來說比較簡單的,想要實現一個插件,需要實現 Interceptor 介面,並通過@Intercepts@Signature兩個註解指定該插件的攔截點(支援對Executor、ParameterHandler、ResultSetHandler 和 StatementHandler 四個對象中的方法進行增強),在實現的intercept方法中進行邏輯處理

在 MyBatis 初始化的時候,會掃描插件,將其添加到InterceptorChain

然後 MyBatis 在 SQL 執行過程中,創建上面四個對象的時候,會將創建的對象交由InterceptorChain去處理,遍歷所有的插件,通過插件的plugin方法為其創建一個動態代理對象並返回,代理類是Plugin對象

Plugin對象中的invoke方法中,將請求交由插件的intercept方法去處理

雖然 MyBatis 的插件機制比較簡單,但是想要實現一個完善且高效的插件卻比較複雜,可以參考PageHelper分頁插件

到這裡,相信大家對 MyBatis 的插件機制有了一定的了解,感謝大家的閱讀!!!😄😄😄

參考文章:芋道源碼《精盡 MyBatis 源碼分析》