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

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

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

配置文件方式
在 mybatis-config.xml 中添加 plugin (Mybatis 将拦截器叫做 plugin)




  
    
  

XMLConfigBuilder.java 负责解析 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 interceptorsProvider,
                              ResourceLoader resourceLoader,
                              ObjectProvider databaseIdProvider,
                              ObjectProvider> configurationCustomizersProvider) {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}

以上构造方法第 7 行 interceptorsProvider.getIfAvailable(); 获取所有注入的 Interceptor,之后在构建 SqlSessionFactory.java 的时候,添加我们的拦截器:

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拦截器执行过程解析_第2张图片

在实例化具体的 StatementHandler 的时候都会先调用父类 BaseStatementHandler 的构造器,在父类的构造器中分别顺序调用了 Configuration 类中的 newParameterHandler (第三步)和 newResultSetHandler (第四步)方法:

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

所以说真正的调用过程是这样的:newExecutor -> ParameterHandler -> ResultSetHandler -> StatementHandler

说了这么多还没有讲到拦截器是怎样被执行的,别急,以上的内容是必要的铺垫,先留有印象,也许有细心的小伙伴已经发现,在 Configuration 类中的那四个方法中,都有相同的一段代码:

interceptorChain.pluginAll(...)

没错,通过名字我们也猜测得到,这是调用拦截器的关键所在,interceptorChain 是 Configuration 类的成员变量,且看 InterceptorChain.java 类:

public class InterceptorChain {

  private final List interceptors = new ArrayList();

  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 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, Set> 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, Set> 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, Set> signatureMap = new HashMap, Set>();
    for (Signature sig : sigs) {
      Set methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet();
        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, Set> signatureMap) {
    Set> interfaces = new HashSet>();
    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 方法,如果 interfaces.length > 0 (接口数组长度大于 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 methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      //拦截器的 intercept 方法执行
      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 方法,再回看注解中的内容,是不是豁然开朗呢?

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

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

结合文章开头说的铺垫,拦截器拦截 Configuration 类中的四个方法,没错,就是 Executor/ParameterHandler/ResultSetHandler/StatementHandler,继续回看 Mybatis拦截器之数据加密解密开篇拦截器介绍内容,充分理解 Executor/ParameterHandler/ResultSetHandler/StatementHandler 的作用,我们就可以应用拦截器玩出我们自己的花样了

问题彩蛋
我们看到调用拦截器的时候通过 interceptorChain 进行调用,直译过来就是 拦截器链, 其实这是设计模式之责任链模式,那么:

你了解责任链模式吗?

你能想到哪些框架或场景中应用了责任链模式吗?

现实业务中有哪些地方应用责任链模式能让我们代码更灵活健壮呢?

如果我们定义多个同类型的拦截器,比如多个 Executor 类型拦截器,那么多个拦截器的顺序要怎样把控呢?

提高效率工具
关注公众号,回复“工具”获取更多那些可以帮助我们高效工作的工具
Free Mybatis Plugin
我们在使用 Mybatis 并需要手写 SQL 时需要在 Mapper 接口中定义方法,同时在 XML 中定义同名 statementId 的 SQL,该Intellij IDEA 插件帮助我们快速定位方法和 XML,并来回切换
从 Java 到 SQL
Mybatis拦截器执行过程解析

从 SQL 到 Java
Mybatis拦截器执行过程解析_第3张图片

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

欢迎留言交流,你有一个思想,我有一个思想, 我们交换后就都有两个思想

tan日拱一兵
转发在看也很赞
喜欢作者