Skip to content

MyBatis 插件逻辑

litter-fish edited this page Jan 2, 2020 · 3 revisions

插件配置

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。

默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

自定义插件

只需实现 Interceptor 接口,并指定想要拦截的方法签名即可

import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.util.Properties;

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
})
public class MySqlPagingPlugin implements Interceptor {

    private static final Integer MAPPED_STATEMENT_INDEX = 0;
    private static final Integer PARAMETER_INDEX = 1;
    private static final Integer ROW_BOUNDS_INDEX = 2;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rb = (RowBounds) args[ROW_BOUNDS_INDEX];
        // 无需分页
        if (rb == RowBounds.DEFAULT) {
            return invocation.proceed();
        }

        // 将原 RowBounds 参数设为 RowBounds.DEFAULT,关闭 MyBatis 内置的分页机制
        args[ROW_BOUNDS_INDEX] = RowBounds.DEFAULT;

        MappedStatement ms = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
        BoundSql boundSql = ms.getBoundSql(args[PARAMETER_INDEX]);

        // 获取 SQL 语句,拼接 limit 语句
        String sql = boundSql.getSql();
        String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
        sql = sql + " " + limit;

        // 创建一个 StaticSqlSource,并将拼接好的 sql 传入
        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());

        // 通过反射获取并设置 MappedStatement 的 sqlSource 字段
        Field field = MappedStatement.class.getDeclaredField("sqlSource");
        field.setAccessible(true);
        field.set(ms, sqlSource);

        // 执行被拦截方法
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {

    }
}

配置文件

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="MySqlPagingPlugin" />
</plugins>

原理

植入插件和执行插件的时序图:

解析插件配置

public class XMLConfigBuilder extends BaseBuilder {
  // 省略其他代码
  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指定的名称解析为Interceptor类型
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  // 省略其他代码
}

添加插件实例到 interceptors 集合中

public class InterceptorChain {
  // 省略其他代码
  /** 添加插件实例到 interceptors 集合中 */
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  // 省略其他代码
}

植入插件逻辑

植入插件

public class Configuration {
  // 省略其他代码
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 根据 executorType 创建相应的 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;
  }
  // 省略其他代码
}

调用拦截器的 plugin 方法植入相应的插件逻辑

public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    // 遍历拦截器集合
    for (Interceptor interceptor : interceptors) {
      // 调用拦截器的 plugin 方法植入相应的插件逻辑
      target = interceptor.plugin(target);
    }
    return target;
  }
  // 省略其他代码
}

为目标对象生成代理

plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了 InvocationHandler 接口,因此它可以作为参数传给 Proxy 的 newProxyInstance 方法。

// MySqlPagingPlugin
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

为目标类生成代理类

// org/apache/ibatis/plugin/Plugin.java
public static Object wrap(Object target, Interceptor interceptor) {
    /*
     * 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:
     * {
     *     Executor.class : [query, update, commit],
     *     ParameterHandler.class : [getParameterObject, setParameters]
     * }
     */
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 获取目标类实现的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
        // 通过 JDK 动态代理为目标类生成代理类
        return Proxy.newProxyInstance(
                type.getClassLoader(),
                interfaces,
                new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

获取Intercepts注解的value值

// org/apache/ibatis/plugin/Plugin.java
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[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
        // 获取 Signature 注解的 type 属性值
        Set<Method> methods = signatureMap.get(sig.type());
        if (methods == null) {
            methods = new HashSet<Method>();
            signatureMap.put(sig.type(), methods);
        }
        try {
            // 通过 Signature 注解中的 method 和 args 值构造Method实例
            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;
}

获取目标类实现的接口

// org/apache/ibatis/plugin/Plugin.java
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()]);
}

执行插件逻辑

Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑

// org/apache/ibatis/plugin/Plugin.java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        /*
         * 获取被拦截方法列表,比如:
         *    signatureMap.get(Executor.class),可能返回 [query, update, commit]
         */
        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);
    }
}

执行插件逻辑

// MySqlPagingPlugin
public Object intercept(Invocation invocation) throws Throwable {
    // 执行插件逻辑
    // 执行被拦截方法
    return invocation.proceed();
}

调用被拦截的方法

// org/apache/ibatis/plugin/Invocation.java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
    // 调用被拦截的方法
    return method.invoke(target, args);
}

加入插件后的session

Clone this wiki locally