Mybatis源码解析(二):Mybatis 插件原理

342 阅读8分钟

Mybatis源码解析(一):MyBatis 是如何执行的
Mybatis源码解析(二):Mybatis 插件原理
Mybatis源码解析(三):Spring 整合 Mybatis 原理

MyBatis 框架允许用户通过自定义拦截器的方式改变 SQL 的执行行为,例如在 SQL 执行时追加 SQL 分页语法,从而达到简化分页查询的目的。用户自定义的拦截器也被称为 MyBatis 插件,本文就来分析一下 MyBatis 插件的实现原理以及如何开发一个慢SQL日志记录插件。

1 原理:Mybatis 插件源码分析

1.1 Configuration 的工厂方法

MyBatis 的插件实际上就是一个拦截器,Configuration 类中维护了一个 InterceptorChain 的实例,用于存放所有注册的拦截器,用户自定义的插件只能对 MyBatis 中的4 种组件的方法进行拦截,这 4 种组件及方法如下:

  1. Executor: update, query, flushStaterments, commit, rollback, getTransaction, close, isClosed
  2. ParameterHandler: getParameterObject, setParameters
  3. ResultSetHandler: handleResultSets, handleOutputParameters
  4. StatementHandler: prepare, parameterize, batch, update, query

Mybatis源码解析(一):MyBatis 是如何执行的 这篇文章中,提到 Configuration 组件有 3 个作用:

  1. 用于描述 MyBatis 配置信息,项目启动时,MyBatis 的所有配置信息都被转换为 Configuration 对象;
  2. 作为中介者简化 MyBatis 各个组件之间的交互,解决了各个组件错综复杂的调用关系;
  3. 作为 ExecutorParameterHandlerResultSetHandlerStatementHandler 组件的工厂创建这些组件的实例。

MyBatis 使用工厂方法创建 ExecutorParameterHandlerResultSetHandlerStatementHandler 组件的实例,在工厂方法中执行拦截逻辑:

public class Configuration {
    // 拦截器链(用来支持插件的插入)
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        ...
        // 为执行器增加拦截器(插件),以启用各个拦截器的功能
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    
    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);
        // 将 ResultHandler 处理器交给拦截器链进行替换,以便拦截器链中的拦截器能注入行为
        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 = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }  
}

Configuration 类的 newParameterHandler()newResutSetHandler()newStatementHandler()newExecutor() 这些工厂方法中,都调用了 InterceptorChain 对象的 pluginAll() 方法,返回 ParameterHandlerResultSetHandlerStatementHandlerExecutor 对象的代理对象,拦截逻辑都是在代理对象中完成的。

1.2 拦截器链 InterceptorChain

看下 InterceptorChain 类的实现:

public class InterceptorChain {
    // 拦截器链
    private final List<Interceptor> interceptors = new ArrayList<>();

    /**
     * target是支持拦截的几个类的实例。该方法依次向所有拦截器插入这几个类的实例
	 * 如果某个插件真的需要发挥作用,则返回一个代理对象即可。如果不需要发挥作用,则返回原对象即可
     * 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
     *
     * @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
     * @return 用来替换目标对象的对象
     */
    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);
    }

}

InterceptorChain 类中通过一个 List 对象维护所有的拦截器实例,在 InterceptorChainpluginAll() 方法中,会调用所有拦截器实例的 plugin() 方法,该方法返回一个目标对象的代理对象。

1.3 MyBatis 插件公共接口 Interceptor

MyBatis 中所有用户自定义的插件都必须实现 Interceptor 接口:

public interface Interceptor {
  /**
   * 该方法内是拦截器拦截到目标方法时的操作
   * @param invocation 拦截到的目标方法的信息
   * @return 经过拦截器处理后的返回结果
   * @throws Throwable
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * 用返回值替代入参对象,代理其行为
   * 通常情况下,可以调用Plugin的warp方法来完成,因为warp方法能判断目标对象是否需要拦截,并根据判断结果返回相应的对象来替换目标对象
   * @param target MyBatis 传入的支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
   * @return 如果当前拦截器要拦截该实例,则返回该实例的代理;如果不需要拦截该实例,则直接返回该实例本身
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  /**
   * 设置拦截器的属性
   * @param properties 要给拦截器设置的属性
   */
  default void setProperties(Properties properties) {}

Interceptor 接口中定义了3个方法:

