• 【Mybatis源码分析】插件机制和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);
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    咱查看一下 pluginAll 方法在项目中的用法。
    在这里插入图片描述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
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    下面是对 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()]);
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    其中核心方法就是 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    现在的话可以梳理一下了:
    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}),
            }
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    刚刚说了一个插件的形成就是它的核心方法 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();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    在这里插入图片描述其中 Page 继承了 ArrayList ,到查询到结果集后,会调用 Pagehelper 中的 afterPage 方法,会对结果集的对象浅克隆到 Page 中(调用 System.arrayCopy 方法)。也就是说咱的 Page 就是结果。

    简单看看其 afterPage 的核心代码:
    在这里插入图片描述

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

    • 在分页之前,会先去执行查询所有记录条数的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,如果没有的话就新建。
      在这里插入图片描述

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

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

    if(list instanceOf Page){Page page = (Page)list;}
    
    • 1
  • 相关阅读:
    Nginx反向代理配置
    Flutter 从源码看Getx的依赖原理
    centos7设置开机启动
    单一职责原则
    故障代码表
    新手使用php7的加密方法来保护代码的安全性
    uniapp项目实践总结(二十六)安卓应用商店上架教程
    同轴电缆抗干扰措施(二)
    二.STM32F030C8T6 MCU开发之 NVIC中断配置
    如何处理 Flink 作业中的数据倾斜问题?
  • 原文地址:https://blog.csdn.net/qq_63691275/article/details/132871869