分页插件PageHelper你还停留在只会用?

2,221 阅读5分钟

前言

mybatis是个优秀的半自动ORM框架,国内使用的特别多。增强版本的有plus,在开发过程对于数据量多的时候会使用分页。而PageHelper是一个很不错的分页插件,我们不用直接写count的sql,不用进行limit offset等的编写,只需要调用少量方法就可以进行分页,十分方便,提高开发效率。和Mybatis Generator、Mybatis Plugin 并称为mybatis三剑客。

使用方式

首先我们导入maven依赖

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

SpringMVC

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageHelper">
                    <property name="properties">
                        <!-- 什么都不配,使用默认的配置 -->
                        <value>
                            dialect=mysql
                        </value>
                    </property>
                </bean>
            </array>
        </property>

    </bean>

SpringBoot,需要通过new一个SqlSessionFactoryBean,然后setPlugins,再build生成sqlSessionFactory就可以了。

我们先看看如何使用,如下

PageHelper.startPage(page.getPageNo(),page.getPageSize());
List<CollectionVO> data1 = collectionMapper.selectByUser(userId,page);

PageInfo<CollectionVO> pageInfo1 = new PageInfo<>(data1);

只需要PageHelper.startPage设置分页的规则,最后用PageInfo包装查询的结果就完成了分页,是不是很方便呢?

源码解析

Mybatis SqlSessionFactoryBean plugins 设置com.github.pagehelper.PageHelper

我们可以看到PageHelper实现了Interceptor接口,这是mybatis为扩展插件的预留接口,实现这个接口编写插件逻辑,并set到plugins,就可以在mybatis执行的时候执行插件中的内容,我们继续往下看。

那么如何使用这个插件呢?

PageHelper.startPage(int pageNum, int pageSize)

在我们需要分页的地方之前,在mapper查询之前使用,PageHelper设置当前的页码和页大小。

我们进入startPage这个方法,可以看到,使用的是构造器模式,建造者模式的前身

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  		  //创建一个分页对象,保存页码,是否统计,页码,排序等规则
        Page<E> page = new Page(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        Page<E> oldPage = SqlUtil.getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
          //设置排序规则
            page.setOrderBy(oldPage.getOrderBy());
        }
			  //将当前实例化的Page对象放入ThreadLocal,这样在执行的时候可以取到,计算分页的offset和limit
        SqlUtil.setLocalPage(page);
        return page;
    }

我们进去看看Page,这里贴出部分代码,我们可以看到这里都是用来记录我们对于分页的一些设置,排序规则,是否统计总数等等。

public class Page<E> extends ArrayList<E> {
    private static final long serialVersionUID = 1L;
    private int pageNum;
    private int pageSize;
    private int startRow;
    private int endRow;
    private long total;
    private int pages;
    private boolean count;
    private Boolean countSignal;
    private String orderBy;
    private boolean orderByOnly;
    private Boolean reasonable;
    private Boolean pageSizeZero;

    public Page() {
    }
  
  	.......
}

image.png

在startPage之后生成了对应的Page,并通过SqlUtil.setLocalPage(page)放入ThreadLocal中,这样之后可以通过ThreadLocal获取

接下来是调用mapper.xxxx()

PageHelper.startPage(page.getPageNo(),page.getPageSize());

List<CollectionVO> data1 = collectionMapper.selectByUser(userId,page);

PageInfo<CollectionVO> pageInfo1 = new PageInfo<>(data1);

mybatis的执行我们知道是通过Executor执行的,底层是原生的PrepareStatement这些,关于mybatis的源码这里就不详细说明了,感兴趣的可以去阅读,也是必读源码之一。

我们直接进入主题,进入到org.apache.ibatis.session.Configuration类,看到如下方法

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    //三种基本的执行器
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
  	//这里CacheExecutor是对前面的进行装饰增强,装饰者模式
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
  	//重点,这里加载插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

接下来我们进入到org.apache.ibatis.plugin.InterceptorChain的如下方法

public Object pluginAll(Object target) {
  	//遍历所有的插件,如果你要问我interceptors集合的数据在哪里初始化的,看下面
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

关于interceptors的初始化,我们可以看到org.mybatis.spring.SqlSessionFactoryBeanbuildSqlSessionFactory放下,由于代码过多,这里只截取有关的,如下

	  //判断插件列表时候为空,this.plugins不就是我们在构造SqlSessionFactoryBean的时候放入的吗,通过Spring去loadBeanDefinition,注入到SqlSessionFactoryBean实例的属性中,这里可能需要稍微了解一下Spring源码
    if (!isEmpty(this.plugins)) {
      //遍历所有的插件
      for (Interceptor plugin : this.plugins) {
        //加入到列表
        configuration.addInterceptor(plugin);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered plugin: '" + plugin + "'");
        }
      }
    }

继续进入configuration.addInterceptor(plugin),org.apache.ibatis.session.Configuration的addInterceptor方法,可以看到调用了InterceptorChain的addInterceptor方法,所以interceptors会包含我们PageHelper这个插件了。回归到主线,继续往下。

上述经过org.apache.ibatis.plugin.InterceptorChain.pluginAll方法后,我们得到了一个执行器的代理对象,在sql执行的时候,就会把执行权交给代理对象。

这里在看下pluginAll方法吧,进入com.github.pagehelper.PageHelper的如下方法

