数据分页性能是咱们软件系统中必备的性能,在长久层应用mybatis的状况下,pageHelper来实现后盾分页则是咱们罕用的一个抉择,所以本文专门类介绍下。

PageHelper原理

相干依赖

<dependency>    <groupId>org.mybatis</groupId>    <artifactId>mybatis</artifactId>    <version>3.2.8</version></dependency><dependency>    <groupId>com.github.pagehelper</groupId>    <artifactId>pagehelper</artifactId>    <version>1.2.15</version></dependency>

1.增加plugin

  要应用PageHelper首先在mybatis的全局配置文件中配置。如下:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE configuration        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration>    <plugins>        <!-- com.github.pagehelper为PageHelper类所在包名 -->        <plugin interceptor="com.github.pagehelper.PageHelper">            <property name="dialect" value="mysql" />            <!-- 该参数默认为false -->            <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码应用 -->            <!-- 和startPage中的pageNum成果一样 -->            <property name="offsetAsPageNum" value="true" />            <!-- 该参数默认为false -->            <!-- 设置为true时,应用RowBounds分页会进行count查问 -->            <property name="rowBoundsWithCount" value="true" />            <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查问出全副的后果 -->            <!-- (相当于没有执行分页查问,然而返回后果依然是Page类型) -->            <property name="pageSizeZero" value="true" />            <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->            <!-- 启用合理化时,如果pageNum<1会查问第一页,如果pageNum>pages会查问最初一页 -->            <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->            <property name="reasonable" value="false" />            <!-- 3.5.0版本可用 - 为了反对startPage(Object params)办法 -->            <!-- 减少了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->            <!-- 能够配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->            <!-- 不了解该含意的前提下,不要轻易复制该配置 -->            <property name="params" value="pageNum=start;pageSize=limit;" />            <!-- always总是返回PageInfo类型,check查看返回类型是否为PageInfo,none返回Page -->            <property name="returnPageInfo" value="check" />        </plugin>    </plugins></configuration>

2.加载过程

  咱们通过如下几行代码来演示过程

// 获取配置文件InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");// 通过加载配置文件获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);// 获取SqlSession对象SqlSession session = factory.openSession();PageHelper.startPage(1, 5);session.selectList("com.bobo.UserMapper.query");

加载配置文件咱们从这行代码开始

new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream) {   return build(inputStream, null, null); }



