Mybatis学习系列(十二):PageHelper分页插件

由于mybatis原生的分页是在内存里面进行的,导致效率很低,但是我们在生产项目中有很多的分页需求,这个时候PageHelper分页插件就诞生了。PageHelper主要是通过插件拦截链实现的。

我们知道在创建StatementHandler 的时候,我们包装了interceptorChain链

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 Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

那这里如何初始化interceptors呢?

我们知道在解析配置文件的时候有一个解析plugin的方法:pluginElement(root.evalNode("plugins"));

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      //解析每一个插件拦截器
      for (XNode child : parent.getChildren()) {
        //获取配置信息
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //添加参数
        interceptorInstance.setProperties(properties);
        //添加插件拦截器到configuration中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

//添加参数  这里看PageInterceptor
public void setProperties(Properties properties) {
        //缓存 count ms
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            //这里如果没有配置dialect  走默认的com.github.pagehelper.PageHelper
            dialectClass = default_dialect_class;
        }
        try {
            Class<?> aClass = Class.forName(dialectClass);
            dialect = (Dialect) aClass.newInstance();
        } catch (Exception e) {
            throw new PageException(e);
        }
        dialect.setProperties(properties);

        String countSuffix = properties.getProperty("countSuffix");
        if (StringUtil.isNotEmpty(countSuffix)) {
            this.countSuffix = countSuffix;
        }

        try {
            //反射获取 BoundSql 中的 additionalParameters 属性
            additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException e) {
            throw new PageException(e);
        }
    }


//添加插件拦截器到configuration中
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }

然后我们看一下pagehelper是如何实现的

@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

首先我们发现这里必须是实现了Interceptor接口,这样才可以配置在plugin标签里

然后我们看一下interceptor.plugin(target);方法

public Object plugin(Object target) {
        //TODO Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化,因此考虑在这个方法中做一次判断和初始化
        //TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26
        return Plugin.wrap(target, this);
    }


public static Object wrap(Object target, Interceptor interceptor) {
    //这里解析注解数据
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    //这里获取当前类是否实现了接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //实现了接口  这里生成代理对象 处理handler为Plugin对象
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

//这里解析注解数据
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //获取当前Intercepts注解的信息
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      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>>();
    //循环所有的Signature
    for (Signature sig : sigs) {
      //当前Signature 是否有对应的methods 
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //获取配置的方法信息 包括类和方法
        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;
  }

这里我们可以看到包装在原始executor上面的是一个代理对象,最终执行的时候是交给Plugin的invoke方法。

 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)) {
        //这里调用的是当前的拦截interceptor (这里是我们的PageInterceptor)
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //直接执行当前executor的方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }


//PageInterceptor.intercept
//这里配置了两个方法,4个参数和6个参数的
public Object intercept(Invocation invocation) throws Throwable {
        try {
            //获取所有的入参
            Object[] args = invocation.getArgs();
            //第一个参数是MappedStatement 
            MappedStatement ms = (MappedStatement) args[0];
            //第二个入参是方法的参数
            Object parameter = args[1];
            //第三个参数是分页参数
            RowBounds rowBounds = (RowBounds) args[2];
            //第四个参数是ResultHandler 
            ResultHandler resultHandler = (ResultHandler) args[3];
            //获取执行器Executor 
            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];
            }
            List resultList;
            //1.调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //反射获取动态参数
                String msId = ms.getId();
                Configuration configuration = ms.getConfiguration();
                Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
                //2.判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    String countMsId = msId + countSuffix;
                    Long count;
                    //先判断是否存在手写的 count 查询
                    MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
                    if(countMs != null){
                        count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
                    } else {
                        countMs = msCountMap.get(countMsId);
                        //自动创建
                        if (countMs == null) {
                            //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                            countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                            msCountMap.put(countMsId, countMs);
                        }
                        count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
                    }
                    //处理查询总数
                    //3.返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                //4.判断是否需要进行分页查询
                if (dialect.beforePage(ms, parameter, rowBounds)) {
                    //生成分页的缓存 key
                    CacheKey pageKey = cacheKey;
                    //处理参数对象
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    //5.调用方言获取分页 sql
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                    //设置动态参数
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //执行分页查询
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
                } else {
                    //不执行分页的情况下,也不执行内存分页
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                }
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            //6.结果处理
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }

1.调用方法判断是否需要进行分页,这里走的是默认dialect:com.github.pagehelper.PageHelper

public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        if(ms.getId().endsWith(MSUtils.COUNT)){
            throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
        }
        //获取分页信息
        Page page = pageParams.getPage(parameterObject, rowBounds);
        //没有分页信息  跳过分页 不执行分页
        if (page == null) {
            return true;
        } else {
            //设置默认的 count 列
            if(StringUtil.isEmpty(page.getCountColumn())){
                page.setCountColumn(pageParams.getCountColumn());
            }
            autoDialect.initDelegateDialect(ms);
            return false;
        }
    }


