从源码分析PageHelper是如何实现分页功能的

今天心血来潮,有点好奇mybaits的分页组件PageHelper是如何实现分页功能的,因为在我日常的使用中,需要分页的地方只需要在查询语句前加一行代码

//增加此行代码开启分页,pageNum为第几页,pageSize为一页多少条
Page<ArticleVO> page = PageHelper.startPage(pageNum, pageSize);
//执行正常的sql查询
articleMapper.selectAll(query);

即可实现分页功能。于是我很好奇PageHelper是如何实现的,使用了aop?还是其他什么办法。

备注

因为我是在springboot中使用的PageHelper,所以PageHelper的版本为

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

本文的源码分析主要是分析主要的流程,一些细节以及mybaits的部分不深入分析(因为分析深了不知不觉就晕了,忘记了我一开始是要干啥)

开启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 = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    setLocalPage(page);
    return page;
}

可以看到,方法实例化了一个Page对象,这个对象用来存放分页相关的数据。其中getLocalPage和setLocalPage这两个方法是对ThreadLocal<Page>线程中存储的Page对象的设置和获取。也就是说startPage方法的作用就是:确保在当前线程中存在一个Page对象(往下看可以看到,代码中会根据是否存在Page对象来决定是否开启分页功能)

至此开启分页的方法已经结束了,我们没有看到任何跟分页有关的操作,那么PageHelper到底是在哪里实现分页功能的呢?因为我们使用的是SpringBoot,所以我猜测应该会有相关的AutoConfiguration类来对PageHelper进行相关的初始化配置等。于是我们使用idea打开pagehelper-spring-boot-starter这个jar包,发现真的找到了PageHelperAutoConfiguration这个类,那我们就继续从这个类着手看看PageHelper在启动的时候做了些什么操作。

/**
 * 自定注入分页插件
 *
 * @author liuzh
 */
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private PageHelperProperties properties;

    /**
     * 接受分页插件额外的属性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

}

对这个类分析我们可以发现,该类做了2件事。

1、实例化了一个带有默认配置的Properties配置对象放到spring上下文中

2、在addPageInterceptor方法中实例化PageInterceptor对象(实例化后的配置操作我们不深究),并添加到mybatis中的SqlSessionFactory中

那么我们上面的疑问就解开了,PageHelper会给mybatis增加一个PageInterceptor拦截器,这样在我们使用mybatis进行数据库操作时,PageHelper就能实现对应的分页操作。这里的Interceptor以及SqlSessionFactory的相关知识属于mybaits的范畴,跟PageHelper关系不是很大,我们只要知道他是在这里对数据库操作进行切入就可以了。那么我们继续看PageInterceptor这个类中都干了些什么事。

@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 {
    private volatile Dialect dialect;
    private String countSuffix = "_COUNT";
    protected Cache<String, MappedStatement> msCountMap = null;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            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();
            }
        }
    }

}
PageInterceptor实现了mybaits中的Interceptor接口,并且在PageHelperAutoConfiguration中,在SqlSessionFactory中添加了该拦截器,所以在使用mybaits进行数据库操作时,都会进入PageInterceptor的intercept方法(注意到了类上方的注解没有@Intercepts,这个注解指定了仅在特定情况如query操作的时候才会进入到该拦截器,很容易理解,分页操作仅在query查询操作才需要)。分页的整体操作流程全部在intercept方法中(上述代码仅保留部分代码,一些细节我删掉了),可以看到主要使用了一个Dialect对象来实现分页的各个操作。Dialect其实是一个接口,定义了分页的各个流程。方法如下:
public interface Dialect {
    /**
     * 跳过 count 和 分页查询
     *
     * @param ms              MappedStatement
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @return true 跳过,返回默认查询结果,false 执行分页查询
     */
    boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds);

    /**
     * 执行分页前,返回 true 会进行 count 查询,false 会继续下面的 beforePage 判断
     *
     * @param ms              MappedStatement
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @return
     */
    boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds);

    /**
     * 生成 count 查询 sql
     *
     * @param ms              MappedStatement
     * @param boundSql        绑定 SQL 对象
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @param countKey        count 缓存 key
     * @return
     */
    String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey);

    /**
     * 执行完 count 查询后
     *
     * @param count           查询结果总数
     * @param parameterObject 接口参数
     * @param rowBounds       分页参数
     * @return true 继续分页查询,false 直接返回
     */
    boolean afterCount(long count, Object parameterObject, RowBounds rowBounds);

    /**
     * 处理查询参数对象
     *
     * @param ms              MappedStatement
     * @param parameterObject
     * @param boundSql
     * @param pageKey
     * @return
     */
    Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey);

    /**
     * 执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
     *
     * @param ms              MappedStatement
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @return
     */
    boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds);

    /**
     * 生成分页查询 sql
     *
     * @param ms              MappedStatement
     * @param boundSql        绑定 SQL 对象
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @param pageKey         分页缓存 key
     * @return
     */
    String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);

    /**
     * 分页查询后,处理分页结果,拦截器中直接 return 该方法的返回值
     *
     * @param pageList        分页查询结果
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @return
     */
    Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds);

    /**
     * 完成所有任务后
     */
    void afterAll();

    /**
     * 设置参数
     *
     * @param properties 插件属性
     */
    void setProperties(Properties properties);
}

具体的接口方法的作用参考注释大致都能看的明白。Dialect针对不同的数据库有多种不同的实现类

PageInterceptor中使用的Dialect默认实现类是PageHelper,PageHelper虽然实现了Dialect接口,但是他对接口中的实现方法除skip方法以外其他方法基本都转发交给了PageAutoDialect这个类进行处理。而skip方法也只是简单的判断线程变量中page对象是否存在来决定是否跳过分页。
public class PageHelper extends PageMethod implements Dialect {
    private PageParams pageParams;
    private PageAutoDialect autoDialect;
    
    @Override
    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;
        }
    }
}

继续看看PageAutoDialect这个类的作用。因为这个类的代码比较多我就不贴代码了,简单说下这个类干啥的。因为多种数据库的分页方式可能存在差异,所以在分页的时候需要根据数据库的类型选择对应的数据库方言,即上文提到的Dialect的多种实现类。这一块可以手动配置指定也可以让pagehelper自己根据数据库连接的url啊等一些因素来判断。PageAutoDialect类在初始化的时候会实例化对应的Dialect存在自己的属性中(多数据源的情况是存在线程变量中)。所以在PageHelper这个类中,针对分页的操作方法他都通过PageAutoDialect来获取dialect进而将操作转交给获取到的Dialect。

@Override
public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
    return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
}

回到PageInterceptor中的intercept方法,具体的分页流程可以详细去看具体的代码。我这里简单说说分页的过程:

1、在query类型的数据库查询进来时,会通过skip方法判断是否需要分页,不需要分页直接进行正常的查询操作并返回。

2、需要分页的情况下,通过beforeCount方法判断是否需要进行count总数的查询,如果需要则调用count方法查询总数并在查询总数结束后调用afterCount,这里多了一个操作。即判断查出来的数据总条数是否为0(为0相当于没数据,直接返回一个空数据的分页对象,节省一次查询操作)

3、在上述操作结束之后,开始进行数据的查询,调用ExecutorUtil.pageQuery方法。该方法会通过beforePage来判断需不需要在sql语句中中添加分页的操作(limit x,x)

4、查询结束之后调用afterPage进行一些分页对象page的处理(数据添加到page对象以及页数总页数等的处理)

至此分页的操作完成。


支付宝搜索:344355 领取随机红包

如果文章对您有帮助,欢迎给作者打赏