Mybatis插件原理

插件简介

MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实是拦截器的功能,实现方式就是在拦截器的责任链中添加一个Interceptor

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  1. Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) 拦截执行器的方法;
  2. ParameterHandler (getParameterObject, setParameters) 拦截参数的处理;
  3. ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理;
  4. StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理;

注意:括号中是允许拦截的方法。比如Executor.query()等
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

下面是Mybatis的底层设计,其中除了对外暴露的SqlSession插件不拦截,后面的其他流程都可以利用插件进行拦截。
在这里插入图片描述

上图Mybatis框架的整个执行过程。Mybatis插件能够对则四大对象进行拦截,可以包含到了Mybatis一次会议的所有操作。可见Mybatis的的插件很强大。

  1. Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
  2. StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。
  3. ParameterHandler是Mybatis解析处理SQL入参的处理器。插件可以改变我们SQL的参数默认设置。
  4. ResultSetHandler是Mybatis把ResultSet集合映射成POJO的处理器。我们可以定义插件对Mybatis的结果集自动映射进行修改。

插件的使用

要想实现一个自定义插件(Mybatis限制查询数据插件),需要实现Interceptor接口,下面就看下接口定义

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

自定义一个插件只需要实现这个接口,并告诉Mybatis,这个插件拦截的方法是哪个就行了,下面以官网提供的为例:

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

要使插件生效还需要交给容器管理,所以可以在mybatis-config.xml配置中添加如下配置:

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

如果使用的Springboot可以使用注解添加到容器:@Component/@Bean等;

其中@Intercepts声明为一个Mybatis插件,@Signature声明需要拦截的签名,可以有多个。比如上面的例子,就说明要拦截的类是Executor,拦截这个类方法是update,这个方法的参数类型:{MappedStatement.class,Object.class}

源码分析

下面我们分析一下这段代码背后的源码。

首先从源头->配置文件开始分析:

XMLConfigBuilder解析MyBatis全局配置文件mybatis-config.xml的时候回去解析plugins节点

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"));
      // mybatis插件
      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
      // 创建DataSourceFactory
      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);
    }
  }

上面会调用pluginElement(root.evalNode("plugins")) 去解析配置中配置拦截器,并且通过反射实例化,最后添加到configuration.interceptorChain中,具体看下面的代码:

/**
   * 将mybatis插件 添加到拦截器的责任链中
   * @param parent
   * @throws Exception
   */
  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.interceptorChain

public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }

上面看到会解析后 拦截器会添加到InterceptorChain中,下面就看下它是什么?

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;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

到此为止,我们已经将配置中配置的插件(也就是拦截器),解析后通过反射实例,最后放入到了configuration.interceptorChain中。下面就看下为何拦截器会拦截这些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 注意看这里,调用了interceptorChain.pluginAll()
    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);
    // 注意看这里,调用了interceptorChain.pluginAll()
    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);
    // 注意看这里,调用了interceptorChain.pluginAll()
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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, autoCommit);
    }
    // 注意看这里,调用了interceptorChain.pluginAll()
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

以上4个方法都是Configuration的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler
这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,下面就看看pluginAll()方法做了什么?

InterceptorChain

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

可以看到只是遍历List<Interceptor>,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象
最后看一下plugin方法中调用的Plugin.wrap(target, this)

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

这里Plugin实现了InvocationHandler,所以是一个动态代理Handler。wrap方法会生成代理对象,并且把类和方法签名保存到一个Map中。

最后在执行(Executor,ParameterHandler,ResultSetHandler,StatementHandler)方法时 ,就会执行Plugin.invoke(),下面看看invoke的实现:

private final Map<Class<?>, Set<Method>> signatureMap;
@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);
    }
  }

如果方法签名在Map中存在,就会调用interceptor.intercept,也就会执行我们自定义的插件功能。

总结

MyBatis拦截器接口提供的3个方法中,plugin方法用于某些处理器(Handler)的构建过程。interceptor方法用于处理代理类的执行。setProperties方法用于拦截器属性的设置。
mybatis的解析xml的时候会把配置中interceptor插件,保存到configuration.interceptorChain中。并且在实例化拦截的类时(Executor,ParameterHandler,ResultSetHandler,StatementHandler),生成对应的代理对象,并把这些类的签名保存在Map中。最后在执行数据库操作时会执行Plugin.invoke(),如果方法在Map中,就会调用我们自定义的插件。

注意:自定义插件需要谨慎操作,因为如果使用不正确很可能会中断数据库操作,并且还会破坏Mybatis的整体设计。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值