mybatis源碼分析05:攔截器

注:本系列源碼分析基於mybatis 3.5.6,源碼的gitee倉庫倉庫地址:funcy/mybatis.

mybatis的攔截器爲org.apache.ibatis.plugin.Interceptor,可以攔截一些操作,對應的mybatis文檔爲mybatis插件

對照着文檔,我們先來準備一個demo吧!

1. 準備攔截器demo:打印執行的sql

按照文檔內容,我們準備一個攔截器:

// 指定攔截的方法:query 與 update
@Intercepts({
  @Signature(type= Executor.class, method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  @Signature(type= Executor.class, method = "update",
    args = {MappedStatement.class, Object.class})
})
public class SqlInterceptor implements Interceptor {

  private Properties properties;

  /**
   * 處理攔截操作
   */
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    MappedStatement ms = (MappedStatement)args[0];
    BoundSql boundSql = ms.getBoundSql(args[1]);
    String sql = boundSql.getSql();
    System.out.println("執行的sql爲:" + sql);
    return invocation.proceed();
  }

  /**
   * 設置一些屬性
   */
  @Override
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

把攔截器添加到mybatis配置文件:

<configuration>
  <properties resource="org/apache/ibatis/demo/config.properties">
  </properties>
  <settings>
    ...
  </settings>
  <!-- 配置攔截器 -->
  <plugins>
    <plugin interceptor="org.apache.ibatis.demo.SqlInterceptor">
    </plugin>
  </plugins>
  <!-- 省略其他配置 -->
  ...
</configuration>

運行Test01,結果如下:

執行的sql爲:select id, login_name as loginName, nick from user
     where  id = ? 
     
      limit ?
[User{id=3, loginName='test', nick='HelloWorld'}]

可以看到,執行的sql成功打印了。

2. 攔截器裝配

2.1 解析

攔截器配置在mybatis配置文件中,因此在解析配置文件的XMLConfigBuilder#parseConfiguration中也會解析攔截器:

  private void parseConfiguration(XNode root) {
    try {
      ...
      pluginElement(root.evalNode("plugins"));
      ...
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

  /**
   * 解析攔截器節點
   */
  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 interceptorInstance 
            = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        // 添加攔截器
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

解析操作比較常規,就不多作分析了我們主要來看configuration.addInterceptor(...)方法,看看攔截器最終的去向:

public class Configuration {

  /**
   * 保存攔截器
   */
  protected final InterceptorChain interceptorChain = new InterceptorChain();

  /**
   * 將攔截器添加到 interceptorChain 中
   */
  public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }

  ...

}

攔截器最終都保存在了Configuration類中的成員變量interceptorChain中了。

這個InterceptorChain又是個啥呢?我們繼續:

public class InterceptorChain {

  /**
   * 保存攔截器的list
   */
  private final List<Interceptor> interceptors = new ArrayList<>();

  ...

  /**
   * 添加攔截器
   */
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

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

}

從代碼來看,它內部維護了一個List類型的成員變量,對外提供了addXxx(...)getXxx()操作方法。

2.2 裝配

mybatis的攔截器保存在Configuration類中的成員變量interceptorChain中,這些攔截器如何裝配到mybatis的執行鏈路上的呢?讓我們回到Configuration#newExecutor(...)方法,其中有這麼一行:

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    ...
    // 處理 plugin(插件)
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

InterceptorChain#pluginAll就是用來處理攔截器的加載了:

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // 調用各個攔截器的 plugin(...) 方法
      target = interceptor.plugin(target);
    }
    return target;
  }

這裏傳入的target類型是Executor,返回的也是Executor.

繼續進入interceptor.plugin()方法:

public interface Interceptor {

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

  ...
}

這是接口的默認方法,調用的是Plugin.wrap(...),我們繼續:

public class Plugin implements InvocationHandler {

  /** 目標對象,如:executor */
  private final Object target;
  /** 攔截器 */
  private final Interceptor interceptor;
  /** 保存攔截的方法 */
  private final Map<Class<?>, Set<Method>> signatureMap;

  /**
   * 私有的構造方法,在 wrap(...) 方法中調用
   */
  private Plugin(Object target, Interceptor interceptor, 
        Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  /**
   * 組裝代理對象
   */
  public static Object wrap(Object target, Interceptor interceptor) {
    // 獲取攔截的方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 獲取攔截類型的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // 生成代理對象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          // InvocationHandler 實例
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

    /**
   *  解析攔截器類上的標籤
   * @param interceptor
   * @return
   */
  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(...);
    }
    // 處理 @Intercepts 中的 @Signature 註解
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        // 獲取攔截的方法
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException(...);
      }
    }
    return signatureMap;
  }

  ...
}

從代碼來看,Plugin.wrap(...)會使用jdk的動態代理功能生成代理對象,Plugin實現了InvocationHandler,它的invoke(...)方法是攔截的關鍵,我們後面再分析。

示例中,傳入的是原始的executor,調用executor = (Executor) interceptorChain.pluginAll(executor)得到的executor就是動態代理類了:

本文使用的示例是攔截Executorqueryupdate方法,實際上還有其他方法可攔截,mybatis攔截器支持的方法如下:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

這些方法的攔截配置同Executor相差不大,就不多作分析了。

3. 執行

在上一節的分析中,我們得到的是executor的動態代理對象,那麼它是何時被執行的呢?

實際上,被動態代理的對象,執行方法時,都會調用其InvocationHandler實例的invoke(...)方法,executor的動態代理對象的InvocationHandlerPlugin,我們進入其invoke(...)方法:

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

Plugin#invoke(...)方法中,先會判斷方法要不要攔截,對於需要攔截的方法,調用其interceptor.intercept(...)方法,否則就直接調用該方法。

interceptor.intercept(...)的執行中,傳入的參數是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;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  /**
   * 執行目標方法
   */
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

這個類主要是保存了目標對象目標方法以及傳入目標方法的參數,在重寫Interceptor#intercept(...)方法時,我們就可以根據這些內容完成一系列操作。

Invocation還有一個重要的方法:proceed(),這個方法會執行目標方法的邏輯,像我們在實現SqlInterceptor時,是這要使用的:

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // 從 invocation 中獲取參數
    Object[] args = invocation.getArgs();
    MappedStatement ms = (MappedStatement)args[0];
    BoundSql boundSql = ms.getBoundSql(args[1]);
    String sql = boundSql.getSql();
    System.out.println("執行的sql爲:" + sql);
    // 處理完攔截邏輯後,要執行目標方法
    return invocation.proceed();
  }

處理完攔截操作後,不要忘了調用invocation.proceed()來執行原始邏輯。

4. 總結

本文是分析了mybatis攔截器機制,分析瞭解析、裝配、執行的流程,說到底,攔截器還是使用了jdk提供的動態代理功能。

關於mybatis攔截器相關分析就到這裏了。


本文原文鏈接:https://my.oschina.net/funcy/blog/4952678 ,限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

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