MyBatis 源碼篇-插件模塊

本章主要描述 MyBatis 插件模塊的原理,從以下兩點出發:

  1. MyBatis 是如何加載插件配置的?
  2. MyBatis 是如何實現用戶使用自定義攔截器對 SQL 語句執行過程中的某一點進行攔截的?

示例準備

首先準備兩個攔截器示例,代碼如下。

@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 AInterceptor implements Interceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(AInterceptor.class);

    /**
     * 執行攔截邏輯的方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        LOGGER.info("--------------執行攔截器A前--------------");
        Object obj = invocation.proceed();
        LOGGER.info("--------------執行攔截器A後--------------");
        return obj;
    }

    /**
     * 決定是否觸發intercept()方法
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 根據配置初始化Interceptor對象
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}
@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 BInterceptor implements Interceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(BInterceptor.class);

    /**
     * 執行攔截邏輯的方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        LOGGER.info("--------------執行攔截器B前--------------");
        Object obj = invocation.proceed();
        LOGGER.info("--------------執行攔截器B後--------------");
        return obj;
    }

    /**
     * 決定是否觸發intercept()方法
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 根據配置初始化Interceptor對象
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}

MyBatis 配置文件 mybatis-config.xml 增加 plugin 配置。

<plugins>
    <plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.AInterceptor"/>
    <plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.BInterceptor"/>
</plugins>

加載插件配置

在 MyBatis 初始化時,會通過 XMLConfigBuilder#pluginElement 方法解析 mybatis-config.xml 配置文件中定義的 <plugin> 節點,得到相應的 Interceptor 對象,最後將 Interceptor 對象添加到 Configuration.interceptorChain  字段中保存。源碼如下所示。

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      // 創建Interceptor對象
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      // 保存到Configuration.interceptorChain字段中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

public void addInterceptor(Interceptor interceptor) {
  interceptorChain.addInterceptor(interceptor);
}

攔截過程

繼續介紹 MyBatis 的攔截器如何對 Exector、StatementHandler、ParameterHandler、ResultSetHandler 進行攔截。

在 MyBatis 中使用的這四類對象,都是通過 Configuration 創建的,方法如下圖所示。如果配置了自定義攔截器,則會在該系列方法中,通過 InterceptorChain.pluginAll() 方法爲目標對象創建代理對象,所以通過 Configuration.new*() 系列方法得到的對象實際是一個代理對象。

以 newExecutor() 方法爲例進行分析,其他方法原理類似,newExecutor() 方法的具體實現如下所示。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  // 默認是SIMPLE
  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);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 通過InterceptorChain.pluginAll()方法創建Exector代理對象
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

在 InterceptorChain.pluginAll() 方法會遍歷 interceptors 集合,並調用每個 interceptor 的 plugin() 方法創建代理對象,具體實現如下所示。

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

一般我們自定義攔截器的 plugin 方法,會使用 MyBatis 提供的 Plugin 工具類,它實現了 InvocationHandler 接口,並提供了 wrap() 靜態方法用於創建代理對象,Plugin.wrap() 方法的具體實現如下所示。

public static Object wrap(Object target, Interceptor interceptor) {
  // 獲取用戶自定義Interceptor中@Signature註解的信息
  // getSignatureMap()方法負責處理@Signature註解
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // 獲取目標類型
  Class<?> type = target.getClass();
  // 獲取目標類型實現的接口
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // 使用JDK動態代理的方式創建代理對象
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  Set<Class<?>> interfaces = new HashSet<Class<?>>();
  while (type != null) {
    for (Class<?> c : type.getInterfaces()) {
      if (signatureMap.containsKey(c)) {
        interfaces.add(c);
      }
    }
    type = type.getSuperclass();
  }
  return interfaces.toArray(new Class<?>[interfaces.size()]);
}

示例中 Exector 存在兩個攔截器 AInterceptor 和 BInterceptor,在執行 InterceptorChain.pluginAll() 方法的時候,傳給 getAllInterfaces() 方法的 type 字段第一次是 CacheExector 對象,第二次是 CacheExector 的代理對象,因爲生成的代理對象也繼承 Exector 接口,signatureMap.containsKey(c) 可以獲得值,繼續生成代理的代理對象,結構如下圖所示。

image.png

在 Plugin.invoke() 方法中,會將當前調用方法與 signatureMap 集合中記錄的方法信息進行比較,如果當前調用的方法是需要被攔截的方法,則調用其 intercept() 方法進行處理,如果不能被攔截則直接調用 target 的相應方法。Plugin.invoke() 方法的具體實現如下所示。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // 獲取當前方法所在類或接口中,可被當前 Interceptor攔截的方法
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    // 如果當前調用的方法需要被攔截,則調用interceptor.intercept()方法進行攔截處理
    if (methods != null && methods.contains(method)) {
      return interceptor.intercept(new Invocation(target, method, args));
    }
    // 如果當前調用的方法不能被攔截,則調用target對象的相應方法
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

Interceptor.intercept() 方法的參數是 Invocation 對象,其中封裝了目標對象、目標方法以及調用目標方法的參數,並提供了 process() 方法調用目標方法,如下所示。

public Object proceed() throws InvocationTargetException, IllegalAccessException {
  return method.invoke(target, args);
}

需要注意的是,在 Interceptor.intercept() 方法中執行完攔截處理之後,如果需要調用目標方法,則通過  Invocation.process() 方法實現。

根據上面的分析,就不難理解示例的如下輸出日誌了,同時配置文件中插件的執行順序也清楚了。

[main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
[main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------執行攔截器B前--------------
[main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------執行攔截器A前--------------
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
[main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 360067785.
[main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==>  Preparing: select id, name, sex, selfcard_no, note from t_student where id = ? 
[main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Parameters: 1(Long)
[main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - <==      Total: 1
[main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------執行攔截器A後--------------
[main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------執行攔截器B後--------------
Student [Hash = 550752602, id=1, name=張三, sex=1, selfcardNo=111, note=zhangsan]

 

MyBatis 源碼篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章