Pagehelper的reasonable配置引起分页失效问题

Pagehelper的reasonable配置引起分页失效问题

一个意外的接口请求,引发了刨根问底的冲动

某天突然一个意外地请求,前端是很正常的列表请求,引发了两个问题:

​ 1、get请求有传pageNumpageSize参数,查询数据库是正常的基础select查询,没有写limit ?,? 分页,也没查到分页配置bean,mybatis拦截器等,但是实现了分页效果

​ 2、请求查询时传了分页,但是永远返回第一页的数据。

一怒之下,必须搞清楚来龙去脉

​ 这让人有点很想了解他怎么实现的冲动,于是开始打断点debug,分析了一波源码。最后两个问题通过阅读源码均解决了,下面描述下两个问题的排查过程

第一个问题排查,没有传入limit,却实现了分页,难道get请求传入pageNumpageSize就能自动分页,太神奇了吧?

先看下代码结构,简易代码:

java 复制代码
//Controller代码:
@GetMapping("/community/getRecordByPage")
@EnablePage
public PageResult getRecordByPage(CommunityRecordDO record) {
    try {
        List<CommunityArSplitRecordDO> list = recordService.getRecordByPage(record);
        return getPageResult(list);
    } catch (Exception e) {
        log.error("xxxxx,MSG:{}", e.getMessage(), e);
        return null;
    }
}

//Service代码:
public List<CommunityRecordDO> getRecordByPage(CommunityRecordDO record) {
    return recordMapper.getRecordByPage(record);
}

//mapper
List<CommunityRecordDO> getRecordByPage(CommunityRecordDO record);

//xml
<select id="getRecordByPage" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from t_community_ar_split_record
    order by create_time desc
</select>
    
//请求地址
localhost:8080/api/community/getRecordByPage?pageNum=1&pageSize=10

可以看得出,代码结构非常简单,就普通的列表查询,重点:没有写limit ?,? 但是他却实现了分页,给前端返回了分页内容。

排查步骤:

1、项目是springboot,查看了下是否有分页的bean配置,全局搜了,并没有。

2、查看了整个项目的pom.xml文件,有引入pagehelper,这会算是基本找到方向了(问题刚刚开始)

xml 复制代码
<!-- pagehelper 分页插件 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>${pagehelper.spring.boot.starter.version}</version>
</dependency>

3、有引入插件,那应该需要配置mybatis的拦截器到Springbean容器里面啊?以前做SpringMVC的是后均是要配置的,但是全局找项目又没找到,后面想了一下Springboot的自动装配,是不是pagehelper-spring-boot-starter自动装配帮忙配了,搜了一下,果不其然,确实是自动配置了(自动装配果然名不虚传)

4、不需要配置拦截器的原因找到了,那我们正常使用pagehelper时一般都是要在执行sql前写一段代码,证明开启了分页的,但是这里也没有写

java 复制代码
//分页开启
PageHelper.startPage(pageNum, pageSize);

5、debug执行mapper代理的源码,跟踪到拦截器这一步,确确实实自动加上了limit,很神奇

6、然后再往上一步如何赋值的地方溯源跟踪,发现拿到这个page对象时候,确实是已经有默认值了,那在什么时候赋值的呢?继续往后查

进入getLocalPage()方法,实际就是一个LOCAL_PAGE对象,还要继续往前,看什么时候set进去的

set处打断点,然后根据堆栈往前找,最终找到了原来是源码包做了个注解切面@EnablePage,每次使用这个注解时候,就会走这个切面,然后给全局调用PageHelper.startPage(pageNum, pageSize)

7、已经找到源头了,那@EnablePage在什么时候加的,然后pageNumpageSize是从哪里来的,其实问题已经解决了,Controller那一层确实有一个注解,这个注解不是spring注解,还有get请求带上了两个参数,这两个参数就是分页配置参数,无非就是从请求地址获取这两个参数做一个全局的开始分页切面

java 复制代码
//请求地址
localhost:8080/api/community/getRecordByPage?pageNum=1&pageSize=10
    
//Controller代码:
@GetMapping("/community/getRecordByPage")
//分页注解
@EnablePage
public PageResult getRecordByPage(CommunityRecordDO record) {
    try {
        List<CommunityArSplitRecordDO> list = recordService.getRecordByPage(record);
        return getPageResult(list);
    } catch (Exception e) {
        log.error("xxxxx,MSG:{}", e.getMessage(), e);
        return null;
    }
}

到这,第一个问题已经解决了,本以为加上pagehelper自动配置,都不需要配置mybatis拦截器,也不需要写PageHelper.startPage(pageNum, pageSize),界面get请求传入pageNumpageSize就可以实现分页,实则并没有这么牛,框架并不知道你业务,所以不会做这种全局的配置。


第二个问题,count记录只有3条,但是传了pageNum=2pageSize=10,但是一直返回第一页的3条数据,按道理应该返回空,因为不存在第二页

