【Mybatis源码分析】插件机制和Pagehelper插件源码分析

分页插件Pagehelper源码分析

  • 一、插件机制
  • 二、Pagehelper源码分析

前文叙述过以下内容:Mybatis对动态代理的使用,一二级缓存和懒加载的原理。其中二级缓存解释了在分布式环境下可能出现缓存不一致问题,但没说解决方案。其实个人认为这种问题除非数据库集群等机制,不然个人认为一个服务大概率就对应的一个持久化层,很少会出现不一致的问题,如果有这边还是建议不使用二级缓存就是了,或者自己写个缓存解决我觉得挺好(没遇到过)。

动态代理的使用(Javassist、CGLIB、JDK动态代理)
Mybatis查询流程(一级、二级缓存、懒加载原理)

Mybatis 除了前面源码分析到的那些核心部分,Mybatis 还提供了一强大的功能,即支持插件机制。Mybatis支持对Executor、StatementHandler、ParameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理,这就是插件的功能。针对插件机制的核心原理和我们常用的Pagehelper源码分析接下来由小编阐述一下。

一、插件机制

关于插件机制的概述,下面这文我觉得解释得很清楚了(看完下面这文的话可以直接跳到下面的Pagehelper源码分析去看)
MyBatis详解 - 插件机制
下面只解释核心部分(像Mybatis解析插件配置等等就不阐述了)

下面是Mybatis解析配置的插件后封装到的拦截器链,这后面插件机制的处理生成代理链的使用类。该拦截器链解析完配置后封装到了熟悉的 Configuration 中。

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

}

咱查看一下 pluginAll 方法在项目中的用法。
【Mybatis源码分析】插件机制和Pagehelper插件源码分析_第1张图片Mybatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,Mybatis 允许使用插件来拦截方法调用包括:

  • Executor(update、query、flushStatement、commit、rollback、getTransaction、close、isClosed)拦截执行器的方法。PageInterceptor 就是对 query 方法进行拦截。
  • ParameterHandler(getParameterObject,setParameters)拦截结果集的处理
  • ResultSetHandler(handlerResults,handleOutputParameters)拦截结果集的处理。
  • StatementHandler(prepare,parameterize、batch、update、query)拦截sql语法构建的处理。

Mybatis 采用责任链的模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出高效的插件。

下面看看 Interceptor 的底层源码,其中 plugin 和 setProperties 是默认方法,注意这个 Plugin.wrap(target,this) 它底层用了动态代理,一个拦截接着一个拦截器代理,然后就产生了一个代理链,看完下面拦截器的源码,咱来看看 Plugin.wrap 源码实现。

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}

下面是对 Plugin 的源码分析,实现了 InvocationHandler 接口。

//这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler
//又是JDK动态代理机制
public class Plugin implements InvocationHandler {

  //目标对象
  private Object target;
  //拦截器
  private Interceptor interceptor;
  //记录需要被拦截的类与方法
  private Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  //一个静态方法,对一个目标对象进行包装,生成代理类。
  public static Object wrap(Object target, Interceptor interceptor) {
    //首先根据interceptor上面定义的注解 获取需要拦截的信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //目标对象的Class
    Class<?> type = target.getClass();
    //返回需要拦截的接口信息
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //如果长度为>0 则返回代理类 否则不做处理
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  //代理对象每次调用的方法
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合
      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)实现类上面的注解获取相关信息
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //获取注解信息
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    //为空则抛出异常
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //获得Signature注解信息
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    //循环注解信息
    for (Signature sig : sigs) {
      //根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合
      Set<Method> methods = signatureMap.get(sig.type());
      //第一次肯定为null 就创建一个并放入signatureMap
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //找到sig.type当中定义的方法 并加入到集合
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

  //根据对象类型与signatureMap获取接口信息
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    //循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    //转换为数组返回
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}

其中核心方法就是 invoke 方法,因为是代理吗,那它最后执行就是这个 invoke 方法咯,所以关注一下这个 invoke 方法,如果执行的方法是咱定义的话会走 Interceptor.interceptor 方法。return interceptor.intercept(new Invocation(target, method, args)); 传的 Invocation 实例可以看见传了 target,method,args 方法。

interceptor.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, boolean autoCommit) {
    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, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

现在的话可以梳理一下了:
interceptorChain.pluginAll 在Mybatis中四处被调用(上面指出了),它上面解释了是一个代理链对象,就是说一个插件就是写一个代理,一个插件的核心在它的 interceptor 方法中,参数是 Invocation 实例对象,从这个实例对象中可以获取 实例对象、参数、和对应的那个方法,然后自定义你想的操作。
代理时它会判断是否是你要执行的那个方法,如果不是的话会直接执行方法,也就是往下一个代理走。

二、Pagehelper源码分析

首先看 PageInterceptor 是对哪个进行拦截,来看看它的注解。可以看见是对 Executor.query 方法进行拦截。

@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}),
        }
)