  1. intercept() 方法用于定义拦截逻辑,该方法会在目标方法调用时执行;
  2. plugin() 方法用于创建 ExecutorParameterHandlerResultSetHandlerStatementHandler 的代理对象;
  3. setProperties() 方法用于设置插件的属性值。
1.4 Invocation 类封装目标对象与方法

其中,interceptor() 接收一个 Invocation 对象作为参数,Invocation 对象中封装了目标对象的方法及参数信息,Invocation 类的实现代码如下:

// 代表了一个调用的详细信息
public class Invocation {
  // 目标对象
  private final Object target;
  // 目标方法
  private final Method method;
  // 方法参数
  private final Object[] args;

  // 用于执行目标方法的逻辑
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }
}
1.5 代理工具类 Plugin

为了便于用户创建 ExecutorParameterHandlerResultSetHandlerStatementHandler 实例的代理对象,MyBatis 中提供了一个 Plugin 工具类:

public class Plugin implements InvocationHandler {

  // 被代理对象
  private final Object target;
  // 拦截器
  private final Interceptor interceptor;
  // 拦截器要拦截的所有的类,以及类中的方法
  private final Map<Class<?>, Set<Method>> signatureMap;

  /**
   * 根据拦截器的配置来生成一个对象用来替换被代理对象,该方法用于简化动态代理对象的创建
   * @param target 被代理对象
   * @param interceptor 拦截器
   * @return 用来替换被代理对象的对象
   */
  public static Object wrap(Object target, Interceptor interceptor) {
    // 得到拦截器interceptor要拦截的类型与方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 被代理对象的类型
    Class<?> type = target.getClass();
    // 逐级寻找被代理对象类型的父类,将父类中需要被拦截的全部找出
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 只要父类中有一个需要拦截,说明被代理对象是需要拦截的
    if (interfaces.length > 0) {
      // 创建并返回一个代理对象,是Plugin类的实例
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // 直接返回原有被代理对象,这意味着被代理对象的方法不需要被拦截
    return target;
  }
    
  /**
   * 代理对象的拦截方法,当被代理对象中方法被触发时会进入这里
   * @param proxy 代理类
   * @param method 被触发的方法
   * @param args 被触发的方法的参数
   * @return 被触发的方法的返回结果
   * @throws Throwable
   */
  @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 类实现了 InvocationHandler 接口,即采用 JDK 内置的动态代理方式创建代理对象。Plugin 类中维护了 ExecutorParameterHandlerResultSetHandlerStatementHandler 类的实例,以及用户自定义的拦截器实例和拦截器中通过 @Intercepts 注解指定的拦截方法。Plugin 类的 invoke() 方法会在调用目标对象的方法时执行,在 invoke() 方法中首先判断该方法是否被 @Intercepts 注解指定为被拦截的方法,如果是,则调用用户自定义拦截器的 intercept() 方法,并把目标方法信息封裝成 Invocation 对象作为 intercept() 方法的参数。 Plugin 类中还提供了一个静态的 wrap() 方法,该方法用于简化动态代理对象的创建。 @Intercepts 注解用于修饰拦截器类,告诉拦截器要对哪些组件的方法进行拦截,下面是 @Intercepts 注解的一个使用案例 (拦截 Executor 组件的 query() 方法):

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

接下来我们就来了解一下 Plugin 类的 getSignatureMap() 方法解析 @Intercepts 注解的过程,代码如下:

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("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
  }
  // 将Intercepts注解的value信息取出来,是一个 Signature 数组
  Signature[] sigs = interceptsAnnotation.value();
  // 将Signature数组数组放入一个Map中,键为Signature注解的type类型,值为该类型下的方法集合
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
  // 对所有Signature注解进行遍历,把Signature注解指定拦截的组件及方法添加到Map中
  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("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
    }
  }
  return signatureMap;
}

