关于java:面试官说说MyBatis分页插件PageHelper工作原理和配置过程

35次阅读

共计 9116 个字符,预计需要花费 23 分钟才能阅读完成。

  数据分页性能是咱们软件系统中必备的性能,在长久层应用 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 类中的

@Override
protected 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 宝典

正文完
 0