今天心血来潮,有点好奇mybaits的分页组件PageHelper是如何实现分页功能的,因为在我日常的使用中,需要分页的地方只需要在查询语句前加一行代码
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- //增加此行代码开启分页,pageNum为第几页,pageSize为一页多少条
- Page<ArticleVO> page = PageHelper.startPage(pageNum, pageSize);
- //执行正常的sql查询
- articleMapper.selectAll(query);
即可实现分页功能。于是我很好奇PageHelper是如何实现的,使用了aop?还是其他什么办法。
备注:
因为我是在springboot中使用的PageHelper,所以PageHelper的版本为
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- <dependency>
- <groupId>com.github.pagehelper</groupId>
- <artifactId>pagehelper-spring-boot-starter</artifactId>
- <version>1.2.12</version>
- </dependency>
本文的源码分析主要是分析主要的流程,一些细节以及mybaits的部分不深入分析(因为分析深了不知不觉就晕了,忘记了我一开始是要干啥)
开启PageHelper是调用了startPage这个方法,所以我直接从这个方法入手,查看这个方法的内部实现,该方法有多个重载,但都只是为了方便使用设置了一些默认参数,最终的实现都是:
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- /**
- * 开始分页
- *
- * @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在启动的时候做了些什么操作。
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- /**
- * 自定注入分页插件
- *
- * @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这个类中都干了些什么事。
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- @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();
- }
- }
- }
- }
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- 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针对不同的数据库有多种不同的实现类
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- 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。
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
- @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对象以及页数总页数等的处理)
至此分页的操作完成。