刚刚说了一个插件的形成就是它的核心方法 intercepor

@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();
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            // dialect 其实就是对Pagehelper实例对象的封装
            // Pagehelper 类是对分页 Page 的一些处理
            // 在每个分页阶段都会去执行 Pagehelper 的一个回调
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //开启debug时,输出触发当前分页执行时的PageHelper调用堆栈
                // 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全
                debugStackTraceLog();
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, 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) {
            			// 去调用 Pagehelper 中的 afterAll,把缓存的 Page 删掉
            			// Pagehelper 把 Page 对象放在 ThreadLocal 中
                dialect.afterAll();
            }
        }
    }

【Mybatis源码分析】插件机制和Pagehelper插件源码分析_第2张图片其中 Page 继承了 ArrayList ,到查询到结果集后,会调用 Pagehelper 中的 afterPage 方法,会对结果集的对象浅克隆到 Page 中(调用 System.arrayCopy 方法)。也就是说咱的 Page 就是结果。

简单看看其 afterPage 的核心代码:
【Mybatis源码分析】插件机制和Pagehelper插件源码分析_第3张图片

大概知道了分页插件的核心原理后,咱来说说其流程吧。

  • 在分页之前,会先去执行查询所有记录条数的sql,然后再判断给的分页参数是否合理什么的;

  • 执行查询所有记录的sql,首先是去看看你有没有设有自己的MappedStatement,其id是id+_COUNT,比如我执行的分页方法是query,那就判断你有没有query_COUNT方法。否则的话Mybatis会为你创一个MappedStatement,这个MappedStatement大部分参数是继承这个查询对应的MappedStatement的参数,比如是否开启二级缓存,那么它会与那个查询对应的MappedStatement公用一个查询 Cache ,即二级缓存。(有时候是应该自己是写COUNT方法,因为比如你用了软删除什么的字段,而它查询的是总数的

  • 二级缓存这里是建议开的,因为如果表很多记录的话,这个查询COUNT的sql执行起来就挺耗时间的。

  • 当然 PageInterceptor 会缓存这生成的 MappedStatement 的,不然反复创建相同的 MappedStatement 影响性能,PageInterceptor 中封装了一个 msCountMap 属性,该属性在 setProperties 方法中进行了实例化,其就是一个 SimpleCache。这样的话如果下次再在执行 count 的sql时,会先从这个缓存中拿这个 MappedStatement,如果没有的话就新建。
    【Mybatis源码分析】插件机制和Pagehelper插件源码分析_第4张图片

  • 然后封装到分页Page数据中,然后去改装原sql为分页sql,即在后面加limit…然后执行这个sql,最后将结果集对象改装为Page对象返回。

这样的话我们得到的结果集就是 Page 了.

if(list instanceOf Page){Page page = (Page)list;}

你可能感兴趣的:(Java源码分析,mybatis,tomcat,java)