public Object plugin(Object target) {
  			//目标对象是执行器,生成代理
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

进入org.apache.ibatis.plugin.Pluginwrap方法

public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  	//目标类类对象
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      //构造JDK代理,这里InvocationHandler是Plugin对象,那么最终执行器执行会被代理到Plugin的invoke方法中来,了解代理模式的应该都能明白。
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

那么接下来我们进入到org.apache.ibatis.plugin.Plugin.invoke方法,如下

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        //看这里就好了。执行PageHelper的intercept方法,并将执行的参数,目标对象,方法等信息传递过去
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

回到com.github.pagehelper.PageHelper.intercept,最终的处理都是在这里进行。

public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }

由于我们只看主线,部分逻辑就没细讲了,进入到sqlUtil.processPage(invocation),继续进入_processPage(invocation),在进入doProcessPage(invocation, page, args),我们可以看到这里对相关sql进行了处理,首先我们看下总数查询如何做的。

 /**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
        //保存RowBounds状态
        RowBounds rowBounds = (RowBounds) args[2];
        //获取原始的ms
        MappedStatement ms = (MappedStatement) args[0];
        //判断并处理为PageSqlSource
        if (!isPageSqlSource(ms)) {
          	//这里去builder一个count的MapperStatement,因为我们要查询总数,需要这样一个Statement去执行
            processMappedStatement(ms);
        }
        //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
        ((PageSqlSource)ms.getSqlSource()).setParser(parser);
        try {
            //忽略RowBounds-否则会进行Mybatis自带的内存分页
            args[2] = RowBounds.DEFAULT;
            //如果只进行排序 或 pageSizeZero的判断
            if (isQueryOnly(page)) {
                return doQueryOnly(page, invocation);
            }

            //简单的通过total的值来判断是否进行count查询
            if (page.isCount()) {
                page.setCountSignal(Boolean.TRUE);
                //替换MS
                args[0] = msCountMap.get(ms.getId());
                //查询总数
              	//这里是去要去执行查询
                Object result = invocation.proceed();
                //还原ms
                args[0] = ms;
                //设置总数
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            } else {
                page.setTotal(-1l);
            }
            //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //将参数中的MappedStatement替换为新的qs
                page.setCountSignal(null);
                BoundSql boundSql = ms.getBoundSql(args[1]);
                args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
                page.setCountSignal(Boolean.FALSE);
                //执行分页查询
                Object result = invocation.proceed();
                //得到处理结果
                page.addAll((List) result);
            }
        } finally {
            ((PageSqlSource)ms.getSqlSource()).removeParser();
        }

        //返回结果
        return page;
    }

经过invocation.proceed()我们会进入到org.apache.ibatis.executor.CachingExecutor.query方法,接着我们进入getBoundSql(parameterObject)进入org.apache.ibatis.mapping.MappedStatement.getBoundSql

接着进入到com.github.pagehelper.sqlsource.PageSqlSource的如下方法

/**
     * 获取BoundSql
     *
     * @param parameterObject
     * @return
     */
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        Boolean count = getCount();
        if (count == null) {
            return getDefaultBoundSql(parameterObject);
        } else if (count) {
            //生成count的sql,我们进去这里
            return getCountBoundSql(parameterObject);
        } else {
            return getPageBoundSql(parameterObject);
        }
    }

进入getCountBoundSql,继续点进去,我们最终会进入到com.github.pagehelper.parser.SqlParsergetSmartCountSql方法,如下

public String getSmartCountSql(String sql) {
        //校验是否支持该sql
        isSupportedSql(sql);
  			//如果已经缓存过sql,直接返回
        if (CACHE.get(sql) != null) {
            return CACHE.get(sql);
        }
        //解析SQL
        Statement stmt = null;
        try {
            //解析器解析sql,这里的解析器用的是jsqlparser,具体的解析器就不剖析了。
            stmt = CCJSqlParserUtil.parse(sql);
        } catch (Throwable e) {
            //无法解析的用一般方法返回count语句
            String countSql = getSimpleCountSql(sql);
            CACHE.put(sql, countSql);
            return countSql;
        }
  			//解析之后会得到一个Select对象,将sql每部分解析填充到Select对象的属性中
        Select select = (Select) stmt;
        SelectBody selectBody = select.getSelectBody();
        //处理body-去order by
        processSelectBody(selectBody);
        //处理with-去order by
        processWithItemsList(select.getWithItemsList());
        //处理为count查询
  			//这里会对sql加上count(0)和别名
        sqlToCount(select);
        String result = select.toString();
        CACHE.put(sql, result);
  			//最终的sql就是select count(0) from (xxxxxxx) table_alias
        return result;
    }

回到com.github.pagehelper.SqlUtil.doProcessPage方法,如下,得到result就是包含总数的一个list,取第一项set到Page对象的total属性

if (page.isCount()) {
                page.setCountSignal(Boolean.TRUE);
                //替换MS
                args[0] = msCountMap.get(ms.getId());
                //查询总数
              	//这里是去要去执行查询
                Object result = invocation.proceed();
                //还原ms
                args[0] = ms;
                //设置总数
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            } else {
                page.setTotal(-1l);
            }

查询的列表结果在如下流程,就是执行原sql,然后得到List结果集,加入到Page中,Page是个List,继承ArrayList

//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //将参数中的MappedStatement替换为新的qs
                page.setCountSignal(null);
                BoundSql boundSql = ms.getBoundSql(args[1]);
                args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
                page.setCountSignal(Boolean.FALSE);
                //执行分页查询
                Object result = invocation.proceed();
                //得到处理结果
                page.addAll((List) result);
            }

最终我们就可以得到一个有总数和结果的Page对象了,也就是mapper.xxxxx()方法返回的数据,如下,我们可以看到执行后返回的不是List,而是一个Page对象

image.png 这个时候我们用PageInfo格式化一下,得到给前端的分页信息,至此,整个流程结束。