PageHelper 4.1.3原理剖析

本文针对PageHelper 4.1.3版本源码,与5.X版本源码差异较大

使用

  • pom文件中加入PageHelper插件
<dependency>
   <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.1.3</version>
</dependency>
  • 在mybatis-config.xml中配置拦截器PageHelper,此为PageHelper的核心类
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql"/>
            <property name="offsetAsPageNum" value="false"/>
            <property name="rowBoundsWithCount" value="false"/>
            <property name="pageSizeZero" value="true"/>
            <property name="reasonable" value="false"/>
            <property name="params" value="pageNum=start;pageSize=limit;"/>
            <property name="supportMethodsArguments" value="true"/>
            <property name="returnPageInfo" value="none"/>
        </plugin>
    </plugins>
</configuration>

配置里面的节点顺序是properties->settings->typeAliases->typeHandlers->objectFactory->objectWrapperFactory->plugins->environments->databaseIdProvider->mappers

  • 代码中直接使用PageHelper.startPage(1, count)即可自动拼接limit参数进行分页查询
Example itemExample = new Example(Item.class);
Example.Criteria itemCriteria = itemExample.createCriteria();
itemCriteria.andBetween("createDate", startTime, endTime);
itemCriteria.andEqualTo("delFlag", BizzConstants.DEL_FLAG_UN_DELETED);
itemExample.orderBy("orderId").asc();

PageHelper.startPage(1, 10);

List<Item> incomeItemList = ItemMapper.selectByExample(itemExample);

PageHelper.startPage()的作用?

  • 从PageHelper.startPage()方法入手
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {
    /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, true);
    }

    /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     * @param count    是否进行count查询
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, null);
    }

    /**
     * 开始分页
     *
     * @param pageNum    页码
     * @param pageSize   每页显示数量
     * @param count      是否进行count查询
     * @param reasonable 分页合理化,null时用默认配置
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
        return startPage(pageNum, pageSize, count, reasonable, null);
    }

    /**
     * 开始分页
     *
     * @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;
    }
}

PageHelper.startPage()方法创建了一个Page对象,将该Page对象保存到SqlUtil中;创建Page对象时,沿用之前Page对象的orderBy

  • Page对象保存了分页参数
public class Page<E> extends ArrayList<E> {
    public Page(int pageNum, int pageSize, boolean count) {
        this(pageNum, pageSize, count, null);
    }

    private Page(int pageNum, int pageSize, boolean count, Boolean reasonable) {
        super(0);
        if (pageNum == 1 && pageSize == Integer.MAX_VALUE) {
            pageSizeZero = true;
            pageSize = 0;
        }
        this.pageNum = pageNum;
        this.pageSize = pageSize;
        this.count = count;
        calculateStartAndEndRow();
        setReasonable(reasonable);
    }
}

注意:Page对象继承了ArrayList

  • SqlUtil中LOCAL_PAGE变量存储该Page对象,ThreadLocal保持线程封闭
public class SqlUtil implements Constant {
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

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

    public static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

    /**
     * 移除本地变量
     */
    public static void clearLocalPage() {
        LOCAL_PAGE.remove();
    }
}

到此已经可以理解PageHelper.startPage(1, count)方法的具体原理:
1、创建Page对象,保存pageNum、pageSize信息
2、将该对象保存到SqlUtil中,以便后续执行SQL时获取


执行时如何使用Page?

  • Mybatis拦截器
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}
  • PageHelper实现该拦截器,拦截Executor
