今天心血来潮,有点好奇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(); } } } }
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针对不同的数据库有多种不同的实现类
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对象以及页数总页数等的处理)
至此分页的操作完成。