private void pluginElement(XNode parent) throws Exception {  if (parent != null) {    for (XNode child : parent.getChildren()) {      // 获取到内容:com.github.pagehelper.PageHelper      String interceptor = child.getStringAttribute("interceptor");      // 获取配置的属性信息      Properties properties = child.getChildrenAsProperties();      // 创立的拦截器实例      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();      // 将属性和拦截器绑定      interceptorInstance.setProperties(properties);      // 这个办法须要进入查看      configuration.addInterceptor(interceptorInstance);    }  }}
public void addInterceptor(Interceptor interceptor) {      // 将拦截器增加到了 拦截器链中 而拦截器链实质上就是一个List有序汇合    interceptorChain.addInterceptor(interceptor);  }

小结:通过SqlSessionFactory对象的获取,咱们加载了全局配置文件及映射文件同时还将配置的拦截器增加到了拦截器链中。

3.PageHelper定义的拦挡信息

  咱们来看下PageHelper的源代码的头部定义

@SuppressWarnings("rawtypes")@Intercepts(    @Signature(        type = Executor.class,         method = "query",         args = {MappedStatement.class                , Object.class                , RowBounds.class                , ResultHandler.class            }))public class PageHelper implements Interceptor {    //sql工具类    private SqlUtil sqlUtil;    //属性参数信息    private Properties properties;    //配置对象形式    private SqlUtilConfig sqlUtilConfig;    //主动获取dialect,如果没有setProperties或setSqlUtilConfig,也能够失常进行    private boolean autoDialect = true;    //运行时主动获取dialect    private boolean autoRuntimeDialect;    //多数据源时,获取jdbcurl后是否敞开数据源    private boolean closeConn = true;
// 定义的是拦挡 Executor对象中的// query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)// 这个办法type = Executor.class, method = "query", args = {MappedStatement.class        , Object.class        , RowBounds.class        , ResultHandler.class    }))

PageHelper中曾经定义了该拦截器拦挡的办法是什么。

4.Executor

  接下来咱们须要剖析下SqlSession的实例化过程中Executor产生了什么。咱们须要从这行代码开始跟踪

SqlSession session = factory.openSession();
public SqlSession openSession() {  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}




加强Executor

  到此咱们明确了,Executor对象其实被咱们生存的代理类加强了。invoke的代码为

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  try {    Set<Method> methods = signatureMap.get(method.getDeclaringClass());    // 如果是定义的拦挡的办法 就执行intercept办法    if (methods != null && methods.contains(method)) {      // 进入查看 该办法加强      return interceptor.intercept(new Invocation(target, method, args));    }    // 不是须要拦挡的办法 间接执行    return method.invoke(target, args);  } catch (Exception e) {    throw ExceptionUtil.unwrapThrowable(e);  }}
/** * Mybatis拦截器办法 * * @param invocation 拦截器入参 * @return 返回执行后果 * @throws Throwable 抛出异样 */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);    }}

该办法中的内容咱们前面再剖析。Executor的剖析咱们到此,接下来看下PageHelper实现分页的具体过程。

5.分页过程

  接下来咱们通过代码跟踪来看下具体的分页流程,咱们须要别离从两行代码开始:

5.1 startPage

PageHelper.startPage(1, 5);
/** * 开始分页 * * @param params */public static <E> Page<E> startPage(Object params) {    Page<E> page = SqlUtil.getPageFromObject(params);    //当曾经执行过orderBy的时候    Page<E> oldPage = SqlUtil.getLocalPage();    if (oldPage != null && oldPage.isOrderByOnly()) {        page.setOrderBy(oldPage.getOrderBy());    }    SqlUtil.setLocalPage(page);    return page;}
/** * 开始分页 * * @param pageNum    页码 * @param pageSize   每页显示数量 * @param count      是否进行count查问 * @param reasonable 分页合理化,null时用默认配置 */public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {    return startPage(pageNum, pageSize, count, reasonable, null);}
/** * 开始分页 * * @param offset 页码 * @param limit  每页显示数量 * @param count  是否进行count查问 */public static <E> Page<E> offsetPage(int offset, int limit, boolean count) {    Page<E> page = new Page<E>(new int[]{offset, limit}, count);    //当曾经执行过orderBy的时候    Page<E> oldPage = SqlUtil.getLocalPage();    if (oldPage != null && oldPage.isOrderByOnly()) {        page.setOrderBy(oldPage.getOrderBy());    }    // 这是重点!!!    SqlUtil.setLocalPage(page);    return page;}
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();// 将分页信息保留在ThreadLocal中 线程平安!public static void setLocalPage(Page page) {    LOCAL_PAGE.set(page);}

5.2selectList办法

session.selectList("com.bobo.UserMapper.query");
public <E> List<E> selectList(String statement) {  return this.selectList(statement, null);}public <E> List<E> selectList(String statement, Object parameter) {  return this.selectList(statement, parameter, RowBounds.DEFAULT);}


咱们须要回到invoke办法中持续看

/** * Mybatis拦截器办法 * * @param invocation 拦截器入参 * @return 返回执行后果 * @throws Throwable 抛出异样 */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);办法

/** * Mybatis拦截器办法 * * @param invocation 拦截器入参 * @return 返回执行后果 * @throws Throwable 抛出异样 */private Object _processPage(Invocation invocation) throws Throwable {    final Object[] args = invocation.getArgs();    Page page = null;    //反对办法参数时,会先尝试获取Page    if (supportMethodsArguments) {        // 从线程本地变量中获取Page信息,就是咱们刚刚设置的        page = getPage(args);    }    //分页信息    RowBounds rowBounds = (RowBounds) args[2];    //反对办法参数时,如果page == null就阐明没有分页条件,不须要分页查问    if ((supportMethodsArguments && page == null)            //当不反对分页参数时,判断LocalPage和RowBounds判断是否须要分页            || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {        return invocation.proceed();    } else {        //不反对分页参数时,page==null,这里须要获取        if (!supportMethodsArguments && page == null) {            page = getPage(args);        }        // 进入查看        return doProcessPage(invocation, page, args);    }}
/**  * 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)) {         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; }

进入 BoundSql boundSql = ms.getBoundSql(args[1])办法跟踪到PageStaticSqlSource类中的

@Overrideprotected BoundSql getPageBoundSql(Object parameterObject) {    String tempSql = sql;    String orderBy = PageHelper.getOrderBy();    if (orderBy != null) {        tempSql = OrderByParser.converToOrderBySql(sql, orderBy);    }    tempSql = localParser.get().getPageSql(tempSql);    return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);}

也能够看Oracle的分页实现

至此咱们发现PageHelper分页的实现原来是在咱们执行SQL语句之前动静的将SQL语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。

关注公众号:java宝典