Mybatis插件机制详解

MyBatis 插件机制详解

MyBatis 是现在比较流行的 ORM 框架,得益于它简单易用,虽然增加了开发者的一些操作,但是带来了设计和使用上的灵活,得到广泛的使用。之前的一篇文章MyBatis 初始化之XML解析详解中我们已经知道了 MyBatis 的加载 XML 配置文件和加载 mappers 配置文件的流程,最终都是封装到了 Configuration 对象中。而MyBatis 插件功能也是 MyBatis 的一块重要功能。我们知道其可以在 DAO 层进行拦截,如实现分页、SQL语句执行的性能监控、公共字段统一赋值等功能。但对其内部实现机制,涉及的软件设计模式,编程思想往往没有深入的理解。本文对 MyBatis 加载 XML 配置文件中 plugins 插件流程以及对 MyBatis 插件实现原理进行深入分析,并实现自己的简单分页插件。

MyBatis 插件介绍

MyBatis 插件主要用到的类再 org.apache.ibatis.plugin 包下,如下图所示:

MybatisPlugins

可以看到,叫 MyBatis 拦截器可能更合适些,实际上它就是一个拦截器,使用 JDK 动态代理方式,实现在方法级别上进行拦截。支持拦截的方法有以下几种:

  1. 执行器 Executor(update、query、commit、rollback等方法);
  2. 参数处理器 ParameterHandler(getParameterObject、setParameters方法);
  3. 结果集处理器 ResultSetHandler(handleResultSets、handleOutputParameters等方法);
  4. SQL 语法构建器 StatementHandler(prepare、parameterize、batch、update、query等方法);

MyBatis 插件实现机制

配置信息的加载

插件配置信息也是配置在 XML 配置文件中,所以我们直接从 XMLConfigBuilder 的 parseConfiguration 方法开始,可以看到 pluginElement(root.evalNode("plugins")) 用于解析 XML配置中的 plugins 节点标签。而 pluginElement 方法实现的主要功能就是遍历 plugins 节点中的 plugin 子节点,获取配置的 interceptor 属性为具体的插件实现,并将其添加到 Configuration 中的 interceptorChain中。interceptorChain 是拦截器链,将拦截器存在 interceptors (是一个 Interceptor 数组)中。

private void parseConfiguration(XNode root) {
    try {
        ...
        // 解析<plugins>节点
        pluginElement(root.evalNode("plugins"));
        ...
        // 解析<mappers>节点
        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();
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

至此,插件加载流程结束,相对比较简单,具体流程图如下图所示:

代理对象的生成

Mybatis插件的实现机制主要是基于动态代理实现的,其中最为关键的就是代理对象的生成,所以有必要来了解下这些代理对象是如何生成的。

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);
    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) 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);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

观察源码,发现这些可拦截的类对应的对象生成都是通过InterceptorChain的pluginAll方法来创建的,进一步观察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;
    }
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

遍历所有拦截器,调用拦截器的plugin方法生成代理对象,注意生成代理对象重新赋值给target,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码;

接下来看一下我们在编写拦截器的时候,一个典型的plugin方法实现方式,如下:InvocationHandler

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

拦截逻辑的执行

由于真正去执行Executor、ParameterHandler、ResultSetHandler和StatementHandler类中的方法的对象是代理对象(建议将代理对象转为class文件,反编译查看其结构,帮助理解),所以在执行方法时,首先调用的是Plugin类(实现了InvocationHandler接口)的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)) {
            return interceptor.intercept(new Invocation(target, method, args));
        }
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合;

判断当前方法需不需要执行拦截逻辑,需要的话,执行拦截逻辑方法(即Interceptor接口的intercept方法实现),不需要则直接执行原方法。

可以关注下Interceptor接口的intercept方法实现,一般需要用户自定义实现逻辑,其中有一个重要参数,即Invocation类,通过改参数我们可以获取执行对象,执行方法,以及执行方法上的参数,从而进行各种业务逻辑实现,一般在该方法的最后一句代码都是invocation.proceed()(内部执行method.invoke方法),否则将无法执行下一个拦截器的intercept方法。

以上逻辑对应的时序图如下,这里我们以执行executor对象的query方法为例,且假设有两个拦截器存在:

MyBatis 插件开发简单分页插件

这里以分页插件为例,来了解下一般mybatis插件的编写规则,如下所示:

<plugins>
    <plugin interceptor="com.hycjack.mybatis.pagehelper.PageInterceptor">
        <property name="defaultPageSize" value="10"/>
        <property name="defaultPageIndex" value="1"/>
    </plugin>
</plugins>

主要需要实现三个方法

  • intercept:在此实现自己的拦截逻辑,可从Invocation参数中拿到执行方法的对象,方法,方法参数,从而实现各种业务逻辑, 如下代码所示,从invocation中获取的statementHandler对象即为被代理对象,基于该对象,我们获取到了执行的原始SQL语句,以及prepare方法上的分页参数,并更改SQL语句为新的分页语句,最后调用invocation.proceed()返回结果。

  • plugin:生成代理对象;

  • setProperties:设置一些属性变量;

    @Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
    })
    public class PageInterceptor implements Interceptor {
        /**
         * 默认页码
         */
        private Integer defaultPageIndex;
        /**
         * 默认每页数据条数
         */
        private Integer defaultPageSize;
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            StatementHandler statementHandler = getUnProxyObject(invocation);
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
            String sql = getSql(metaObject);
            if (!checkSelect(sql)) {
                // 不是select语句,进入责任链下一层
                return invocation.proceed();
            }
            BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
            Object parameterObject = boundSql.getParameterObject();
            Page page = getPage(parameterObject);
            if (page == null) {
                // 没有传入page对象,不执行分页处理,进入责任链下一层
                return invocation.proceed();
            }
            // 设置分页默认值
            if (page.getPageNum() == null) {
                page.setPageNum(this.defaultPageIndex);
            }
            if (page.getPageSize() == null) {
                page.setPageSize(this.defaultPageSize);
            }
            // 设置分页总数,数据总数
            setTotalToPage(page, invocation, metaObject, boundSql);
            // 校验分页参数
            checkPage(page);
            return changeSql(invocation, metaObject, boundSql, page);
        }
        @Override
        public Object plugin(Object target) {
            // 生成代理对象
            return Plugin.wrap(target, this);
        }
        @Override
        public void setProperties(Properties properties) {
            // 初始化配置的默认页码,无配置则默认1
            this.defaultPageIndex = Integer.parseInt(properties.getProperty("defaultPageIndex", "1"));
            // 初始化配置的默认数据条数,无配置则默认20
            this.defaultPageSize = Integer.parseInt(properties.getProperty("defaultPageSize", "20"));
        }
    }
    

简单的说,mybatis插件就是对ParameterHandler、ResultSetHandler、StatementHandler、Executor这四个接口上的方法进行拦截,利用JDK动态代理机制,为这些接口的实现类创建代理对象,在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑,所以真正要用好mybatis插件,主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义;

另外,如果配置了多个拦截器的话,会出现层层代理的情况,即代理对象代理了另外一个代理对象,形成一个代理链条,执行的时候,也是层层执行;

  • 不要定义过多的插件,代理嵌套过多,执行方法的时候,比较耗性能;
  • 拦截器实现类的intercept方法里最后不要忘了执行invocation.proceed()方法,否则多个拦截器情况下,执行链条会断掉;
更新时间:2021-01-18 22:00:49

本文由 HycJack 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.hycjack.cn/archives/mybatis插件机制详解
最后更新:2021-01-18 22:00:49

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×