之前项目一直使用的是普元框架,最近公司项目搭建了新框架,主要是由公司的大佬搭建的,以springboot为基础。为了多学习点东西,我也模仿他搭了一套自己的框架,但是在完成分页功能的时候,确遇到了问题。

框架的分页组件使用的是pagehelper,对其我也是早有耳闻,但是也是第一次接触(ps:工作1年,一直使用的是普元封装好的前端框架)。

要是用pagehelper,首先maven项目,要引入

1
2
3
4
5
<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>4.1.6</version>
</dependency>


前端使用的bootstrap分页插件,这里不再赘述,直接切入正题,持久层框架使用的是mybatis,分页插件的URL,指向后台的controller,

@ResponseBody
@RequestMapping("/testPage")
public String testPage(HttpServletRequest request) {
Page<PmProduct> page = PageUtil.pageStart(request);
List<PmProduct> list = prodMapper.selectAll();
JSONObject rst = PageUtil.pageEnd(request, page, list);
return rst.toString();
}

这是controller的代码,当时就产生一个疑问,为啥在这个pageStart后面查询就能够实现分页呢?可以查出想要的10条分页数据,而去掉则是全部查询出来的84条记录。

带着问题,我开始了我的debug之旅,首先,我跟进pageStart方法,这个PageUtil类是公司大佬封装的一个工具类,代码如下:

public class PageUtil {

    public enum CNT{
total,
res
} public static <E> Page<E> pageStart(HttpServletRequest request){
return pageStart(request,null);
} /**
*
* @param request
* @param pageSize 每页显示条数
* @param orderBy 写入 需要排序的 字段名 如: product_id desc
* @return
*/
public static <E> Page<E> pageStart(HttpServletRequest request,String orderBy){ int pageon=getPageon(request);
int pageSize=getpageSize(request);
Page<E> page=PageHelper.startPage(pageon, pageSize);
if(!StringUtils.isEmpty(orderBy)){
PageHelper.orderBy(orderBy);
} return page;
} private static int getPageon(HttpServletRequest request){
String pageonStr=request.getParameter("pageon");
int pageon;
if(StringUtils.isEmpty(pageonStr)){
pageon=1;
}else{
pageon=Integer.parseInt(pageonStr);
}
return pageon;
} private static int getpageSize(HttpServletRequest request){
String pageSizeStr=request.getParameter("pageSize");
int pageSize;
if(StringUtils.isEmpty(pageSizeStr)){
pageSize=1;
}else{
pageSize=Integer.parseInt(pageSizeStr);
}
return pageSize;
} /**
*
* @param request
* @param page
* @param list
* @param elName 页面显示所引用的变量名
*/
public static JSONObject pageEnd(HttpServletRequest request, Page<?> page,List<?> list){
JSONObject rstPage=new JSONObject();
rstPage.put(CNT.total.toString(), page.getTotal());
rstPage.put(CNT.res.toString(), list);
return rstPage;
} }

可以看到,pageStart有两个方法重载,进入方法后,获取了前端页面传递的pageon、pageSize两个参数,分别表示当前页面和每页显示多少条,然后调用了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 = SqlUtil.getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
SqlUtil.setLocalPage(page);
return page;
}

上面的方法才是真正分页调用的地方,原来是对传入参数的赋值,赋给Page这个类,继续,发现getLocalPage和setLoaclPage这两个方法,很可疑,跟进,看看他到底做了啥,

public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
} public static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
} private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

哦,有点明白了,原来就是赋值保存到本地线程变量里面,这个ThreadLocal是何方神圣,居然有这么厉害,所以查阅了相关博客,链接:https://blog.csdn.net/u013521220/article/details/73604917,个人简单理解大概意思是这个类有4个方法,set,get,remove,initialValue,可以使每个线程独立开来,参数互不影响,里面保存当前线程的变量副本。

OK,那这个地方就是保存了当前分页线程的Page参数的变量。有赋值就有取值,那么在下面的分页过程中,肯定在哪边取到了这个threadLocal的page参数。

