MyBatis 插件开发基础原理

⼤多数框架,都⽀持插件,⽤户可通过编写插件来⾃⾏扩展功能。Mybatis 也不例外,我们可以从插件配置、插件编写、插件运⾏原理、插件注册与执⾏拦截的时机、初始化插件、分⻚插件的原理等六个⽅⾯来对 Mybatis 插件进行阐述。

1、插件配置

Mybatis 的插件配置在 configuration 内部,初始化时,会读取这些插件,保存于 Configuration 对象的 InterceptorChain 中。

org.apache.ibatis.session.Configuration 源码如下:

public class Configuration {
  	// 插件
    protected final InterceptorChain interceptorChain = new InterceptorChain();
}

org.apache.ibatis.plugin.InterceptorChain 类定义如下:

public class InterceptorChain {

  // 插件集合
  private final List<Interceptor> interceptors = new ArrayList<>();

  // 目标 target 执行插件
  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);
  }

}

上⾯的for循环代表了只要是插件,就会以责任链的⽅式逐⼀执⾏(别指望它能跳过某个节点),所谓插件,其实就类似于拦截器。

2、插件编写

编写 Mybatis 插件必须实现org.apache.ibatis.plugin.Interceptor接⼝。org.apache.ibatis.plugin.Interceptor接⼝定义如下:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}
  • intercept()⽅法:执⾏拦截内容的地⽅,⽐如加点权限控制什么的。由 plugin() ⽅法触发,在 InterceptorChain 类中的interceptor.plugin(target) ⾜以证明。
  • plugin()⽅法:决定是否触发 intercept() ⽅法。
  • setProperties()⽅法:给⾃定义的拦截器传递 xml 配置的属性参数。

下面我们自定义一个插件:

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

@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}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MyBatisInterceptor implements Interceptor {

    private Integer value;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 这里就可以添加自己的业务逻辑
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        System.out.println(value);
        if (target instanceof Executor || target instanceof StatementHandler) {
            // Plugin类是插件的核⼼类,⽤于给target创建⼀个JDK的动态代理对象,触发intercept()⽅法
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // 自定义 value
        value = Integer.valueOf((String) properties.get("value"));
    }
}

上面是一个简单的插件代码,主要有几个问题:

  • 为什么要写Annotation注解?注解都是什么含义?

Mybatis 规定插件必须编写 Annotation 注解,是必须,⽽不是可选。

@Intercepts注解:装载⼀个 @Signature 列表,⼀个 @Signature 其实就是⼀个需要拦截的⽅法封装。那么,⼀个拦截器要拦截多个⽅法,⾃然就是⼀个 @Signature 列表。

type 含义:要拦截 Executor 接⼝内的 query() ⽅法,参数类型为 args 列表。

type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
  • Plugin.wrap(target, this)是⼲什么的?

使⽤ JDK 的动态代理,给 target 对象创建⼀个 delegate 代理对象,以此来实现⽅法拦截和增强功 能,它会回调 intercept() ⽅法。

org.apache.ibatis.plugin.Plugin 源码分析:

public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  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);
    // 创建JDK动态代理对象
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @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)) {
        // 回调intercept()⽅法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
		// ......
    return signatureMap;
  }

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
		// ......
  }

}
  • signatureMap:缓存需拦截对象的反射结果,避免多次反射,即 target 的反射结 果。所以,我们不要动不动就说反射性能很差,那是因为你没有像 Mybatis ⼀样去缓存⼀个对象的反射结果,而是每次重新生成。
  • methods != null && methods.contains(method):这里判断是否是需要拦截的⽅法,这里很重要,⼀旦忽略了,都不知道Mybatis 是怎么判断是否执⾏拦截内容的。

3、插件运⾏原理

Mybatis 的插件配置在 configuration 内部,要探究其插件运行原理,还是要看 Configuration 类。

org.apache.ibatis.session.Configuration 源码如下:

public class Configuration {

  // ......
  
  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, 
                                                Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = 
      mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 1、注册插件
    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);
    // 2、注册插件
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, 
                                              Object parameterObject, RowBounds rowBounds, 
                                              ResultHandler resultHandler, BoundSql boundSql) {
    // 根据路由规则,设置不同的StatementHandler
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, 
                                                                    rowBounds, resultHandler, boundSql);
    // 3、注册插件
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  
  public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
  }

  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);
    }
    // 4、注册插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
  
   // ......
}
  • 从源码中可以看出 Mybatis 只能拦截 ParameterHandlerResultSetHandlerStatementHandlerExecutor 这4个接⼝对象内的⽅法。

  • 重新审视interceptorChain.pluginAll()⽅法:该⽅法在创建上述4个接⼝对象时调⽤,其含义为给这些接⼝对象注册拦截器功能,注意是注册,⽽不是执⾏拦截。

  • 拦截器执⾏时机:plugin()⽅法注册拦截器后,那么,在执⾏上述4个接⼝对象内的具体⽅法时,就会⾃动触发拦截器的执⾏,也就是插件的执⾏。 所以,⼀定要分清,何时注册,何时执⾏。切不可认为 pluginAll() 或 plugin() 就是执⾏,它只是注册。

4、插件注册与执⾏拦截的时机

前面已经提到,interceptorChain.pluginAll只是注册插件,查看其源码:

public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<>();
  
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
}

这里,对插件集合做循环,然后调用插件的 plugin() 方法:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    // 创建JDK动态代理对象
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}

这里的 intercept 就是执行插件,它需要 Invocation 参数,org.apache.ibatis.plugin.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);
  }

}

org.apache.ibatis.plugin.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)) {
      // 回调intercept()⽅法
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

5、初始化插件

org.apache.ibatis.builder.xml.XMLConfigBuilder#parse 中,初始化了插件。

/**
 * 全局配置文件的解析器
 */
public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 从全局配置文件根节点开始解析,加载的信息设置到Configuration对象中
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

private void parseConfiguration(XNode root) {
  try {
    // issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    // 初始化插件
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } 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();
      // 这⾥就是setProperties()⽅法的调⽤时机
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

对于Mybatis来说,它并不区分是何种拦截器接⼝,所有的插件都是Interceptor。Mybatis完全依靠 Annotation 去标识对谁进⾏拦截,所以,具备接⼝⼀致性。

6、分⻚插件的原理

由于Mybatis采⽤的是逻辑分⻚,⽽⾮物理分⻚。那么,市场上就出现了可以实现物理分⻚的Mybatis的分⻚插件。

要实现物理分⻚,就需要对String sql进⾏拦截并增强,Mybatis通过BoundSql对象存储String sql,⽽ BoundSql 则由StatementHandler 对象获取。

public interface StatementHandler {
	
  // 增强该接口
  // String sql = getBoundSql();
  // 分⻚语句: sql+"limit 语句"
  // 查询总数语句:"SELECT COUNT(1) "" +sql.substring(from语句之后)
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  BoundSql getBoundSql();

}

因此,就需要编写⼀个针对 StatementHandler 的 query ⽅法拦截器,然后获取到sql,对sql进⾏重写增强。

你可能感兴趣的:(源码分析,mybatis,插件)