面试官:说说MyBatis分页插件(PageHelper)工作原理和配置过程? - Java宝典的文章 - 知乎 https://zhuanlan.zhihu.com/p/377904216
数据分页功能是我们软件系统中必备的功能,在持久层使用mybatis的情况下,pageHelper来实现后台分页则是我们常用的一个选择,所以本文专门类介绍下。

PageHelper原理

相关依赖

  1. <dependency>
  2. <groupId>org.mybatis</groupId>
  3. <artifactId>mybatis</artifactId>
  4. <version>3.2.8</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.github.pagehelper</groupId>
  8. <artifactId>pagehelper</artifactId>
  9. <version>1.2.15</version>
  10. </dependency>

1.添加plugin

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

  1. 作者:Java宝典
  2. 链接:https://zhuanlan.zhihu.com/p/377904216
  3. 来源:知乎
  4. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  5. <?xml version="1.0" encoding="UTF-8"?>
  6. <!DOCTYPE configuration
  7. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  8. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  9. <configuration>
  10. <plugins>
  11. <!-- com.github.pagehelperPageHelper类所在包名 -->
  12. <plugin interceptor="com.github.pagehelper.PageHelper">
  13. <property name="dialect" value="mysql" />
  14. <!-- 该参数默认为false -->
  15. <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
  16. <!-- startPage中的pageNum效果一样 -->
  17. <property name="offsetAsPageNum" value="true" />
  18. <!-- 该参数默认为false -->
  19. <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
  20. <property name="rowBoundsWithCount" value="true" />
  21. <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
  22. <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) -->
  23. <property name="pageSizeZero" value="true" />
  24. <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
  25. <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
  26. <!-- 禁用合理化时,如果pageNum<1pageNum>pages会返回空数据 -->
  27. <property name="reasonable" value="false" />
  28. <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
  29. <!-- 增加了一个`params`参数来配置参数映射,用于从MapServletRequest中取值 -->
  30. <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->
  31. <!-- 不理解该含义的前提下,不要随便复制该配置 -->
  32. <property name="params" value="pageNum=start;pageSize=limit;" />
  33. <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
  34. <property name="returnPageInfo" value="check" />
  35. </plugin>
  36. </plugins>
  37. </configuration>

2.加载过程

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

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

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

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

image.png

image.png

image.png

  1. private void pluginElement(XNode parent) throws Exception {
  2. if (parent != null) {
  3. for (XNode child : parent.getChildren()) {
  4. // 获取到内容:com.github.pagehelper.PageHelper
  5. String interceptor = child.getStringAttribute("interceptor");
  6. // 获取配置的属性信息
  7. Properties properties = child.getChildrenAsProperties();
  8. // 创建的拦截器实例
  9. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
  10. // 将属性和拦截器绑定
  11. interceptorInstance.setProperties(properties);
  12. // 这个方法需要进入查看
  13. configuration.addInterceptor(interceptorInstance);
  14. }
  15. }
  16. }
  1. public void addInterceptor(Interceptor interceptor) {
  2. // 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合
  3. interceptorChain.addInterceptor(interceptor);
  4. }

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

3.PageHelper定义的拦截信息

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

  1. @SuppressWarnings("rawtypes")
  2. @Intercepts(
  3. @Signature(
  4. type = Executor.class,
  5. method = "query",
  6. args = {MappedStatement.class
  7. , Object.class
  8. , RowBounds.class
  9. , ResultHandler.class
  10. }))
  11. public class PageHelper implements Interceptor {
  12. //sql工具类
  13. private SqlUtil sqlUtil;
  14. //属性参数信息
  15. private Properties properties;
  16. //配置对象方式
  17. private SqlUtilConfig sqlUtilConfig;
  18. //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
  19. private boolean autoDialect = true;
  20. //运行时自动获取dialect
  21. private boolean autoRuntimeDialect;
  22. //多数据源时,获取jdbcurl后是否关闭数据源
  23. private boolean closeConn = true;
  1. // 定义的是拦截 Executor对象中的
  2. // query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)
  3. // 这个方法
  4. type = Executor.class,
  5. method = "query",
  6. args = {MappedStatement.class
  7. , Object.class
  8. , RowBounds.class
  9. , ResultHandler.class
  10. }))

PageHelper中已经定义了该拦截器拦截的方法是什么。

4.Executor

接下来我们需要分析下SqlSession的实例化过程中Executor发生了什么。我们需要从这行代码开始跟踪

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

image.png

image.png

image.png

image.png
增强Executor

image.png

image.png
到此我们明白了,Executor对象其实被我们生存的代理类增强了。invoke的代码为

  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. try {
  3. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
  4. // 如果是定义的拦截的方法 就执行intercept方法
  5. if (methods != null && methods.contains(method)) {
  6. // 进入查看 该方法增强
  7. return interceptor.intercept(new Invocation(target, method, args));
  8. }
  9. // 不是需要拦截的方法 直接执行
  10. return method.invoke(target, args);
  11. } catch (Exception e) {
  12. throw ExceptionUtil.unwrapThrowable(e);
  13. }
  14. }
  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. public Object intercept(Invocation invocation) throws Throwable {
  9. if (autoRuntimeDialect) {
  10. SqlUtil sqlUtil = getSqlUtil(invocation);
  11. return sqlUtil.processPage(invocation);
  12. } else {
  13. if (autoDialect) {
  14. initSqlUtil(invocation);
  15. }
  16. return sqlUtil.processPage(invocation);
  17. }
  18. }

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

5.分页过程

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

5.1 startPage