public class PageHelper implements Interceptor {
    /**
     * 只拦截Executor
     *
     * @param target
     * @return
     */
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 设置属性值
     *
     * @param p 属性值
     */
    public void setProperties(Properties p) {
        checkVersion();
        //多数据源时,获取jdbcurl后是否关闭数据源
        String closeConn = p.getProperty("closeConn");
        this.closeConn = Boolean.parseBoolean(closeConn);
        //数据库方言
        String dialect = p.getProperty("dialect");
        String runtimeDialect = p.getProperty("autoRuntimeDialect");
        if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
            this.autoRuntimeDialect = true;
            this.autoDialect = false;
            this.properties = p;
        } else if (StringUtil.isEmpty(dialect)) {
            autoDialect = true;
            this.properties = p;
        } else {
            autoDialect = false;
            sqlUtil = new SqlUtil(dialect);
            sqlUtil.setProperties(p);
        }
    }

    /**
     * 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);
        }
    }
}

PageHelper拦截Executor,使用SqlUtil.processPage()方法进行处理

public class SqlUtil implements Constant {
    /**
     * Mybatis拦截器方法,这一步嵌套为了在出现异常时也可以清空Threadlocal
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    public Object processPage(Invocation invocation) throws Throwable {
        try {
            Object result = _processPage(invocation);
            return result;
        } finally {
            clearLocalPage();
        }
    }

    /**
     * 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);
        }
    }

    /**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
        //保存RowBounds状态
        RowBounds rowBounds = (RowBounds) args[2];
        //获取原始的ms
        MappedStatement ms = (MappedStatement) args[0];
        //判断并处理为PageSqlSource
        if (!isPageSqlSource(ms)) {
            processMappedStatement(ms);
        }
        //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
        ((PageSqlSource)ms.getSqlSource()).setParser(parser);
        //忽略RowBounds-否则会进行Mybatis自带的内存分页
        args[2] = RowBounds.DEFAULT;
        //如果只进行排序 或 pageSizeZero的判断
        if (isQueryOnly(page)) {
            return doQueryOnly(page, invocation);
        }

        //简单的通过total的值来判断是否进行count查询
        if (page.isCount()) {
            page.setCountSignal(Boolean.TRUE);
            //替换MS
            args[0] = msCountMap.get(ms.getId());
            //查询总数
            Object result = invocation.proceed();
            //还原ms
            args[0] = ms;
            //设置总数
            page.setTotal((Integer) ((List) result).get(0));
            if (page.getTotal() == 0) {
                return page;
            }
        } else {
            page.setTotal(-1l);
        }
        //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
        if (page.getPageSize() > 0 &&
                ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                        || rowBounds != RowBounds.DEFAULT)) {
            //将参数中的MappedStatement替换为新的qs
            page.setCountSignal(null);
            BoundSql boundSql = ms.getBoundSql(args[1]);
            args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
            page.setCountSignal(Boolean.FALSE);
            //执行分页查询
            Object result = invocation.proceed();
            //得到处理结果
            page.addAll((List) result);
        }
        //返回结果
        return page;
    }

    /**
     * 是否只做查询
     *
     * @param page
     * @return
     */
    private boolean isQueryOnly(Page page) {
        return page.isOrderByOnly()
                || ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0);
    }

    /**
     * 只做查询
     *
     * @param page
     * @param invocation
     * @return
     * @throws Throwable
     */
    private Page doQueryOnly(Page page, Invocation invocation) throws Throwable {
        page.setCountSignal(null);
        //执行正常(不分页)查询
        Object result = invocation.proceed();
        //得到处理结果
        page.addAll((List) result);
        //相当于查询第一页
        page.setPageNum(1);
        //这种情况相当于pageSize=total
        page.setPageSize(page.size());
        //仍然要设置total
        page.setTotal(page.size());
        //返回结果仍然为Page类型 - 便于后面对接收类型的统一处理
        return page;
    }
}

一、_processPage(Invocation invocation) 方法中,取出此前保存的Page对象
二、doProcessPage(Invocation invocation, Page page, Object[] args)方法中执行具体查询
1、如果只做查询,将会调用doQueryOnly(Page page, Invocation invocation),此方法将SQL执行返回的List结果转换为Page对象,设置pageSize、total的值
2、如果Page.isCount()为true,则先进行一次count查询统计总条数,根据count结果计算好total、pages、pageNum的值;如果count结果为0,则不进行后续分页查询(使用PageHelper.startPage(int pageNum, int pageSize)创建Page对象时,Page.isCount()值默认为true)


更多使用方法

  • 不分页
PageHelper.startPage(1, -1); 
  • 返回的Page对象,是ArrayList的子类,包含了分页信息,如果想获取查询结果中的分页信息,需要将结果强转为Page类型
Page<User> page = (Page) userMapper.findAllUsers();
page.getPageNum();
page.getPageSize();
page.getPages();
page.getTotal();
page.size();
  • PageInfo包装Page对象,可以获取更多属性
PageInfo<User> info = new PageInfo<>(userMapper.findAllUsers());

猜你喜欢

转载自blog.csdn.net/zbb451053802/article/details/80277329