MyBatis插件开发学习

905 阅读5分钟

前言:

开发MyBatis 插件时通过学习资料知道mybatis插件是基于拦截器的原理实现,MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的接口及方法调用包括:

  1. Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) 拦截执行器的方法,Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的;

  2. ParameterHandler (getParameterObject, setParameters) 拦截参数的处理,ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置;

  3. ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理,ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改;

  4. StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法,StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。

拦截器之所以会拦截上述四个接口及方法是因为Mybatis源码中的核心类Configuration在实例化上述接口的实现类时都会调用interceptorChain.pluginAll()方法;源码如下:

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

通过上述Mybatis源码段可以看出在实例化四类实例后都会将放进拦截器链中,Mybaitis此处也正式使用责任链的设计模式。会依次执行加入到拦截器链中的拦截器。

简单案例:

假如我们现在需要查看查询所消耗的时间,可以使用拦截器实现此需求;实现方式如下:

首先定义一个拦截器类:CalcQueryTimePlugin 

@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})
      })
public class CalcQueryTimePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
        System.out.println("执行时间为" + (endTime-startTime)/1000 + "秒");
        return  result;
    }
​
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
​
    @Override
    public void setProperties(Properties properties) {
​
    }
}

此类中拦截器注解表明此类只拦截查询方法。执行查询方法时会将查询数据消耗时间打印出来。

然后将拦截器添加到Mytatis的插件集合中,此处需要修改Mybatis的基础配置文件。

<plugins>
    <plugin interceptor="org.ibatis.plugin.CalcQueryTimePlugin"></plugin>
</plugins>

注意:根据Mybaitis配置文件xml的约束条件各配置项的顺序应为:properties,settings,typeAliases,typeHandlers,objectFactory,objectWrapperFactory,reflectorFactory,plugins,environments,databaseIdProvider,mappers),如果不按照此顺序,配置文件会有警告。

到此拦截器已经定义好,只要执行普通查询方法都会经过此拦截器,

遇到的问题及解决方法:

开始学习使用拦截器时启动经常会报以下异常:

org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query.
 Cause: java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds)

出现问题的原因是因为拦截器会根据拦截器类上的拦截器注解动态代理生成相应的代理类,当程序执行该方法是其实是执行一个代理方法,生成代理类时根据被代理类、方法、及参数个数确定被代理的方法,例如在Executor类中定义了两个query方法:

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
​
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

如果在注解中参数类型数组有误则会抛出上述异常。只需要根据需求及源码修改即可。

PageHelper插件:

       在实际开发中,使用的比较多的Mybatis插件有分页插件PageHelper,他拦截了Mybatis执行的查询方法,他会根据传入参数判断是否需要分页,再判断是否需要查询总页数,然后做分页查询,并将结果封装程分页结果返回,源码如下:

public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        //由于逻辑关系,只会进入一次
        if (args.length == 4) {
            //4 个参数时
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 个参数时
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();

        List resultList;
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms, parameter, rowBounds)) {
            //判断是否需要进行 count 查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                //查询总数
                Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    //当查询总数为 0 时,直接返回空的结果
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if(dialect != null){
            dialect.afterAll();
        }
    }
}