Mybatis拦截器执行过程解析

Mybatis拦截器执行过程解析

上一篇文章 Mybatis拦截器之数据加密解密 介绍了 Mybatis 拦截器的简单使用,这篇文章将透彻的分析 Mybatis 是怎样发现拦截器以及调用拦截器的 intercept 方法的

小伙伴先按照文章内容细致但不入微的了解整个拦截器执行过程,在纸上勾勒出各个点,再细致入微的读源码,将这些点用线串起来,这样站在上帝视角后,理解的更加深刻

发现拦截器

按照官网说明,我们通过实现 org.apache.ibatis.plugin.Interceptor 接口自定义的拦截器,有两种方式将自定义拦截器添加到 Mybatis 的 configuration 中

配置文件方式

mybatis-config.xml 中添加 plugin

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

XMLConfigBuilder 负责解析 Mybatis 全局配置文件,其中有 pluginElement 方法:

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 中

注解方式

文章 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 个非常重要的方法,也是拦截器拦截的方法

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 方法

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 方法

@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, 看下面的类关系图
Mybatis拦截器执行过程解析

在实例化具体的 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
Mybatis拦截器执行过程解析

从 SQL 到 Java
Mybatis拦截器执行过程解析

推荐阅读:
程序猿为什么要看源码


欢迎关注公众号,趣谈coding那些事,提升硬实力
Mybatis拦截器执行过程解析

相关推荐