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

1. 原理概述

PageHelper是MyBatis的一个插件,内部实现了一个PageInterceptor拦截器。Mybatis会加载这个拦截器到拦截器链中。在我们使用过程中先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用PageInterceptor这个分页拦截器拦截,从ThreadLocal中拿到分页的信息,如果有分页信息拼装分页SQL(limit语句等)进行分页查询,最后再把ThreadLocal中的东西清除掉。

2. 使用注意事项

PageHelper使用了ThreadLocal保存分页参数,分页参数和线程是绑定的。因此我们需要保证PageHelper 的startPage调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

如果代码在进入Executor前发生异常,就会导致线程不可用,这属于人为Bug(例如接口方法和XML中的不匹配,导致找不到MapppedStatement时)。这种情况由于线程不可用,也不会导致 ThreadLocal参数被错误使用。

但是如果写出以下代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){list = countryMapper.selectIf(param1);
} else {list = new ArrayList<Country>();
}

这种情况下由于param1存在null的情况,就会导致PageHelper产生了一个分页参数,但是没有被消费,也没有被清理。这个参数就会一直保存在ThreadLocal中。可能导致其它不该分页的方法去消费了这个参数,莫名其妙的做了分页。

此外如果考虑到发生异常,可以加一个finally块,手动清理参数。

3. 使用过程源码分析

3.1 PageHelper.startPage

我们先来看PageHelper.startPage这个静态方法

   /*** 开始分页** @param pageNum      页码* @param pageSize     每页显示数量* @param count        是否进行count查询* @param reasonable   分页合理化,null时用默认配置* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置*/public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {Page<E> page = new Page<E>(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);//当已经执行过orderBy的时候Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}setLocalPage(page);return page;}

上面的代码有个比较关键的地方setLocalPage(page),方法是这样的:

/*** 设置 Page 参数** @param page*/
protected static void setLocalPage(Page page) {LOCAL_PAGE.set(page);
}

再来看LOCAL_PAGE的定义

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

终于明白了,是基于ThreadLocal,但是还没完,我们只看到了set的地方,却没有看到remove的地方,com.github.pagehelper.page.PageMethod类里有个clearPage方法:

/*** 移除本地变量*/
public static void clearPage() {LOCAL_PAGE.remove();
}

清除本地线程变量的就是这个clearPage方法,PageHelper在PageInterceptor的finally代码段中调用clearPage方法清除了ThreadLocal存储的对象。

3.2 PageInterceptor

我们再来看一下PageInterceptor拦截器是如何使用的。

3.2.1 加载过程

首先我们看拦截器是如何被加载的,查看SqlSessionFactoryBuilder类的build方法

进入pluginElement方法

private void pluginElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {// 获取到内容:com.github.pagehelper.PageHelperString interceptor = child.getStringAttribute("interceptor");// 获取配置的属性信息Properties properties = child.getChildrenAsProperties();// 创建的拦截器实例Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();// 将属性和拦截器绑定interceptorInstance.setProperties(properties);// 这个方法需要进入查看configuration.addInterceptor(interceptorInstance);}}
}

addInterceptor就是加载拦截器的方法

public void addInterceptor(Interceptor interceptor) {// 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合interceptorChain.addInterceptor(interceptor);}

通过SqlSessionFactory对象的获取,加载了全局配置文件及映射文件同时还将配置的拦截器添加到了拦截器链中

3.2.2 PageHelper定义的拦截信息

我们来看看PageHepler的源代码片段

@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;//运行时自动获取dialectprivate boolean autoRuntimeDialect;//多数据源时,获取jdbcurl后是否关闭数据源private boolean closeConn = true;

这里可以看到通过注解,定义的是拦截 Executor对象中的query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)这个方法。

3.2.3 Executor中发生的事情

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

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

再来看newExecutor中做了什么处理

这里循环调用了所有拦截器

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

3.2.4 selectList方法

当我们调用例如session.selectList("com.bobo.UserMapper.query")方法时,最终是交由Executor类的方法来处理

我们需要回到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);方法,这里会从ThreadLocal中获取分页信息

/*** Mybatis拦截器方法** @param invocation 拦截器入参* @return 返回执行结果* @throws Throwable 抛出异常*/
private Object _processPage(Invocation invocation) throws Throwable {final Object[] args = invocation.getArgs();Page page = null;//支持方法参数时,会先尝试获取Pageif (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];//获取原始的msMappedStatement ms = (MappedStatement) args[0];//判断并处理为PageSqlSourceif (!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);//替换MSargs[0] = msCountMap.get(ms.getId());//查询总数Object result = invocation.proceed();//还原msargs[0] = ms;//设置总数page.setTotal((Integer) ((List) result).get(0));if (page.getTotal() == 0) {return page;}} else {page.setTotal(-1l);}//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个countif (page.getPageSize() > 0 &&((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)|| rowBounds != RowBounds.DEFAULT)) {//将参数中的MappedStatement替换为新的qspage.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);
}

