1. 程式人生 > >mybatis 原始碼分析(五)Interceptor 詳解

mybatis 原始碼分析(五)Interceptor 詳解

本篇部落格將主要講解 mybatis 外掛的主要流程,其中主要包括動態代理和責任鏈的使用;

一、mybatis 攔截器主體結構

在編寫 mybatis 外掛的時候,首先要實現 Interceptor 介面,然後在 mybatis-conf.xml 中新增外掛,

<configuration>
  <plugins>
    <plugin interceptor="***.interceptor1"/>
    <plugin interceptor="***.interceptor2"/>
  </plugins>
</configuration>

這裡需要注意的是,新增的外掛是有順序的,因為在解析的時候是依次放入 ArrayList 裡面,而呼叫的時候其順序為:2 > 1 > target > 1 > 2;(外掛的順序可能會影響執行的流程)更加細緻的講解可以參考 QueryInterceptor 規範 ;

然後當外掛初始化完成之後,新增外掛的流程如下:

首先要注意的是,mybatis 外掛的攔截目標有四個,Executor、StatementHandler、ParameterHandler、ResultSetHandler:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  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) {
  ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
  return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  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);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

這裡使用的時候都是用動態代理將多個外掛用責任鏈的方式新增的,最後返回的是一個代理物件; 其責任鏈的新增過程如下:

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

最終動態代理生成和呼叫的過程都在 Plugin 類中:

public static Object wrap(Object target, Interceptor interceptor) {
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 獲取簽名Map
  Class<?> type = target.getClass(); // 攔截目標 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  // 獲取目標介面
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(  // 生成代理
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

這裡所說的簽名是指在編寫外掛的時候,指定的目標介面和方法,例如:

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

這裡就指定了攔截 Executor 的具有相應方法的 update、query 方法;註解的程式碼很簡單,大家可以自行檢視;然後通過 getSignatureMap 方法反射取出對應的 Method 物件,在通過 getAllInterfaces 方法判斷,目標物件是否有對應的方法,有就生成代理物件,沒有就直接反對目標物件;

在呼叫的時候:

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);
  }
}

二、PageHelper 攔截器分析

mybatis 外掛我們平時使用最多的就是分頁外掛了,這裡以 PageHelper 為例,其使用方法可以檢視相應的文件 如何使用分頁外掛,因為官方文件講解的很詳細了,我這裡就簡單補充分頁外掛需要做哪幾件事情;

使用:

PageHelper.startPage(1, 2);
List<User> list = userMapper1.getAll();

PageHelper 還有很多中使用方式,這是最常用的一種,他其實就是在 ThreadLocal 中設定了 Page 物件,能取到就代表需要分頁,在分頁完成後在移除,這樣就不會導致其他方法分頁;(PageHelper 使用的其他方法,也是圍繞 Page 物件的設定進行的)

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  Page<E> page = new Page<E>(pageNum, pageSize, count);
  page.setReasonable(reasonable);
  page.setPageSizeZero(pageSizeZero);
  //當已經執行過orderBy的時候
  Page<E> oldPage = getLocalPage();
  if (oldPage != null && oldPage.isOrderByOnly()) {
    page.setOrderBy(oldPage.getOrderBy());
  }
  setLocalPage(page);
  return page;
}

主要實現:

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

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    try {
      Object[] args = invocation.getArgs();
      MappedStatement ms = (MappedStatement) args[0];
      Object parameter = args[1];
      RowBounds rowBounds = (RowBounds) args[2];
      ResultHandler resultHandler = (ResultHandler) args[3];
      Executor executor = (Executor) invocation.getTarget();
      CacheKey cacheKey;
      BoundSql boundSql;
      //由於邏輯關係,只會進入一次
      if (args.length == 4) {
        //4 個引數時
        boundSql = ms.getBoundSql(parameter);
        cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
      } else {
        //6 個引數時
        cacheKey = (CacheKey) args[4];
        boundSql = (BoundSql) args[5];
      }
      checkDialectExists();

      List resultList;
      //呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果
      if (!dialect.skip(ms, parameter, rowBounds)) {
        //判斷是否需要進行 count 查詢
        if (dialect.beforeCount(ms, parameter, rowBounds)) {
          //查詢總數
          Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
          //處理查詢總數,返回 true 時繼續分頁查詢,false 時直接返回
          if (!dialect.afterCount(count, parameter, rowBounds)) {
            //當查詢總數為 0 時,直接返回空的結果
            return dialect.afterPage(new ArrayList(), parameter, rowBounds);
          }
        }
        resultList = ExecutorUtil.pageQuery(dialect, executor,
            ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
      } else {
        //rowBounds用引數值,不使用分頁外掛處理時,仍然支援預設的記憶體分頁
        resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
      }
      return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
      if(dialect != null){
        dialect.afterAll();
      }
    }
  }
}
  • 首先可以看到攔截的是 Executor 的兩個 query 方法(這裡的兩個方法具體攔截到哪一個受外掛順序影響,最終影響到 cacheKey 和 boundSql 的初始化);
  • 然後使用 checkDialectExists 判斷是否支援對應的資料庫;
  • 在分頁之前需要查詢總數,這裡會生成相應的 sql 語句以及對應的 MappedStatement 物件,並快取;
  • 然後拼接分頁查詢語句,並生成相應的 MappedStatement 物件,同時快取;
  • 最後查詢,查詢完成後使用 dialect.afterPage 移除 Page物件