• Pagehelper分页插件的原理


    出自

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


       com.github.pagehelper
       pagehelper
       4.1.6


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

    @ResponseBody
    @RequestMapping("/testPage")
    public String testPage(HttpServletRequest request) {
       Page page = PageUtil.pageStart(request);
       List list = prodMapper.selectAll();
       JSONObject rst = PageUtil.pageEnd(request, page, list);
       return rst.toString();
    }
    这是controller的代码,当时就产生一个疑问,为啥在这个pageStart后面查询就能够实现分页呢?可以查出想要的10条分页数据,而去掉则是全部查询出来的84条记录。

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

    /**
     * 开始分页
     *
     * @param pageNum      页码
     * @param pageSize     每页显示数量
     * @param count        是否进行count查询
     * @param reasonable   分页合理化,null时用默认配置
     * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
     */
    public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page page = new Page(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        //当已经执行过orderBy的时候
        Page oldPage = SqlUtil.getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        SqlUtil.setLocalPage(page);
        return page;
    }

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

    1. public static Page getLocalPage() {
    2.     return LOCAL_PAGE.get();
    3. }
    4. public static void setLocalPage(Page page) {
    5.     LOCAL_PAGE.set(page);
    6. }
    7. private static final ThreadLocal LOCAL_PAGE = new ThreadLocal();

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

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

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

    1. <select id="selectAll" resultMap="BaseResultMap">
    2.   select SEQ_ID, PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESC, CREATE_TIME, EFFECT_TIME, 
    3.   EXPIRE_TIME, PRODUCT_STATUS, PROVINCE_CODE, REGION_CODE, CHANGE_TIME, OP_OPERATOR_ID, 
    4.   PRODUCT_SYSTEM, PRODUCT_CODE
    5.   from PM_PRODUCT
    6. 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 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 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用的好不好,对看框架源码很有很大的影响。
     

  • 相关阅读:
    AI前沿-YOLOV9算法
    前端基础之《ECMAScript 6(7)—异步技术》
    ECMAScript 6 新特性详解
    【Java实战】大厂都是怎样进行单元测试的
    Mysql 我随手造200W条数据,给你们讲讲分页优化。
    从零开始设计一个共识算法——一场没有硝烟的战争
    uni-app框架
    【大模型应用开发教程】动手学大模型应用开发,一起探索LLM Universe
    Python学习之——环境配置
    浅谈支付系统的业务精细化监控
  • 原文地址:https://blog.csdn.net/weixin_47188125/article/details/126973242