专栏首页Java编程指南只知道用是不行的!浅析pagehelper分页原理

只知道用是不行的!浅析pagehelper分页原理

之前项目一直使用的是普元框架,最近公司项目搭建了新框架,主要是由公司的大佬搭建的,以springboot为基础。为了多学习点东西,我也模仿他搭了一套自己的框架,但是在完成分页功能的时候,确遇到了问题。

框架的分页组件使用的是pagehelper,对其我也是早有耳闻,但是也是第一次接触(ps:工作1年,一直使用的是普元封装好的前端框架)。

要是用pagehelper,首先maven项目,要引入

<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>4.1.6</version>
</dependency>

前端使用的bootstrap分页插件,这里不再赘述,直接切入正题,持久层框架使用的是mybatis,分页插件的URL,指向后台的controller,

@ResponseBody
@RequestMapping("/testPage")
public String testPage(HttpServletRequest request) {
   Page<PmProduct> page = PageUtil.pageStart(request);
   List<PmProduct> list = prodMapper.selectAll();
   JSONObject rst = PageUtil.pageEnd(request, page, list);
   return rst.toString();
}

这是controller的代码,当时就产生一个疑问,为啥在这个pageStart后面查询就能够实现分页呢?可以查出想要的10条分页数据,而去掉则是全部查询出来的84条记录。

带着问题,我开始了我的debug之旅,首先,我跟进pageStart方法,这个PageUtil类是公司大佬封装的一个工具类,代码如下:

public class PageUtil {


    public enum CNT{
        total,
        res
    }


    public static <E> Page<E> pageStart(HttpServletRequest request){
        return pageStart(request,null);
    }


    /**
     *
     * @param request
     * @param pageSize 每页显示条数
     * @param orderBy 写入 需要排序的 字段名 如:product_id desc
     * @return
     */
    public static <E> Page<E> pageStart(HttpServletRequest request,String orderBy){


        int pageon=getPageon(request);
        int pageSize=getpageSize(request);
        Page<E> page=PageHelper.startPage(pageon, pageSize);
        if(!StringUtils.isEmpty(orderBy)){
            PageHelper.orderBy(orderBy);
        }


        return page;
    }


    private static int getPageon(HttpServletRequest request){
        String pageonStr=request.getParameter("pageon");
        int pageon;
        if(StringUtils.isEmpty(pageonStr)){
            pageon=1;
        }else{
            pageon=Integer.parseInt(pageonStr);
        }
        return pageon;
    }


    private static int getpageSize(HttpServletRequest request){
        String pageSizeStr=request.getParameter("pageSize");
        int pageSize;
        if(StringUtils.isEmpty(pageSizeStr)){
            pageSize=1;
        }else{
            pageSize=Integer.parseInt(pageSizeStr);
        }
        return pageSize;
    }


    /**
     *
     * @param request
     * @param page
     * @param list
     * @param elName 页面显示所引用的变量名
     */
    public static JSONObject pageEnd(HttpServletRequest request, Page<?> page,List<?> list){
        JSONObject rstPage=new JSONObject();
        rstPage.put(CNT.total.toString(), page.getTotal());
        rstPage.put(CNT.res.toString(), list);
        return rstPage;
    }




}

可以看到,pageStart有两个方法重载,进入方法后,获取了前端页面传递的pageon、pageSize两个参数,分别表示当前页面和每页显示多少条,然后调用了PageHelper.startPage,接着跟进此方法,发现也是一对方法重载,没关系,往下看

/**
 * 开始分页
 *
 * @param pageNum      页码
 * @param pageSize     每页显示数量
 * @param count        是否进行count查询
 * @param reasonable   分页合理化,null时用默认配置
 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
 */
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 = SqlUtil.getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    SqlUtil.setLocalPage(page);
    return page;
}

上面的方法才是真正分页调用的地方,原来是对传入参数的赋值,赋给Page这个类,继续,发现getLocalPage和setLoaclPage这两个方法,很可疑,跟进,看看他到底做了啥,

public static <T> Page<T> getLocalPage() {
    return LOCAL_PAGE.get();
}


public static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

哦,有点明白了,原来就是赋值保存到本地线程变量里面,这个ThreadLocal是何方神圣,居然有这么厉害,所以查阅了相关博客,链接:https://blog.csdn.net/u013521220/article/details/73604917,个人简单理解大概意思是这个类有4个方法,set,get,remove,initialValue,可以使每个线程独立开来,参数互不影响,里面保存当前线程的变量副本。

OK,那这个地方就是保存了当前分页线程的Page参数的变量。有赋值就有取值,那么在下面的分页过程中,肯定在哪边取到了这个threadLocal的page参数。

好,执行完startPage,下面就是执行了mybatis的SQL语句,

<select id="selectAll" resultMap="BaseResultMap">
  select SEQ_ID, PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESC, CREATE_TIME, EFFECT_TIME, 
  EXPIRE_TIME, PRODUCT_STATUS, PROVINCE_CODE, REGION_CODE, CHANGE_TIME, OP_OPERATOR_ID, 
  PRODUCT_SYSTEM, PRODUCT_CODE
  from PM_PRODUCT
</select>

SQL语句很简单,就是简单的查询出PM_PRODUCT的全部记录,那到底是哪边做了拦截吗?带着这个疑问,我跟进了代码,

