PageHelper原理分析

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

<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
  1. //增加此行代码开启分页,pageNum为第几页,pageSize为一页多少条
  2. Page<ArticleVO> page = PageHelper.startPage(pageNum, pageSize);
  3. //执行正常的sql查询
  4. 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>
  1. <dependency>
  2. <groupId>com.github.pagehelper</groupId>
  3. <artifactId>pagehelper-spring-boot-starter</artifactId>
  4. <version>1.2.12</version>
  5. </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>
  1. /**
  2. * 开始分页
  3. *
  4. * @param pageNum 页码
  5. * @param pageSize 每页显示数量
  6. * @param count 是否进行count查询
  7. * @param reasonable 分页合理化,null时用默认配置
  8. * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
  9. */
  10. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  11. Page<E> page = new Page<E>(pageNum, pageSize, count);
  12. page.setReasonable(reasonable);
  13. page.setPageSizeZero(pageSizeZero);
  14. //当已经执行过orderBy的时候
  15. Page<E> oldPage = getLocalPage();
  16. if (oldPage != null && oldPage.isOrderByOnly()) {
  17. page.setOrderBy(oldPage.getOrderBy());
  18. }
  19. setLocalPage(page);
  20. return page;
  21. }

可以看到,方法实例化了一个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>
  1. /**
  2. * 自定注入分页插件
  3. *
  4. * @author liuzh
  5. */
  6. @Configuration
  7. @ConditionalOnBean(SqlSessionFactory.class)
  8. @EnableConfigurationProperties(PageHelperProperties.class)
  9. @AutoConfigureAfter(MybatisAutoConfiguration.class)
  10. public class PageHelperAutoConfiguration {
  11. @Autowired
  12. private List<SqlSessionFactory> sqlSessionFactoryList;
  13. @Autowired
  14. private PageHelperProperties properties;
  15. /**
  16. * 接受分页插件额外的属性
  17. *
  18. * @return
  19. */
  20. @Bean
  21. @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
  22. public Properties pageHelperProperties() {
  23. return new Properties();
  24. }
  25. @PostConstruct
  26. public void addPageInterceptor() {
  27. PageInterceptor interceptor = new PageInterceptor();
  28. Properties properties = new Properties();
  29. //先把一般方式配置的属性放进去
  30. properties.putAll(pageHelperProperties());
  31. //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
  32. properties.putAll(this.properties.getProperties());
  33. interceptor.setProperties(properties);
  34. for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
  35. sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
  36. }
  37. }
  38. }