好,执行完startPage,下面就是执行了mybatis的SQL语句,

1
2
3
4
5
6
<select id="selectAll" resultMap="BaseResultMap">
  select SEQ_ID, PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESC, CREATE_TIME, EFFECT_TIME,
  EXPIRE_TIME, PRODUCT_STATUS, PROVINCE_CODE, REGION_CODE, CHANGE_TIME, OP_OPERATOR_ID,
  PRODUCT_SYSTEM, PRODUCT_CODE
  from PM_PRODUCT
</select>

SQL语句很简单,就是简单的查询出PM_PRODUCT的全部记录,那到底是哪边做了拦截吗?带着这个疑问,我跟进了代码,

发现进入了mybatis的MapperPoxy这个代理类,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

最后执行的execute方法,再次跟进,进入MapperMethod这个类的execute方法,

public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else if (SqlCommandType.FLUSH == command.getType()) {
result = sqlSession.flushStatements();
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

由于执行的是select操作,并且查询出多条,所以就到了executeForMany这个方法中,后面继续跟进代码SqlSessionTemplate,DefaultSqlSession(不再赘述),最后可以看到代码进入了Plugin这个类的invoke方法中,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
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);
}
}

这下明白了,interceptor是mybatis的拦截器,而PageHelper这个类就实现了interceptor接口,调用其中的intercept方法。