MySQL数据库分页实现

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

分页插件PageHelper工作原理相关推荐

  1. 【MyBatis】MyBatis分页插件PageHelper的使用

    转载自 https://www.cnblogs.com/shanheyongmu/p/5864047.html 好多天没写博客了,因为最近在实习,大部分时间在熟悉实习相关的东西,也没有怎么学习新的东西 ...

  2. 因为一个bug来深入探讨下分页插件PageHelper

    事情来源是这样的,因为某些操作失误,在使用分页插件pageHelper时,因为这样一句不起眼的操作,竟然引发了一系列的灾难,下面来看下灾难的由来: Page localPage = PageHelpe ...

  3. 框架 day74 涛涛商城项目整合ssm,分页插件pagehelper,商品列表查询

    讲师:入云龙 1.  课程计划 1. SSM框架整合 2. mybatis逆向工程 3. 商品列表 4. 商品列表分页处理 2.  SSM框架整合 2.1.  后台系统所用的技术 框架:Spring ...

  4. springboot进阶,分页插件 pageHelper,Swagger整合,日志

    文章目录 1,课程回顾 2,本章重点 3,具体内容 3.1 整合连接池 3.2 springboot日志配置: 3.3 springboot整合shiro 3.4 mybatis分页插件 pageHe ...

  5. 解决使用mybatis分页插件PageHelper的一个报错问题

    解决使用mybatis分页插件PageHelper的一个报错问题 参考文章: (1)解决使用mybatis分页插件PageHelper的一个报错问题 (2)https://www.cnblogs.co ...

  6. MyBatis分页插件PageHelper使用练习

    转载自:http://git.oschina.net/free/Mybatis_PageHelper/blob/master/wikis/HowToUse.markdown 1.环境准备: 分页插件p ...

  7. vue分页+spring boot +分页插件pagehelper

    vue分页+spring boot +分页插件pagehelper https://blog.csdn.net/baidu_38603246/article/details/98854013

  8. MyBatis学习总结(17)——Mybatis分页插件PageHelper

    2019独角兽企业重金招聘Python工程师标准>>> 如果你也在用Mybatis,建议尝试该分页插件,这一定是最方便使用的分页插件. 分页插件支持任何复杂的单表.多表分页,部分特殊 ...

  9. SpringBoot集成MyBatis的分页插件PageHelper

    [写在前面] 项目的后台管理系统需要展示所有资源信息,select * 虽然方便但数据量过于庞大会严重降低查找效率,页面加载慢,用户体验差.分页自然是必要选择,但原生的方法过于繁杂.MyBatis的分 ...

最新文章

  1. mybaits if判断进入不了
  2. 读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的
  3. 【Android RTMP】音频数据采集编码 ( FAAC 编码器编码 AAC 音频解码信息 | 封装 RTMP 音频数据头 | 设置 AAC 音频数据类型 | 封装 RTMP 数据包 )
  4. (转载)详解Hive配置Kerberos认证
  5. 用神经网络分类随机数与无理数
  6. 如何发表自己的第一篇SCI?
  7. Python字符型验证码识别
  8. JFreeChart(七)之气泡图表​​​​​​​
  9. 沈阳建筑大学c语言真题,沈阳建筑大学C语言复习资料.doc
  10. STM32之SDIO例程
  11. python3中的rang()函数
  12. Fix Elementary Boot Screen (plymouth) After Installing Nvidia Drivers
  13. 刚看到另外一个育儿作者收入是我的3000倍
  14. matlab-lsqcurvefit函数 初始值选取
  15. 令人拍案叫绝的Wasserstein GAN 及代码(WGAN两篇论文的中文详细介绍)
  16. 帝国时代2战役php文件,帝国时代各种类型文件使用说明
  17. 如何从零开始学习软件测试
  18. 【超详细】全国大学生软件测试大赛:移动应用测试参赛指南
  19. 离散数学复习/预习大纲
  20. 2019款ipad支持电容笔吗?Ipad2019电容笔推荐

热门文章

  1. python读取和保存json文件
  2. AI直播间互动管家使用教程,语音对接直播间
  3. python实现fofa调用api批量查询子域
  4. 洛谷P1424小鱼的航程(改进版)【C
  5. 用安卓手机juicessh连接linux系统
  6. 打印机服务print spooler 无法启动 错误代码1068
  7. 康威定律如何解释微服务的合理性
  8. 傅里叶变换分析A股大盘周期
  9. 免费课程-作业 误入这个论坛
  10. 阿里云服务器创建用户