对这个类分析我们可以发现,该类做了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>
  1. @Intercepts(
  2. {
  3. @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  4. @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
  5. }
  6. )
  7. public class PageInterceptor implements Interceptor {
  8. private volatile Dialect dialect;
  9. private String countSuffix = "_COUNT";
  10. protected Cache<String, MappedStatement> msCountMap = null;
  11. private String default_dialect_class = "com.github.pagehelper.PageHelper";
  12. @Override
  13. public Object intercept(Invocation invocation) throws Throwable {
  14. try {
  15. List resultList;
  16. //调用方法判断是否需要进行分页,如果不需要,直接返回结果
  17. if (!dialect.skip(ms, parameter, rowBounds)) {
  18. //判断是否需要进行 count 查询
  19. if (dialect.beforeCount(ms, parameter, rowBounds)) {
  20. //查询总数
  21. Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  22. //处理查询总数,返回 true 时继续分页查询,false 时直接返回
  23. if (!dialect.afterCount(count, parameter, rowBounds)) {
  24. //当查询总数为 0 时,直接返回空的结果
  25. return dialect.afterPage(new ArrayList(), parameter, rowBounds);
  26. }
  27. }
  28. resultList = ExecutorUtil.pageQuery(dialect, executor,
  29. ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
  30. } else {
  31. //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
  32. resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
  33. }
  34. return dialect.afterPage(resultList, parameter, rowBounds);
  35. } finally {
  36. if(dialect != null){
  37. dialect.afterAll();
  38. }
  39. }
  40. }
  41. }
PageInterceptor实现了mybaits中的Interceptor接口,并且在PageHelperAutoConfiguration中,在SqlSessionFactory中添加了该拦截器,所以在使用mybaits进行数据库操作时,都会进入PageInterceptor的intercept方法(注意到了类上方的注解没有@Intercepts,这个注解指定了仅在特定情况如query操作的时候才会进入到该拦截器,很容易理解,分页操作仅在query查询操作才需要)。分页的整体操作流程全部在intercept方法中(上述代码仅保留部分代码,一些细节我删掉了),可以看到主要使用了一个Dialect对象来实现分页的各个操作。Dialect其实是一个接口,定义了分页的各个流程。方法如下:
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
  1. public interface Dialect {
  2. /**
  3. * 跳过 count 和 分页查询
  4. *
  5. * @param ms MappedStatement
  6. * @param parameterObject 方法参数
  7. * @param rowBounds 分页参数
  8. * @return true 跳过,返回默认查询结果,false 执行分页查询
  9. */
  10. boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds);
  11. /**
  12. * 执行分页前,返回 true 会进行 count 查询,false 会继续下面的 beforePage 判断
  13. *
  14. * @param ms MappedStatement
  15. * @param parameterObject 方法参数
  16. * @param rowBounds 分页参数
  17. * @return
  18. */
  19. boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds);
  20. /**
  21. * 生成 count 查询 sql
  22. *
  23. * @param ms MappedStatement
  24. * @param boundSql 绑定 SQL 对象
  25. * @param parameterObject 方法参数
  26. * @param rowBounds 分页参数
  27. * @param countKey count 缓存 key
  28. * @return
  29. */
  30. String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey);
  31. /**
  32. * 执行完 count 查询后
  33. *
  34. * @param count 查询结果总数
  35. * @param parameterObject 接口参数
  36. * @param rowBounds 分页参数
  37. * @return true 继续分页查询,false 直接返回
  38. */
  39. boolean afterCount(long count, Object parameterObject, RowBounds rowBounds);
  40. /**
  41. * 处理查询参数对象
  42. *
  43. * @param ms MappedStatement
  44. * @param parameterObject
  45. * @param boundSql
  46. * @param pageKey
  47. * @return
  48. */
  49. Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey);
  50. /**
  51. * 执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
  52. *
  53. * @param ms MappedStatement
  54. * @param parameterObject 方法参数
  55. * @param rowBounds 分页参数
  56. * @return
  57. */
  58. boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds);
  59. /**
  60. * 生成分页查询 sql
  61. *
  62. * @param ms MappedStatement
  63. * @param boundSql 绑定 SQL 对象
  64. * @param parameterObject 方法参数
  65. * @param rowBounds 分页参数
  66. * @param pageKey 分页缓存 key
  67. * @return
  68. */
  69. String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
  70. /**
  71. * 分页查询后,处理分页结果,拦截器中直接 return 该方法的返回值
  72. *
  73. * @param pageList 分页查询结果
  74. * @param parameterObject 方法参数
  75. * @param rowBounds 分页参数
  76. * @return
  77. */
  78. Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds);
  79. /**
  80. * 完成所有任务后
  81. */
  82. void afterAll();
  83. /**
  84. * 设置参数
  85. *
  86. * @param properties 插件属性
  87. */
  88. void setProperties(Properties properties);
  89. }

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

PageInterceptor中使用的Dialect默认实现类是PageHelper,PageHelper虽然实现了Dialect接口,但是他对接口中的实现方法除skip方法以外其他方法基本都转发交给了PageAutoDialect这个类进行处理。而skip方法也只是简单的判断线程变量中page对象是否存在来决定是否跳过分页。
<span style="background-color:#f6f6f6"><span style="color:#333333"><span style="color:rgba(140, 140, 140, 0.8)">复制代码</span></span></span>
  1. public class PageHelper extends PageMethod implements Dialect {
  2. private PageParams pageParams;
  3. private PageAutoDialect autoDialect;
  4. @Override
  5. public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
  6. if (ms.getId().endsWith(MSUtils.COUNT)) {
  7. throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
  8. }
  9. Page page = pageParams.getPage(parameterObject, rowBounds);
  10. if (page == null) {
  11. return true;
  12. } else {
  13. //设置默认的 count 列
  14. if (StringUtil.isEmpty(page.getCountColumn())) {
  15. page.setCountColumn(pageParams.getCountColumn());
  16. }
  17. autoDialect.initDelegateDialect(ms);
  18. return false;
  19. }
  20. }
  21. }

继续看看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>
  1. @Override
  2. public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
  3. return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
  4. }

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

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

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

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

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

至此分页的操作完成。

猜你喜欢

转载自blog.csdn.net/XJF199001/article/details/121004869
今日推荐