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

大艺充电角磨机多功能充电角向磨光机打磨机切割机电动工具抛光除锈机器电锤电锯手电钻组合套装 A7-5801-40S一电一充标配+赠品 >> 限时秒杀¥436.00

PageHelper是MyBatis中一个很好用的分页插件,在集成插件之后,只需要在需要分页的MyBatis查询方法前调用PageHelper.startPage静态方法,紧跟在这个方法后的第一个MyBatis查询方法会被进行分页。

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

简单好用,但是实现原理是什么呢?

总结实现原理:

1、插件是通过MyBatis的拦截器来实现分页逻辑的,入口是一个PageInterceptor类。
2、通过ThreadLocal线程变量的形式增加需要分页的标识以及分页信息,这也就是PageHelper.startPage能开启实现分页的原因。
3、在MySql下(其他数据库没去研究),最终实现分页的查询操作,还是通过给sql添加limit x,x实现的。

准备工作:

使用的源码版本为:

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

由于项目中使用的是SpringBoot,所以引入的是starter的依赖就够了,使用的是:

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

分页信息及分页标识

聚焦于PageHelper.startPage这个静态方法,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对象,这个对象用来存放分页相关的信息,页码,每页显示数量等。然后通过setLocalPage方法将这个Page对象存储到线程变量ThreadLocal<Page> LOCAL_PAGE中。到这里分页信息的设置就结束了,剩下的工作就是拦截器里面的内容,拦截器会通过LOCAL_PAGE中这个Page对象的存在与否决定是否开启分页逻辑。

分页的核心逻辑PageInterceptor拦截器

由于使用的是starter的依赖,所以在代码中是通过PageHelperAutoConfiguration这个配置类将PageInterceptor拦截器添加到MyBatis中的

@PostConstruct
public void addPageInterceptor() {
    PageInterceptor interceptor = new PageInterceptor();
    Properties properties = new Properties();
    properties.putAll(pageHelperProperties());
    properties.putAll(this.properties.getProperties());
    interceptor.setProperties(properties);
    for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
        //添加分页拦截器
        sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
    }
}

通过@PostConstruct注解执行addInterceptor方法来添加拦截器。

整个分页的核心代码逻辑就在于拦截器中,拦截器是分页的入口

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

分页的基本逻辑都在这个方法中,通过dialect对象的各个方法结合实现分页功能,Dialect是一个接口,他有不同的子类实现,对应不同的数据库方言。如果我们没有在配置中指定具体的实现类的话,默认的实现类是PageHelper。而PageHelper相当于一个大管家,他内部有一个PageAutoDialect自动方言类来自动的选择对应的数据库方言。

PageAutoDialect识别数据库的实现原理是通过获取DataSource对象继而获取jdbcUrl,然后通过jdbcUrl来识别的,也就是通过识别jdbc:mysql://实现

/**
 * 根据 jdbcUrl 获取数据库方言
 *
 * @param ms
 * @return
 */
private AbstractHelperDialect getDialect(MappedStatement ms) {
    //改为对dataSource做缓存
    DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
    String url = getUrl(dataSource);
    if (urlDialectMap.containsKey(url)) {
        return urlDialectMap.get(url);
    }
    try {
        lock.lock();
        if (urlDialectMap.containsKey(url)) {
            return urlDialectMap.get(url);
        }
        if (StringUtil.isEmpty(url)) {
            throw new PageException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
        }
        String dialectStr = fromJdbcUrl(url);
        if (dialectStr == null) {
            throw new PageException("无法自动获取数据库类型,请通过 helperDialect 参数指定!");
        }
        AbstractHelperDialect dialect = initDialect(dialectStr, properties);
        urlDialectMap.put(url, dialect);
        return dialect;
    } finally {
        lock.unlock();
    }
}

回到intercept方法看分页的主流程,流程主要是:是否需要分页(skip)->分页前是否要count查询(beforeCount)->count查询(count)->查询分页数据(ExecutorUtil.pageQuery)->处理分页结果(afterPage)->清理数据(afterAll)

skip方法:逻辑就是判断线程变量LOCAL_PAGE是否有page对象,如果没有的话看是否有原始的rowBounds对象

beforeCount方法:page对象中的count属性判断,默认是true

count方法:会通过主语句的id+_COUNT后缀的形式查找是否有自定义的count查询语句,如果没有的话则会自动创建一个;自动创建的count语句是在com.github.pagehelper.parser.CountSqlParser#getSmartCountSql(java.lang.String, java.lang.String)方法中,大部分是通过创建简单的count子查询实现

ExecutorUtil.pageQuery方法:这个方法比较复杂,是实现最终数据查询的地方,主要的逻辑是获取对应数据库方言的分页语句形式,MySql的话是在com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql方法中,通过添加limit实现

afterPage方法:返回结果的整理,将查询的数据结果以及分页的信息(总数总页数等)统一聚合设置到page对象中

afterAll方法:清理线程变量中的page对象,避免影响下一个sql的执行

到这里分页就完成了,更多的逻辑可以下载源码仔细看看,本地debug一下更高效。

汉牌 苹果7/8钢化膜 iPhone7/8手机膜全屏覆盖高清玻璃手机保护贴膜 4.7英寸 苹果7/苹果8 【全屏白色】2片 >> 限时秒杀¥17.80

觉得内容还不错?打赏个钢镚鼓励鼓励!!👍