PageHelper.startPage(1, 5);

  1. /**
  2. * 开始分页
  3. *
  4. * @param params
  5. */
  6. public static <E> Page<E> startPage(Object params) {
  7. Page<E> page = SqlUtil.getPageFromObject(params);
  8. //当已经执行过orderBy的时候
  9. Page<E> oldPage = SqlUtil.getLocalPage();
  10. if (oldPage != null && oldPage.isOrderByOnly()) {
  11. page.setOrderBy(oldPage.getOrderBy());
  12. }
  13. SqlUtil.setLocalPage(page);
  14. return page;
  15. }
  1. /**
  2. * 开始分页
  3. *
  4. * @param pageNum 页码
  5. * @param pageSize 每页显示数量
  6. * @param count 是否进行count查询
  7. * @param reasonable 分页合理化,null时用默认配置
  8. */
  9. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
  10. return startPage(pageNum, pageSize, count, reasonable, null);
  11. }
  1. /**
  2. * 开始分页
  3. *
  4. * @param offset 页码
  5. * @param limit 每页显示数量
  6. * @param count 是否进行count查询
  7. */
  8. public static <E> Page<E> offsetPage(int offset, int limit, boolean count) {
  9. Page<E> page = new Page<E>(new int[]{offset, limit}, count);
  10. //当已经执行过orderBy的时候
  11. Page<E> oldPage = SqlUtil.getLocalPage();
  12. if (oldPage != null && oldPage.isOrderByOnly()) {
  13. page.setOrderBy(oldPage.getOrderBy());
  14. }
  15. // 这是重点!!!
  16. SqlUtil.setLocalPage(page);
  17. return page;
  18. }
  1. private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
  2. // 将分页信息保存在ThreadLocal中 线程安全!
  3. public static void setLocalPage(Page page) {
  4. LOCAL_PAGE.set(page);
  5. }

5.2selectList方法

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

image.png

我们需要回到invoke方法中继续看

  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. public Object intercept(Invocation invocation) throws Throwable {
  9. if (autoRuntimeDialect) {
  10. SqlUtil sqlUtil = getSqlUtil(invocation);
  11. return sqlUtil.processPage(invocation);
  12. } else {
  13. if (autoDialect) {
  14. initSqlUtil(invocation);
  15. }
  16. return sqlUtil.processPage(invocation);
  17. }
  18. }

进入sqlUtil.processPage(invocation);方法

  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. private Object _processPage(Invocation invocation) throws Throwable {
  9. final Object[] args = invocation.getArgs();
  10. Page page = null;
  11. //支持方法参数时,会先尝试获取Page
  12. if (supportMethodsArguments) {
  13. // 从线程本地变量中获取Page信息,就是我们刚刚设置的
  14. page = getPage(args);
  15. }
  16. //分页信息
  17. RowBounds rowBounds = (RowBounds) args[2];
  18. //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
  19. if ((supportMethodsArguments && page == null)
  20. //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
  21. || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
  22. return invocation.proceed();
  23. } else {
  24. //不支持分页参数时,page==null,这里需要获取
  25. if (!supportMethodsArguments && page == null) {
  26. page = getPage(args);
  27. }
  28. // 进入查看
  29. return doProcessPage(invocation, page, args);
  30. }
  31. }
  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
  9. //保存RowBounds状态
  10. RowBounds rowBounds = (RowBounds) args[2];
  11. //获取原始的ms
  12. MappedStatement ms = (MappedStatement) args[0];
  13. //判断并处理为PageSqlSource
  14. if (!isPageSqlSource(ms)) {
  15. processMappedStatement(ms);
  16. }
  17. //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
  18. ((PageSqlSource)ms.getSqlSource()).setParser(parser);
  19. try {
  20. //忽略RowBounds-否则会进行Mybatis自带的内存分页
  21. args[2] = RowBounds.DEFAULT;
  22. //如果只进行排序 或 pageSizeZero的判断
  23. if (isQueryOnly(page)) {
  24. return doQueryOnly(page, invocation);
  25. }
  26. //简单的通过total的值来判断是否进行count查询
  27. if (page.isCount()) {
  28. page.setCountSignal(Boolean.TRUE);
  29. //替换MS
  30. args[0] = msCountMap.get(ms.getId());
  31. //查询总数
  32. Object result = invocation.proceed();
  33. //还原ms
  34. args[0] = ms;
  35. //设置总数
  36. page.setTotal((Integer) ((List) result).get(0));
  37. if (page.getTotal() == 0) {
  38. return page;
  39. }
  40. } else {
  41. page.setTotal(-1l);
  42. }
  43. //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
  44. if (page.getPageSize() > 0 &&
  45. ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
  46. || rowBounds != RowBounds.DEFAULT)) {
  47. //将参数中的MappedStatement替换为新的qs
  48. page.setCountSignal(null);
  49. // 重点是查看该方法
  50. BoundSql boundSql = ms.getBoundSql(args[1]);
  51. args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
  52. page.setCountSignal(Boolean.FALSE);
  53. //执行分页查询
  54. Object result = invocation.proceed();
  55. //得到处理结果
  56. page.addAll((List) result);
  57. }
  58. } finally {
  59. ((PageSqlSource)ms.getSqlSource()).removeParser();
  60. }
  61. //返回结果
  62. return page;
  63. }

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

  1. @Override
  2. protected BoundSql getPageBoundSql(Object parameterObject) {
  3. String tempSql = sql;
  4. String orderBy = PageHelper.getOrderBy();
  5. if (orderBy != null) {
  6. tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
  7. }
  8. tempSql = localParser.get().getPageSql(tempSql);
  9. return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
  10. }

image.png
image.png
也可以看Oracle的分页实现

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