Mybatis攔截器執行過程解析

mark-516277_640.jpg

上一篇文章 Mybatis攔截器之數據加密解密 介紹了 Mybatis 攔截器的簡單使用,這篇文章將透徹的分析 Mybatis 是怎樣發現攔截器以及調用攔截器的 intercept 方法的java

小夥伴先按照文章內容 細緻但不入微的瞭解整個攔截器執行過程,在紙上勾勒出各個點,再 細緻入微的讀源碼,將這些點用線串起來,這樣站在上帝視角後,理解的更加深入

發現攔截器

按照官網說明,咱們經過實現 org.apache.ibatis.plugin.Interceptor 接口自定義的攔截器,有兩種方式將自定義攔截器添加到 Mybatis 的 configuration 中node

配置文件方式

mybatis-config.xml 中添加 plugingit

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

XMLConfigBuilder 負責解析 Mybatis 全局配置文件,其中有 pluginElement 方法:sql

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).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

從該方法中能夠看出,遍歷 XML XNode,若是該 node 有 interceptor 屬性,說明咱們配置了攔截器,經過 configuration.addInterceptor(interceptorInstance); 將攔截器實例添加到 Mybatis Configuration 中apache

註解方式

文章 Mybatis攔截器之數據加密解密中看到我在自定義的攔截器類上添加了 @Component 註解, 當下微服務框架中多以 Spring Boot 添加 Mybatis Starter 依賴的形式存在,來看MybatisAutoConfiguration.java 的構造方法:設計模式

public MybatisAutoConfiguration(MybatisProperties properties,
                              ObjectProvider<Interceptor[]> interceptorsProvider,
                              ResourceLoader resourceLoader,
                              ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                              ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}

構造方法中 interceptorsProvider.getIfAvailable(); 獲取全部注入的 Interceptor,同時在構建 SqlSessionFactory 的時候,添加咱們的攔截器:數組

if (!ObjectUtils.isEmpty(this.interceptors)) {
  factory.setPlugins(this.interceptors);
}

調用過程解析

Configuration 類包含 Mybatis 的一切配置信息,裏面有 4 個很是重要的方法,也是攔截器攔截的方法session

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

他們的執行順序是 newExecutor -> StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler,爲何是這個順序,且看:
咱們知道在 MyBatis 中,使用 SqlSessionFactory 來建立 SqlSession。 一旦有了會話,就可使用它來執行映射語句,提交或回滾鏈接,最後,當再也不須要時,關閉會話。
且看,在 DefaultSqlSessionFactory.java 類中的 openSessionFromDataSource 方法中調用 Configuration 類的 newExecutor 方法mybatis

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //調用 Configuration 類的 newExecutor 方法建立一個執行器
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

SqlSessionTemplate.java 實現了 SqlSession 接口,裏面有一個 私有內部類 SqlSessionInterceptor 並實現了 InvocationHandler, 很明顯,這是 Java 動態代理的實現方式,關注重寫的 invoke 方法,這裏是方法真正被調用的地方,跟蹤調用棧發現最終調用到 Configuration 類的 newStatementHandler 方法app

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 真正的方法執行,順着方法走下去,就會調用 Configuration 類的 newStatementHandler 方法
        Object result = method.invoke(sqlSession, args);
        ...
      } catch (Throwable t) {
        ...
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

在 Configuration 類的 newStatementHandler 方法中,經過 new RoutingStatementHandler(...) 方法來構建 StatementHandler,在該方法中,根據 statementType 來判斷生成哪種 StatementHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

三種類型 StatementHandler 都繼承了 BaseStatementHandler.java, 看下面的類關係圖
Xnip2019-06-14_10-15-05.jpg

在實例化具體的 StatementHandler 的時候都會先調用父類 BaseStatementHandler 的構造器,在父類的構造器中分別順序調用了 Configuration 類中的 newParameterHandlernewResultSetHandler 方法:

this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);

因此說整個調用過程是這樣的:newExecutor -> StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler

說了這麼多尚未講到攔截器是怎樣被執行的,別急,前面這些都是鋪墊,也許有細心的小夥伴已經發現,在 Configuratin 類中的那四個方法中,都有相同的一段代碼:

interceptorChain.pluginAll(...)

沒錯,經過名字咱們也猜想獲得,這是攔截器的關鍵,interceptorChain 是 Configuration 類的成員變量,且看 InterceptorChain.java 類:

public class InterceptorChain {

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

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

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

}

