Mybatis之分页插件PageHelper工作原理

2023-10-31

前言

数据分页功能是软件系统中必备的功能,在持久层使用mybatis的情况下,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>

配置插件

要使用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>

加载过程

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

// 获取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 通过加载配置文件获取SqlSessionFactory对象
// 将PageHelper拦截器加载到了拦截器链中
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession对象 获取了一个Executor的增强代理类
SqlSession session = factory.openSession();
// 将 pageNum和pageSize封装到了 Page对象中,同时将Page对象保存到了当前线程的本地变量中的。
PageHelper.startPage(1, 5);
session.selectList("com.yjn.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对象的获取,我们加载了全局配置文件及映射文件同时还将配置的拦截器添加到了拦截器链中。

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中已经定义了该拦截器拦截的方法是什么。

Executor

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

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

å¨è¿éæå¥å¾çæè¿°

å¨è¿éæå¥å¾çæè¿°

å¨è¿éæå¥å¾çæè¿°

 

å¨è¿éæå¥å¾çæè¿°

 

å¨è¿éæå¥å¾çæè¿°

到此我们明白了,Executor对象其实被我们生存的代理类增强了.

PageHelper实现分页的具体过程

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);
}

selectList

session.selectList("com.yjn.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);
}
/**
 * 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);
}

å¨è¿éæå¥å¾çæè¿°

å¨è¿éæå¥å¾çæè¿°

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Mybatis之分页插件PageHelper工作原理 的相关文章

  • 面向对象(多态)

    面向对象 多态 一 多态 同一个对象具有多种形态 二 作用 核心在于方法的重写 覆写 使多种数据类型进行统一 让程序具有超强的扩展性 三 模拟人物进行网络游戏 类图 创建游戏父类 public class Game public void

随机推荐

  • 一个使用MongoDB和Fuse开发的虚拟文件系统

    https sourceforge net projects gridfs fuse files source navbar 以前基于这个Demo开发了一个文件系统 记录一下
  • idea 找不到tomcat、Application Servers配置

    idea 找不到tomcat Application Servers配置 在网上找到的解决方式是File Settings Plugins 中搜索tomcat 如上图显示 我本地已经安装tomcat插件了 又在网上找到说File Setti
  • PAL制搜台

    PAL电视制式 PAL电视制式 Phase Alternating Line 采用625线制式 视讯制式采用PAL B G PAL D K PAL I等 PAL电视不像NTSC制式有中心频点 它采用宽带的频率范围进行电视信号的调制和传输 P
  • Windows在命令行中直接使用conda虚拟环境以及一些实用的conda命令

    在命令行中输入conda env list 可以查看当前已有的全部conda虚拟环境 如下图 输入conda create n name python version可以创建指定python版本的名称为name的虚拟环境 输入conda c
  • Linux常用命令-2

    为了掌握本关知识 你需要掌握 Linux文件操作之创建文件 删除文件 Linux文件夹操作之创建文件夹 删除文件夹 文件和文件夹拷贝 移动 重命名 Linux文件操作 Linux系统中最常用的文件操作有创建 删除文件等 创建文件 linux
  • 【LaTeX Workshop】VS Code 与 SumatraPDF 双向链接配置方法

    TeX Live 2021 Sumatra PDF v3 4 6 64 bit VS Code v1 62 3 system setup LaTeX Workshop v8 22 0 SumatraPDF 配置 安装 SumatraPDF
  • luckysheet使用——导出并下载excel数据及钩子函数的使用等

    这里记录一些常用的API 包括对于获取整个在线excel的数据 使用钩子函数等 获取Excel数据luckysheet getAllSheets 可以将获取到的数据传递到后端通过JSONArray来获取 它是一个一个的sheet页对象的数组
  • C语言基础入门48篇_15_条件运算符(expr1?expr2:expr3,三元运算符,

    条件运算符是C语言中唯一的三元运算符 其基本形式为 expr1 expr2 expr3 一个条件运算符它需要三个表达式 如果expr1的值为非0 则整个条件表达式的值为expr2 如果expr1的值为0 则整个条件表达式的值为expr3 1
  • java和c++区别

    java string类写在heap中 无法修改 c 放在text区 可以修改 java 的块内镶嵌的块不允许变量名重复 c 允许 实行就近法则 转载于 https www cnblogs com bloomingFlower p 1161
  • saltstack之系统初始化状态编写

    saltstack之系统初始化状态编写 1 目录结构 2 关闭selinux 3 关闭防火墙 4 时间同步 chrony 5 内核优化与文件描述 6 ssh服务优化 7 精简开机系统服务 8 历史记录优化 设置终端超时时间 9 yum源 1
  • 数据库基础知识(MySQL入门)

    数据库基础知识 MySQL入门 文章目录 数据库基础知识 MySQL入门 一 基本概念 二 特点 一 基本概念 Information 信息是对客观世界中各种事物的运动状态和变化的反映 是数据的含义 Data 数据用来记录信息的可识别符号
  • java基本语法题目_JAVA的语法基础 练习题

    1 编写Java程序 声明两个int型变量 运用三元运算符判断两个变量是否相等 若不相等 求出两个数中较大的 public class 练习题 public static void main String args int a 10 int
  • 验证集准确率上不去_经验之谈|处理不平衡数据集的7个技巧

    作者 Ye Wu Rick Radewagen编译 ronghuaiyang 介绍 具体的领域中的数据集是什么样的 银行中的欺诈检测 市场中的实时投标 网络中的入侵检测 常见吗 这些领域中的数据 常常只有不到1 的少数 但是 有兴趣 的事件
  • jmeter 固定定时器

    固定定时器 Constant Timer 是一个定时器元件 可以在线程组中的每个线程之间添加固定的延迟时间 固定定时器会对每个线程的执行进行一定的暂停 聊一下和线程组中的调度器对线程组执行时长的影响 相同 都会影响线程组的运行时长 不同 固
  • 加载动态库失败(loadLibrary返回为空 GetLastError126)解决办法 dll有依赖的dll缺失

    问题 加载动态库失败 loadLibrary返回为空 排除 64位也对 平台相同 错误 至少找不到一个必需的隐式或转发依赖项 这个不影响 SmartPay PGL dll下的四个dll 则是他所依赖的四个dll 因为我这里有缺失 显示黄色的
  • 本地ubuntu22.04装cuda11.7+torch2.0.1流程记录

    想要更新cuda11 7 需要的驱动最低版本515 65 01 而我的电脑原驱动为470 需要更新 1 卸载原驱动 使用了命令 sudo apt get purge nvidia sudo apt get autoremove 用以下命令检
  • MATLAB矩阵乘法14例

    MATLAB矩阵乘法14例 简介 矩阵乘法是线性代数中的基本运算之一 也是MATLAB中的重要运算 矩阵乘法的结果是两个矩阵的乘积 其中一个矩阵的列数等于另一个矩阵的行数 在这篇文章中 我们将介绍20个MATLAB矩阵乘法的例子 帮助您更好
  • 什么是自动化测试框架?我们该如何搭建自动化测试框架?

    无论是在自动化测试实践 还是日常交流中 经常听到一个词 框架 之前学习自动化测试的过程中 一直对 框架 这个词知其然不知其所以然 最近看了很多自动化相关的资料 加上自己的一些实践 算是对 框架 有了一些理解 这篇博客 就聊聊自动化框架的一些
  • 根因定位FluxRank论文通过核密度估计(KDE)获得变化量部分

    II CHANGE QUANTIFICATION CHANGE QUANTIFICATION说明 A 变化开始时间 B 变化程度 论文 FluxRank A Widely Deployable Framework to Automatica
  • Mybatis之分页插件PageHelper工作原理

    前言 数据分页功能是软件系统中必备的功能 在持久层使用mybatis的情况下 pageHelper来实现后台分页则是我们常用的一个选择 所以本文专门介绍下 需要的依赖