# MyBatis源码-插件解析

这一节我们来看看 MyBatis 中的插件是如何配置及源码的实现

# 如何配置一个自定义插件

配置一个 mybatis 自定义插件,一共需要2步,实现 Interceptor 接口 + 在xml 文件把插件配置进去,下边看下具体配置代码:

//实现Interceptor 接口
//配置注解 表明需要拦截的类及拦截的方法
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class LogInterceptor implements Interceptor {
    private String info;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("LogInterceptor");
        // 执行原有方法
        Object result = invocation.proceed();
        // 打印原方法输出结果的数目
        System.out.println(info + ":" + ((List) result).size());
        // 返回原有结果
        return result;
    }

    @Override
    public void setProperties(Properties properties) {
        // 为拦截器设置属性
        info = properties.get("preInfo").toString();
    }
}

xml配置:

    <plugins>
        <plugin interceptor="com.github.yeecode.mybatisdemo.plugin.LogInterceptor1">
            <property name="preInfo" value="LogInterceptor1 本次查询记录数目"/>
        </plugin>
        <plugin interceptor="com.github.yeecode.mybatisdemo.plugin.LogInterceptor2">
            <property name="preInfo" value="LogInterceptor2 本次查询记录数目 : "/>
        </plugin>
    </plugins>

通过上面2 步就简单的配置了一个插件,可以拦截 mybatis 方法执行前后,然后加上自己的业务代码就可以了。

# mybatis 插件的拦截方法

不是所有 mybatis 的类都支持配置插件形式进行拦截,mybatis 有4大接口分别为:

1.Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) MyBatis的执行器,用于执行增删改查操作;
2.ParameterHandler (getParameterObject, setParameters) 处理SQL的参数对象;
3.ResultSetHandler (handleResultSets, handleOutputParameters) 处理SQL的返回结果集;
4.StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理
//type 就是插件拦截的类
//method 就是插件拦截的类的方法
//args 就是方法的参数
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})

# 插件的原理

系统在启动的时候,mybatis 会解析 xml 文件里边的 插件节点,然后把插件对应的类添加到拦截器链中

//XMLConfigBuilder 类
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).newInstance();
          //获取配置的插件参数
        interceptorInstance.setProperties(properties);
          //添加到拦截器链中 自然而然肯定可以添加多个插件
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

好了,上面做的一切就是把插件初始化到系统中,完事具备只待某个节点来触发插件了,下面继续

时光流转,让我们来到 mybatis 的核心配置类来看看

//Configuration 类
//看了下面代码,应该似曾相识,没错就是前面说到过的 插件的拦截方法
//主要是4个类,每个基本上实现都大同小异,只标注这一个方法,其它方法也一样
//参数处理
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    //拿到参数处理器,通过 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);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }
//sql语句处理
  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) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
//增删改查处理
  public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
  }
//InterceptorChain类
public Object pluginAll(Object target) {
    //多个插件依次遍历, target最终就是类似这样的结果,形成了一个链条如:插件1->插件2->插件3
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
//Interceptor 类
default Object plugin(Object target) {
    //返回一个代理
    return Plugin.wrap(target, this);
  }
//Plugin 代理类
//这个就是生成插件代理对象的最终代码!!!
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,
          //target 上一个插件对象  interceptor 当前插件的对象
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

上面代码主要做了:

  1. 解析配置文件插件节点,然后反射生成对象;
  2. 在 mybatis 操作数据库的前后,可以插入我们定义的插件代理对象;
  3. 如果是多个插件,会形式一个代理链条

就拿上面我们配置的插件来说,那是一个对查询结果处理的插件,触发时机在这里:

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
      //通过上面代码,sql语句在数据库已经执行完成,返回了相应的结果集。resultSetHandler其实是一个代理,执行handleResultSets的时候调用的是代理 invoke 方法
    return resultSetHandler.handleResultSets(ps);
  }

image-20241211102131552

上面配置的插件,最后执行结果为:

LogInterceptor2
LogInterceptor1
LogInterceptor1 本次查询记录数目:4
LogInterceptor2 本次查询记录数目:4

先执行到 LogInterceptor2 插件,然后调用 invocation.proceed() 会调用 LogInterceptor1 ,然后 LogInterceptor1 执行完返回结果,LogInterceptor2 拿到结果,执行结束。

img

mybatis 的插件,其实就是一个责任链设计模式,通过对方法拦截然后做层层处理,前提是在 mybats 暴露的4大类中,才能实现插件效果,同理如果设置的插件越多,链条执行就越长,执行时间也就越久。

参考文章:

1.https://www.cnblogs.com/qdhxhz/p/11390778.html

2.https://www.jianshu.com/p/b82d0a95b2f3

3.https://www.rewind.show/2022/12/06/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/MyBatis%E6%BA%90%E7%A0%81/MyBatis%E6%BA%90%E7%A0%81%EF%BC%884%EF%BC%89-%E6%8F%92%E4%BB%B6%E6%9C%BA%E5%88%B6/

上次更新: 2024/12/11