mybatis源码分析05:拦截器

604 阅读4分钟

注:本系列源码分析基于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拦截器相关分析就到这里了。


本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。