zoukankan      html  css  js  c++  java
  • mybatis 源码分析(五)Interceptor 详解

    本篇博客将主要讲解 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对象
  • 相关阅读:
    gson Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path
    angularjs-$interval使用
    angularjs的页面拆分思想
    桌面工具的分享
    怎么理解高内聚低耦合
    sqlserver 还原数据库
    Sqlserver 读取EXCEL
    Search everything 使用说明
    根据文件大小搜索电脑文件
    一个不错的录屏的软件的分享
  • 原文地址:https://www.cnblogs.com/sanzao/p/11423849.html
Copyright © 2011-2022 走看看