发现进入了mybatis的MapperPoxy这个代理类,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if (Object.class.equals(method.getDeclaringClass())) {
    try {
      return method.invoke(this, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}
最后执行的execute方法,再次跟进,进入MapperMethod这个类的execute方法,


public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  if (SqlCommandType.INSERT == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.insert(command.getName(), param));
  } else if (SqlCommandType.UPDATE == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.update(command.getName(), param));
  } else if (SqlCommandType.DELETE == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.delete(command.getName(), param));
  } else if (SqlCommandType.SELECT == command.getType()) {
    if (method.returnsVoid() && method.hasResultHandler()) {
      executeWithResultHandler(sqlSession, args);
      result = null;
    } else if (method.returnsMany()) {
      result = executeForMany(sqlSession, args);
    } else if (method.returnsMap()) {
      result = executeForMap(sqlSession, args);
    } else if (method.returnsCursor()) {
      result = executeForCursor(sqlSession, args);
    } else {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = sqlSession.selectOne(command.getName(), param);
    }
  } else if (SqlCommandType.FLUSH == command.getType()) {
      result = sqlSession.flushStatements();
  } else {
    throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName() 
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

由于执行的是select操作,并且查询出多条,所以就到了executeForMany这个方法中,后面继续跟进代码SqlSessionTemplate,DefaultSqlSession(不再赘述),最后可以看到代码进入了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)) {
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

这下明白了,interceptor是mybatis的拦截器,而PageHelper这个类就实现了interceptor接口,调用其中的intercept方法。

/**
 * Mybatis拦截器方法
 *
 * @param invocation 拦截器入参
 * @return 返回执行结果
 * @throws Throwable 抛出异常
 */
public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) {
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}
/**
 * Mybatis拦截器方法
 *
 * @param invocation 拦截器入参
 * @return 返回执行结果
 * @throws Throwable 抛出异常
 */
private Object _processPage(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    Page page = null;
    //支持方法参数时,会先尝试获取Page
    if (supportMethodsArguments) {
        page = getPage(args);
    }
    //分页信息
    RowBounds rowBounds = (RowBounds) args[2];
    //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
    if ((supportMethodsArguments && page == null)
            //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
            || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
        return invocation.proceed();
    } else {
        //不支持分页参数时,page==null,这里需要获取
        if (!supportMethodsArguments && page == null) {
            page = getPage(args);
        }
        return doProcessPage(invocation, page, args);
    }
}

最终我在SqlUtil中的_processPage方法中找到了,getPage这句话,getLocalPage就将保存在ThreadLocal中的Page变量取了出来,这下一切一目了然了,

跟进代码,发现进入了doProcessPage方法,通过反射机制,首先查询出数据总数量,然后进行分页SQL的拼装,MappedStatement的getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }


  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }


  return boundSql;
}

继续,跟进代码,发现,最终分页的查询,调到了PageStaticSqlSource类的getPageBoundSql中,

protected BoundSql getPageBoundSql(Object parameterObject) {
    String tempSql = sql;
    String orderBy = PageHelper.getOrderBy();
    if (orderBy != null) {
        tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
    }
    tempSql = localParser.get().getPageSql(tempSql);
    return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
}

进入getPageSql这个方法,发现,进入了OracleParser类中(还有很多其他的Parser,适用于不同的数据库),

public String getPageSql(String sql) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
    sqlBuilder.append("select * from ( select tmp_page.*, rownum row_id from ( ");
    sqlBuilder.append(sql);
    sqlBuilder.append(" ) tmp_page where rownum <= ? ) where row_id > ?");
    return sqlBuilder.toString();
}

终于,原来分页的SQL是在这里拼装起来的。

总结:PageHelper首先将前端传递的参数保存到page这个对象中,接着将page的副本存放入ThreadLoacl中,这样可以保证分页的时候,参数互不影响,接着利用了mybatis提供的拦截器,取得ThreadLocal的值,重新拼装分页SQL,完成分页。

PS:DEBUG用的好不好,对看框架源码很有很大的影响。

本文分享自微信公众号 - Java编程指南(JavaXxzyfx),作者:Java编程指南

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 这可能是把SpringBoot配置文件讲解的最详细的文章了

    SpringBoot使用一个全局的配置文件,配置文件名是固定的,配置文件主要分为以下两种

    Java编程指南
  • 安全框架Shiro和SpringSecurity的比较

    首先Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。

    Java编程指南
  • 工作了几年还不知道Java虚拟机?(图文详解JVM)

    Java编程指南
  • SSM框架——实现分页和搜索分页

    分页是Java Web项目常用的功能,昨天在Spring MVC中实现了简单的分页操作和搜索分页,在此记录一下。使用的框架为(MyBatis+SpringMVC...

    Java团长
  • FRM 数量分析 区间估计与假设检验

    这里开始就是数理统计的东西了,没有接触过的童鞋可能会觉得有点不能接受,其实接触多了也就熟悉了。个人有这样一种感觉,假设检验和区间估计就是概率的概率,或者说,就是...

    钱塘小甲子
  • 关于ListView的那些坑

    做过Android的同学应该都有适配安卓兼容性的问题,今天我们来说一个常见却又经常被忽略的问题。

    PhoenixZheng
  • 数据可视化(3)-Seaborn系列 | 折线图lineplot()

    案例代码已上传:Githubhttps://github.com/Vambooo/SeabornCN

    数据分析可视化
  • 数据防泄漏 | 禁止PrintScreen键

    在数据防泄漏软件,通常会禁止 PrintScreen 键,防止通过截屏来将数据保存为图片而导致泄密。

    码农UP2U
  • uniapp存放json格式的数据

    在做前端开发的时候,少不了要用一些模拟的json的数据来进行测试,在没有拿到后端开发小伙伴的接口之前,就先尝试着自己写一下json数据吧,前面有说过,使用moc...

    王小婷
  • 一场官司曝光了Waymo激光雷达技术细节

    陈桦 允中 编译整理 量子位 报道 | 公众号 QbitAI Waymo/谷歌无人车与Uber/Otto之间漫长的专利诉讼终于告一段落,上周五,Otto联合创始...

    量子位

扫码关注云+社区

领取腾讯云代金券