mybatis 源码分析(五)Interceptor 详解

  • 2019 年 10 月 3 日
  • 笔记

本篇博客将主要讲解 mybatis 插件的主要流程,其中主要包括动态代理和责任链的使用;

一、mybatis 拦截器主体结构

在编写 mybatis 插件的时候,首先要实现 Interceptor 接口,然后在 mybatis-conf.xml 中添加插件,

<configuration>    <plugins>      <plugin interceptor="***.interceptor1"/>      <plugin interceptor="***.interceptor2"/>    </plugins>  </configuration>

这里需要注意的是,添加的插件是有顺序的,因为在解析的时候是依次放入 ArrayList 里面,而调用的时候其顺序为:2 > 1 > target > 1 > 2;(插件的顺序可能会影响执行的流程)更加细致的讲解可以参考 QueryInterceptor 规范 ;

然后当插件初始化完成之后,添加插件的流程如下:

首先要注意的是,mybatis 插件的拦截目标有四个,Executor、StatementHandler、ParameterHandler、ResultSetHandler:

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

这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象; 其责任链的添加过程如下:

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

最终动态代理生成和调用的过程都在 Plugin 类中:

public static Object wrap(Object target, Interceptor interceptor) {    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取签名Map    Class<?> type = target.getClass(); // 拦截目标 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  // 获取目标接口    if (interfaces.length > 0) {      return Proxy.newProxyInstance(  // 生成代理          type.getClassLoader(),          interfaces,          new Plugin(target, interceptor, signatureMap));    }    return target;  }

这里所说的签名是指在编写插件的时候,指定的目标接口和方法,例如:

@Intercepts({    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})  })  public class ExamplePlugin implements Interceptor {    public Object intercept(Invocation invocation) throws Throwable {      ...    }  }

这里就指定了拦截 Executor 的具有相应方法的 update、query 方法;注解的代码很简单,大家可以自行查看;然后通过 getSignatureMap 方法反射取出对应的 Method 对象,在通过 getAllInterfaces 方法判断,目标对象是否有对应的方法,有就生成代理对象,没有就直接反对目标对象;

在调用的时候:

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

二、PageHelper 拦截器分析

mybatis 插件我们平时使用最多的就是分页插件了,这里以 PageHelper 为例,其使用方法可以查看相应的文档 如何使用分页插件,因为官方文档讲解的很详细了,我这里就简单补充分页插件需要做哪几件事情;

使用:

PageHelper.startPage(1, 2);  List<User> list = userMapper1.getAll();

PageHelper 还有很多中使用方式,这是最常用的一种,他其实就是在 ThreadLocal 中设置了 Page 对象,能取到就代表需要分页,在分页完成后在移除,这样就不会导致其他方法分页;(PageHelper 使用的其他方法,也是围绕 Page 对象的设置进行的)

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();  public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {    Page<E> page = new Page<E>(pageNum, pageSize, count);    page.setReasonable(reasonable);    page.setPageSizeZero(pageSizeZero);    //当已经执行过orderBy的时候    Page<E> oldPage = getLocalPage();    if (oldPage != null && oldPage.isOrderByOnly()) {      page.setOrderBy(oldPage.getOrderBy());    }    setLocalPage(page);    return page;  }

主要实现:

@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 PageInterceptor implements Interceptor {      @Override    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();        }      }    }  }
  • 首先可以看到拦截的是 Executor 的两个 query 方法(这里的两个方法具体拦截到哪一个受插件顺序影响,最终影响到 cacheKey 和 boundSql 的初始化);
  • 然后使用 checkDialectExists 判断是否支持对应的数据库;
  • 在分页之前需要查询总数,这里会生成相应的 sql 语句以及对应的 MappedStatement 对象,并缓存;
  • 然后拼接分页查询语句,并生成相应的 MappedStatement 对象,同时缓存;
  • 最后查询,查询完成后使用 dialect.afterPage 移除 Page对象