/**
* 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);
}
} /**
* 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 = 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);
}
}

最终我在SqlUtil中的_processPage方法中找到了,getPage这句话,getLocalPage就将保存在ThreadLocal中的Page变量取了出来,这下一切一目了然了,


跟进代码,发现进入了doProcessPage方法,通过反射机制,首先查询出数据总数量,然后进行分页SQL的拼装,MappedStatement的getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
} // check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
} return boundSql;
}

继续,跟进代码,发现,最终分页的查询,调到了PageStaticSqlSource类的getPageBoundSql中,

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

进入getPageSql这个方法,发现,进入了OracleParser类中(还有很多其他的Parser,适用于不同的数据库),

public String getPageSql(String sql) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
sqlBuilder.append("select * from ( select tmp_page.*, rownum row_id from ( ");
sqlBuilder.append(sql);
sqlBuilder.append(" ) tmp_page where rownum <= ? ) where row_id > ?");
return sqlBuilder.toString();
}

终于,原来分页的SQL是在这里拼装起来的。

总结:PageHelper首先将前端传递的参数保存到page这个对象中,接着将page的副本存放入ThreadLoacl中,这样可以保证分页的时候,参数互不影响,接着利用了mybatis提供的拦截器,取得ThreadLocal的值,重新拼装分页SQL,完成分页。

浅析 pagehelper 分页的更多相关文章

  1. 浅析pagehelper分页原理(转)

    之前项目一直使用的是普元框架,最近公司项目搭建了新框架,主要是由公司的大佬搭建的,以springboot为基础.为了多学习点东西,我也模仿他搭了一套自己的框架,但是在完成分页功能的时候,确遇到了问题. ...

  2. 浅析pagehelper分页原理

    原文链接 https://blog.csdn.net/qq_21996541/article/details/79796117 之前项目一直使用的是普元框架,最近公司项目搭建了新框架,主要是由公司的大 ...

  3. Springboot 系列(十二)使用 Mybatis 集成 pagehelper 分页插件和 mapper 插件

    前言 在 Springboot 系列文章第十一篇里(使用 Mybatis(自动生成插件) 访问数据库),实验了 Springboot 结合 Mybatis 以及 Mybatis-generator 生 ...

  4. PageHelper分页插件的使用

    大家好!今天写ssm项目实现分页的时候用到pageHelper分页插件,在使用过程中出现了一些错误,因此写篇随笔记录下整个过程 1.背景:在项目的开发的过程中,为了实现所有的功能. 2.目标:实现分页 ...

  5. SpringBoot整合系列-PageHelper分页插件

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9971043.html SpringBoot整合MyBatis分页插件PageHelper ...

  6. SpringBoot+Mybatis配置Pagehelper分页插件实现自动分页

    SpringBoot+Mybatis配置Pagehelper分页插件实现自动分页 **SpringBoot+Mybatis使用Pagehelper分页插件自动分页,非常好用,不用在自己去计算和组装了. ...

  7. 记录pageHelper分页orderby的坑

    pageHelper的count查询会过滤查询sql中的order by条件! pageHelper分页功能很强大,如果开启count统计方法,在你执行查询条件时会再执行一条selet count(* ...

  8. mybatis pagehelper分页插件使用

    使用过mybatis的人都知道,mybatis本身就很小且简单,sql写在xml里,统一管理和优化.缺点当然也有,比如我们使用过程中,要使用到分页,如果用最原始的方式的话,1.查询分页数据,2.获取分 ...

  9. spring boot 整合pagehelper分页插件

    Spring Boot 整合pagehelper分页插件 测试环境: spring boot  版本 2.0.0.M7 mybatis starter 版本  1.3.1 jdk 1.8 ------ ...

随机推荐

  1. Prometheus入门到放弃(3)之Grafana展示监控数据

    grafana我们这里采用docker方式部署 1.下载镜像 镜像官网地址:https://hub.docker.com/r/grafana/grafana/tags [root@prometheus ...

  2. 《TCP/IP - TCP/UDP》

    一:概述 - 由于 IP 的传输是无状态的,IP 提供尽力服务,但并不保证数据可以到达主机. - 所以,数据的完整性需要更上层的 传输层来保证.TCP和UDP 均属于 传输层. 二:UDP - 特点 ...

  3. 题解 Luogu P3959 【宝藏】

    来一篇不那么慢的状压??? 话说这题根本没有紫题难度吧,数据还那么水 我是不会告诉你我被hack了 一看数据规模,n≤12,果断状压. 然后起点要枚举,就设dp状态: f[i][j]=以i为起点到j状 ...

  4. Python 爬取陈都灵百度图片

    Python 爬取陈都灵百度图片 标签(空格分隔): 随笔 今天意外发现了自己以前写的一篇爬虫脚本,爬取的是我的女神陈都灵,尝试运行了一下发现居然还能用.故把脚本贴出来分享一下. import req ...

  5. 14.Python略有小成(自由模块)

    Python(模块) 一.模块定义与分类 ​ 我们说一个函数就是一个功能,那么把一些常用的函数放在一个py文件中,这个文件就称之为模块,模块,就是一些列常用功能的集合体,模块就是文件,存放一堆常用的函 ...

  6. Linux下使用ip netns命令进行网口的隔离和配置ip地址

    1. 添加隔离标记符: ip netns add fd 2. 将指定网卡放入隔离中: ip link set eth1 netns fd 3. 在隔离环境下执行命令: ip netns exec fd ...

  7. COGS 有标号的DAG/强连通图计数

    COGS索引 一堆神仙容斥+多项式-- 有标号的DAG计数 I 考虑\(O(n^2)\)做法:设\(f_i\)表示总共有\(i\)个点的DAG数量,转移考虑枚举DAG上所有出度为\(0\)的点,剩下的 ...

  8. Unity项目 - MissionDemolition 愤怒的小鸟核心机制

    目录 游戏原型 项目演示 绘图资源 代码实现 注意事项 技术探讨 参考来源 游戏原型 爆破任务 MissionDemolition 是一款核心机制类似于愤怒的小鸟的游戏,玩家将用弹弓发射炮弹,摧毁城堡 ...

  9. 转 让NET C# 程序独立运行(脱离 .NET Framework运行,绿色运行) 未验证

    但是.net版本众多.而且.NET Framework框架很大.拖着一个大大的.net Framework总是让人很郁闷. 在网上找呀找呀.找到另一个.NET Framework 替代方案.Mono. ...

  10. split()方法 splice()方法 slice()方法

    split()方法是对字符串的操作:splice()和slice()是对数组的操作.slice()也可用于字符串. 一.作用对象 1.split()方法是对字符串的操作:splice()和slice( ...