//获取分页信息
public Page getPage(Object parameterObject, RowBounds rowBounds) {
        //从当前threadLocal中获取分页信息
        Page page = PageHelper.getLocalPage();
        //如果当前没有分页信息
        if (page == null) {
            //这里mybatis的分页数据不为默认值
            if (rowBounds != RowBounds.DEFAULT) {
                //如果设置了RowBounds参数offset作为PageNum使用
                if (offsetAsPageNum) {
                    //生成分页信息
                    page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
                } else {
                    //没有开启RowBounds参数offset作为PageNum使用
                    //根据RowBounds参数生成分页信息
                    page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
                    //offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
                    page.setReasonable(false);
                }
                if(rowBounds instanceof PageRowBounds){
                    PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
                    page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
                }
            } else if(supportMethodsArguments){
                //mybatis的分页为默认值  然后开启了接口参数来传递分页参数
                try {
                    //从参数中生成分页信息
                    page = PageObjectUtil.getPageFromObject(parameterObject, false);
                } catch (Exception e) {
                    return null;
                }
            }
            if(page == null){
                return null;
            }
            //把当前分页信息放到threadLocal中
            PageHelper.setLocalPage(page);
        }
        //分页合理化
        if (page.getReasonable() == null) {
            page.setReasonable(reasonable);
        }
        //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
        if (page.getPageSizeZero() == null) {
            page.setPageSizeZero(pageSizeZero);
        }
        return page;
    }

2.判断是否需要进行 count 查询

public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        //这里autoDialect的Delegate实例  是我们设置的mysql
        return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
    }

public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        //获取当前分页参数
        Page page = getLocalPage();
        //这里判断是否只增加排序 而且需要count查询
        return !page.isOrderByOnly() && page.isCount();
    }

3.处理查询总数

public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
        //获取分页信息
        Page page = getLocalPage();
        //设置sql总数
        page.setTotal(count);
        if (rowBounds instanceof PageRowBounds) {
            ((PageRowBounds) rowBounds).setTotal(count);
        }
        //pageSize < 0 的时候,不执行分页查询
        //pageSize = 0 的时候,还需要执行后续查询,但是不会分页
        if (page.getPageSize() < 0) {
            return false;
        }
        return count > 0;
    }

4.是否进行分页查询

public boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        //获取当前分页信息
        Page page = getLocalPage();
        //只是排序  或者每页大小>0
        if (page.isOrderByOnly() || page.getPageSize() > 0) {
            return true;
        }
        return false;
    }

5.调用方言获取分页 sql

public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        //拿到原始SQL
        String sql = boundSql.getSql();
        //获取分页信息
        Page page = getLocalPage();
        //支持 order by
        //获取order by信息
        String orderBy = page.getOrderBy();
        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            //这里添加order by语句
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        //如果只是orderby 直接返回sql
        if (page.isOrderByOnly()) {
            return sql;
        }
        //组装分页sql
        return getPageSql(sql, page, pageKey);
    }

//这里添加order by语句
public static String converToOrderBySql(String sql, String orderBy) {
        //解析SQL
        Statement stmt = null;
        try {
            stmt = CCJSqlParserUtil.parse(sql);
            Select select = (Select) stmt;
            SelectBody selectBody = select.getSelectBody();
            //处理body-去最外层order by
            List<OrderByElement> orderByElements = extraOrderBy(selectBody);
            String defaultOrderBy = PlainSelect.orderByToString(orderByElements);
            if (defaultOrderBy.indexOf('?') != -1) {
                throw new RuntimeException("原SQL[" + sql + "]中的order by包含参数,因此不能使用OrderBy插件进行修改!");
            }
            //新的sql
            sql = select.toString();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return sql + " order by " + orderBy;
    }

//组装分页sql  mysql
public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        //如果开始下标为0 直接取 n条数据
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ? ");
        } else {
            //组装开始条数  和获取条数
            sqlBuilder.append(" LIMIT ?, ? ");
        }
        //更新缓存key
        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }

6.结果处理

public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
        //这个方法即使不分页也会被执行,所以要判断 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if(delegate != null){
            //这里走配置的数据库方言 我们这里是mysql
            return delegate.afterPage(pageList, parameterObject, rowBounds);
        }
        return pageList;
    }


//这里走配置的数据库方言 我们这里是mysql
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
        //获取分页信息
        Page page = getLocalPage();
        //如果为空  直接返回当前数据
        if (page == null) {
            return pageList;
        }
        //当前数据放到分页对象
        page.addAll(pageList);
        //没有查询总数量  设置总数量为-1  
        if (!page.isCount()) {
            page.setTotal(-1);
        } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
            page.setTotal(pageList.size());
        } else if(page.isOrderByOnly()){
            //如果只是 orderby 设置总数为 当前数据条数
            page.setTotal(pageList.size());
        }
        return page;
    }

总结:这里的分页插件主要是通过实现mybatis内部的Interceptor接口,然后通过@Intercepts和@Signature注解在确定当前插件在什么时候,什么方法上执行。


版权声明:本文为jadebai原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。