使用的插件有

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper-spring-boot-starter</artifactId>
  <version>1.2.13</version>
</dependency>

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.3.1</version>
</dependency>

控制器中使用下面语句就能分页。

startPage();
  List<SysUser> list = userService.selectUserList(user);
  return getDataTable(list);

方法startPage,有什么用呢?

protected void startPage()
{
    PageDomain pageDomain = TableSupport.buildPageRequest();
    Integer pageNum = pageDomain.getPageNum();
    Integer pageSize = pageDomain.getPageSize();
    if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
    {
        String orderBy = pageDomain.getOrderBy();
        PageHelper.startPage(pageNum, pageSize, orderBy);
    }
}

startPage首先将前台传递过来的分页信息记录下来。

public static PageDomain getPageDomain()
{
    PageDomain pageDomain = new PageDomain();
    pageDomain.setPageNum(ServletUtils.getParameterToInt(Constants.PAGE_NUM));
    pageDomain.setPageSize(ServletUtils.getParameterToInt(Constants.PAGE_SIZE));
    pageDomain.setOrderByColumn(ServletUtils.getParameter(Constants.ORDER_BY_COLUMN));
    pageDomain.setIsAsc(ServletUtils.getParameter(Constants.IS_ASC));
    return pageDomain;
}

public static PageDomain buildPageRequest()
{
    return getPageDomain();
}

那么PageHelper.startPage(pageNum, pageSize, orderBy)是什么呢?

/**
  * 开始分页
  *
  * @param pageNum  页码
  * @param pageSize 每页显示数量
  * @param orderBy  排序
  */
public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
    Page<E> page = startPage(pageNum, pageSize);
    page.setOrderBy(orderBy);
    return page;
}
···
/**
  * 开始分页
  *
  * @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 = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    setLocalPage(page);
    return page;
}

其中getLocalPage()和setLocalPage();很奇怪:

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;

/**
  * 设置 Page 参数
  *
  * @param page
  */
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

/**
  * 获取 Page 参数
  *
  * @return
  */
public static <T> Page<T> getLocalPage() {
    return LOCAL_PAGE.get();
}

原来setLocalPage是将page保存到本地线程变量里面。那么有赋值,就该有取值,不然就没什么用了,对吧,但是在哪里用呢?先跳过这里。回到selectUserList。
执行完startPage,就执行了mybatis的sql语句:

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.phonenumber, u.password, u.sex, u.avatar, u.salt, u.status, u.order_num, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.update_time, u.remark
from sys_user u
where u.del_flag = '0'
group by u.user_id
order by u.order_num, u.user_name, d.order_num
</select>

就是这么简单的sql语句,为甚页面会分页呢?难道有地方进行了拦截处理?

debug发现进入com.baomidou.mybatisplus.core.override的MybatisMapperProxy类中

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
          if (Object.class.equals(method.getDeclaringClass())) {
              return method.invoke(this, args);
          } else if (method.isDefault()) {
              if (privateLookupInMethod == null) {
                  return invokeDefaultMethodJava8(proxy, method, args);
              } else {
                  return invokeDefaultMethodJava9(proxy, method, args);
              }
          }
      } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
      }
      final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
      return mapperMethod.execute(sqlSession, args);
  }

最后执行execute方法,接着进入MybatisMapperMethod类中:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
      case INSERT: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.insert(command.getName(), param));
          break;
      }
      case UPDATE: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.update(command.getName(), param));
          break;
      }
      case DELETE: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.delete(command.getName(), param));
          break;
      }
      case SELECT:
          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);
              if (IPage.class.isAssignableFrom(method.getReturnType())) {
                  assert args != null;
                  IPage<?> page = null;
                  for (Object arg : args) {
                      if (arg instanceof IPage) {
                          page = (IPage) arg;
                          break;
                      }
                  }
                  assert page != null;
                  result = executeForIPage(sqlSession, args);
                  if (result instanceof PageList) {
                      PageList pageList = (PageList) result;
                      page.setRecords(pageList.getRecords());
                      page.setTotal(pageList.getTotal());
                      result = page;
                  } else {
                      List list = (List<Object>) result;
                      result = page.setRecords(list);
                  }
              } else {
                  result = sqlSession.selectOne(command.getName(), param);
                  if (method.returnsOptional()
                      && (result == null || !method.getReturnType().equals(result.getClass()))) {
                      result = Optional.ofNullable(result);
                  }
              }
          }
          break;
      case FLUSH:
          result = sqlSession.flushStatements();
          break;
      default:
          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,所以到了 result = executeForMany(sqlSession, args)
然后是SqlSessionTemplate和DefaultSqlSession
最后可以看到代码进入了org.apache.ibatis.plugin.Plugin这个类的invoke方法中

@Override
  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的拦截器,而com.github.pagehelper.PageInterceptor这个类就实现了interceptor接口,调用其中的intercept方法。

@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();

        List resultList;
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms, parameter, rowBounds)) {
            //判断是否需要进行 count 查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                //查询总数
                Long count = count(executor, ms, parameter, rowBounds, resultHandler, 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){
            dialect.afterAll();//这里将销毁threadlocal
        }
    }
}

ExecutorUtil.pageQuery()来处理分页,但是怎么处理的呢?
com.github.pagehelper.util.ExecutorUtil

public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
    //判断是否需要进行分页查询
    if (dialect.beforePage(ms, parameter, rowBounds)) {
        //生成分页的缓存 key
        CacheKey pageKey = cacheKey;
        //处理参数对象
        parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
        //调用方言获取分页 sql
        String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        //设置动态参数
        for (String key : additionalParameters.keySet()) {
            pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        //执行分页查询
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
    } else {
        //不执行分页的情况下,也不执行内存分页
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
    }
}

getPageSql方法:
com.github.pagehelper.PageHelper

@Override
  public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
      return autoDialect.getDelegate().getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
  }

然后调用com.github.pagehelper.dialect.AbstractHelperDialect

@Override
  public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
      String sql = boundSql.getSql();
      Page page = getLocalPage();
      //支持 order by
      String orderBy = page.getOrderBy();
      if (StringUtil.isNotEmpty(orderBy)) {
          pageKey.update(orderBy);
          sql = OrderByParser.converToOrderBySql(sql, orderBy);
      }
      if (page.isOrderByOnly()) {
          return sql;
      }
      return getPageSql(sql, page, pageKey);
  }

在getPageSql中getLocalPage将ThreadLocal中page变量取出来。
com.github.pagehelper.dialect.helper.MySqlDialect

@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    if (page.getStartRow() == 0) {
        sqlBuilder.append(" LIMIT ? ");
    } else {
        sqlBuilder.append(" LIMIT ?, ? ");
    }
    return sqlBuilder.toString();
}

PageHelper首先将前端传递的参数保存到page这个对象中,接着将page的副本存放入ThreadLocal中,这样可以保证分页的时候,参数互不影响,接着利用了mybatis提供的拦截器,取得ThreadLocal的值,重新拼装分页SQL,完成分页。我们都知道ThreadLocal使用完以后需要销毁来防止内存泄露,那么pageHelper是在哪里销毁的呢?
dialect.afterAll() 调用PageHelper类

@Override
    public void afterAll() {
        //这个方法即使不分页也会被执行,所以要判断 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if (delegate != null) {
            delegate.afterAll();
            autoDialect.clearDelegate();
        }
        clearPage();//就是 LOCAL_PAGE.remove();
    }