在 pluginAll 方法中遍歷全部攔截器的 plugin 方法,在自定義的攔截器中,咱們重寫 plugin 方法,這裏以 通用分頁攔截器 講解調用攔截器過程,來看關鍵代碼:

@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 plugin(Object target) {
        return Plugin.wrap(target, this);
    }
}

Plugin.java 實現了 InvocationHandler 接口,看的出也是 Java 動態代理,調用其靜態方法 wrap:

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,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

若是 interfaces.length > 0 也就會爲 target 生成代理對象,也就是說爲 Configuration類 四個方法調用的 executor/parameterHandler/resultSetHandler/statementHandler 生成代理對象,這裏須要單獨分析裏面的兩個很重要的方法 getSignatureMap(interceptor)getAllInterfaces(type, signatureMap)
先看 getSignatureMap(interceptor) 方法:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        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;
  }

該方法經過 Java 反射讀取攔截器類上的註解信息,最終返回一個以 Type 爲 key,Method 集合爲 Value 的HashMap, 以上面分頁攔截器爲例子, key 是 org.apache.ibatis.executor.Executor, Value 是兩個重載的 query 方法
再看 getAllInterfaces(type, signatureMap)

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

該方法返回根據目標實例 target 和它的父類們的接口數組,回看 Plugin.wrap 方法,若是接口數組長度大於 0,則爲 target 生成代理對象
最後當在 DefaultSqlSession 中執行具體執行時,如 selectList 方法中, 此時的 executor 是剛剛生成的代理對象

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

executor 調用的方法就會執行 Plugin 重寫的 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);
    }
  }

最終,執行自定義攔截器的 intercept 方法,攔截器就是這樣被執行的.
咱們發現,在 Mybatis 框架中,大量的使用了 Java 動態代理,好比只需在 Mapper 接口中定義方法,並無具體的實現類,這一切都是應用 Java 動態代理,因此理解動態代理,能更好的理解整個執行過程.

攔截器註解詳解

本文中截取了分頁攔截器的部分關鍵代碼,看到該攔截器的註解內容是:

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

而在 Mybatis攔截器之數據加密解密中請求參數攔截器和返回結果集攔截器的內容分別是:

@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})

每種攔截器攔截的方法簽名(Signature)都不同,我要怎樣寫呢?其實很簡單,這些都是接口 Executor/ParameterHandler/ResultSetHandler 中的方法,按照相應的接口方法在這裏配置就行了,在經過反射解析攔截器的時候會判斷可否找到相應的方法簽名,若是找不到會報 NoSuchMethodException 異常
舉例來看 Executor 接口,裏面有兩個重載的 query 方法,再回看註解中的內容,是否是豁然開朗呢?

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

結合文章開頭說的鋪墊,攔截器攔截 Configuration 類中的四個方法,沒錯,就是 Executor/ParameterHandler/ResultSetHandler/StatementHandler,繼續回看 Mybatis攔截器之數據加密解密開篇攔截器介紹內容,充分理解 Executor/ParameterHandler/ResultSetHandler/StatementHandler 的做用,咱們就能夠應用攔截器玩出咱們本身的花樣了,

問題彩蛋

咱們看到調用攔截器的時候經過 interceptorChain 進行調用,直譯過來就是 攔截器鏈, 其實這是設計模式之責任鏈模式

  1. 你瞭解責任鏈模式嗎?
  2. 你能想到哪些框架或場景中應用了責任鏈模式嗎?
  3. 現實業務中有哪些地方應用責任鏈模式能讓咱們代碼更靈活健壯呢?
  4. 若是咱們定義多個同類型的攔截器,好比多個 Executor 類型攔截器,那麼多個攔截器的順序要怎樣把控呢?

提升效率工具

關注公衆號,回覆工具獲取更多那些能夠幫助咱們高效工做的工具

Free Mybatis Plugin

咱們在使用 Mybatis 並須要手寫 SQL 時須要在 Mapper 接口中定義方法,同時在 XML 中定義同名 statementId 的 SQL,該Intellij IDEA 插件幫助咱們快速定位方法和 XML,並來回切換:
從 Java 到 SQL
Xnip2019-06-14_13-05-07.jpg

從 SQL 到 Java
Xnip2019-06-14_13-05-34.jpg

推薦閱讀:
程序猿爲何要看源碼


歡迎關注公衆號,趣談coding那些事,提高硬實力
a (1).png

相關文章
相關標籤/搜索