Plugin 类的 getSignatureMap() 方法中,首先获取 @Intercepts 注解,然后获取 @Intercepts 注解中配置的所有 Signature 注解,接着对所有的 Signature 注解信息进行遍历,将 Signature 注解中指定要拦截的组件及方法添加到 Map 对象中,其中 Key 为 ExecutorParameterHandlerResutSetHandlerStatementHandler 对应的 Class 对象,Value 为拦截的所有方法对应的 Method 对象数组。

1.6 自定义 MyBatis 插件一般流程

当我们需要自定义一个 MyBatis 插件时,只需要实现 Interceptor 接口,在 intercept() 方法中编写拦截逻辑,通过 plugin() 方法返回一个动态代理对象:

@Intercepts({
        @Signature(type = xxx.class, method = "xxx", args = {xxx.class, xxx.class, ...})
})
@Component
public class MybatisInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // TODO 自定义拦截逻辑
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
}
1.7 插件代理流程总结

Executor 为例,介绍该组件的代理流程。

SqlSessionMyBatis 中提供的面向用户的操作数据库的接口,而真正执行 SQL 操作的是 Executor 组件,MyBatis 通过工厂模式创建 Executor 实例,Configuration 类中提供了一个 newExecutor() 工厂方法,该方法返回的实际上是一个 Executor 的动态代理对象,其代理流程如下: image.png

  1. SqlSession 中会调用 Configuration 类提供的 newExecutor() 工厂方法创建 Executor 对象;
  2. Configuration 类中通过一个 InterceptorChain 对象维护了用户自定义的拦截器链。newExecutor() 工厂方法中调用 InterceptorChain 对象的 pluginAll() 方法;
  3. InterceptorChain 对象的 pluginAll() 方法中会调用自定义拦截器的 plugin() 方法;
  4. 自定义拦截器的 plugin() 方法是由我们来编写的,通常会调用 Plugin 类的 wrap() 静态方法创建一个代理对象。

2 案例:慢 SQL 日志记录插件开发

2.1 注册自定义插件

mybatis-config.xml 配置文件中注册实现的插件:

<plugins>
    <plugin interceptor="com.example.learn.mybatis.plugin.SlowSqlInterceptor">
        <property name="limitSecond" value="2"/>
    </plugin>
</plugins>
2.1 实现自定义插件

com.example.learn.mybatis.plugin.SlowSqlInterceptor.java

@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class SlowSqlInterceptor implements Interceptor {

    // 执行时间阈值,超过该时间,记录慢 SQL
    private Integer limitSecond;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        long begin = System.currentTimeMillis();
        StatementHandler statementHandler = (StatementHandler) target;
        try {
            // 执行原有逻辑
            return invocation.proceed();
        } finally {
            long end = System.currentTimeMillis();
            // 判断超时
            if ((end - begin) > limitSecond * 1000) {
                // 使用反射工具包装,方便取对象属性 delegate.mappedStatement
                MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
                // MappedStatement 是 MyBatis 中描述 Statement 的对象
                MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
                // 方法名称
                String methodName = mappedStatement.getId();
                // 类型
                String sqlType = mappedStatement.getSqlCommandType().toString();
                // MyBatis 中描述 SQL 文本的对象
                BoundSql boundSql = statementHandler.getBoundSql();
                String sql = boundSql.getSql();
                // 参数map
                Object parameterObject = boundSql.getParameterObject();
                // 参数列表
                List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
                // 格式化sql语句,去除换行符,替换参数
                sql = formatSQL(sql, parameterObject, parameterMappingList);
                // 控制台打印日志
                log.error("执行 SQL:[ {} ], 执行耗时[ {} ms ]", sql, (end - begin));
            }
        }
    }

    private String formatSQL(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {
        if (sql == null || sql.length() == 0) {
            return "";
        }
        // 去除换行符
        sql = sql.replaceAll("[\\s\n ]+", "  ");
        // 替换参数
        Map<String, Object> params = (Map<String, Object>) parameterObject;
        for (ParameterMapping pm : parameterMappingList) {
            if (pm.getMode().name().equals("IN")) {
                sql = sql.replaceFirst("\\?", params.get(pm.getProperty()).toString());
            }
        }
        return sql;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        String limitSecond = (String) properties.get("limitSecond");
        this.limitSecond = Integer.parseInt(limitSecond);
    }
}