代码结构不变,跟第一个问题是相同代码。传入pageNum=2pageSize=10,返回第一页的数据,即使把pageNum改成3,4,5也是返回第一页数据

java 复制代码
//请求第一页
localhost:8080/api/community/getRecordByPage?pageNum=1&pageSize=10
//请求第二页
localhost:8080/api/community/getRecordByPage?pageNum=2&pageSize=10
//请求第三页
localhost:8080/api/community/getRecordByPage?pageNum=3&pageSize=10

排查步骤:

1、本次思路也是按照第一个方法的步骤去debug跟踪源码排查,找到page这个对象,查看它的pageNum值是多少

url 复制代码
localhost:8080/api/community/getRecordByPage?pageNum=2&pageSize=10

可以看到,切面是实质拿到了pageNum=2,那为什么执行时候返回的是第一页数据呢?

2、排查实际执行的sqldebuggetPageSql这个方法,拿到的page对象,pageNum确实是1,那可以猜测,从切面设置为2开始,中途肯定有什么方法让他改变成1

3、到这里代码太多,不可能一步步走,此时需要监听pageNum这个字段,看它什么时候变化

第一次赋值是2,跳过

第二次赋值也是2,跳过

第三次赋值是1,好家伙,就是他了,顺着堆栈往前看,具体是哪里改变的

贴一下执行顺序,其实是在分页插件拦截器的afterCount里面有一个重新赋值的setTotal方法,然后对pageNum做了重新计算,一些列操作,把它计算成了1

4、问题找到了,那为啥会自动计算成1呢?其实从备注也看出来了,是为了防止分页不合理

比如:

1、查询页数比实际页数大,防止不合理,会查询最后一页

2、传递一个负数页数,返回第一页

总之这是一个为了防止安全场景考虑的自动计算,主要是靠reasonable=true控制的,因为只有这个字段为true,才会走到pageNum = pages,如果为false,则不会重新赋值,像我的案例,那pageNum应该还是2,不应该是1

问题是找到了,那又想知道reasonable是在什么时候赋值的呢?代码都看到这了,那就把它看完整把,继续监听该字段的变化

此时入参已经是true

往上看,入参是一个全局变量,也为true,默认值是false,所以还是有地方改变了

最终找到是获取配置项的,只有在项目启动初期赋值

搜了一下yml,确实有这个值配置

到此,问题是已经排查清楚了,是因为在yml配置pagehelper的分页合理化配置,导致了分页一直返回第一页的bug,其实也不算是bug,应该是属于分页合理化考虑,防止本来就没有第二页数据,硬是要查第二页的信息

如果需要正确返回分页信息,三种方式:

1、不配置该字段,默认值就是false

2、配置该字段,设置为false

3、使用PageHelper.startPage的重载方法,把false传入


后记

​ 小小的两个bug花了竟然快2个小时,一点点debug分析,观察字段变化,反复请求从头来,真费神费脑,但是结果是快乐的,又通透了一边PageHelpermybatis的源码流程。

得出最后结论的那一刻,突然想到使用这个分页合理化参数,会导致某种死循环问题,比如代码写法:

java 复制代码
int pageNum = 1;
int pageSize = 10;
List<String> result = new ArrayList();
while(true){
    List<String> tempList = mapper.select(pageNum,pageSize);
    if(tempList.isEmpty){
        return;
    }
    result.add(tempList);
    pageNum++;
}

有时候我们做批量导出时,某张表数据量很大的时候,不可能一次性全读出来,那么这时候就会分页取,通过取不到值去结束循环,结果分页到最后一页再往后时,由于分页合理化配置,pageNum++继续增加,结果都是最后一页,那么循环就无法结束,导致死循环。

所以这个参数个人感觉还是使用默认配置false即可,不需要是否合理化,到底合不合理,由业务代码来评估。

相关推荐
David爱编程10 分钟前
Java 守护线程 vs 用户线程:一文彻底讲透区别与应用
java·后端
小奏技术28 分钟前
国内APP的隐私进步,从一个“营销授权”弹窗说起
后端·产品
小研说技术1 小时前
Spring AI存储向量数据
后端
苏三的开发日记1 小时前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台处于同一台服务器)
后端
苏三的开发日记1 小时前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台不在同一服务器)
后端
陈三一1 小时前
MyBatis OGNL 表达式避坑指南
后端·mybatis
whitepure1 小时前
万字详解JVM
java·jvm·后端
我崽不熬夜1 小时前
Java的条件语句与循环语句:如何高效编写你的程序逻辑?
java·后端·java ee
我崽不熬夜2 小时前
Java中的String、StringBuilder、StringBuffer:究竟该选哪个?
java·后端·java ee
我崽不熬夜2 小时前
Java中的基本数据类型和包装类:你了解它们的区别吗?
java·后端·java ee