心有多大,舞台就有多大。——巴尔扎克

近期有社区用户反馈,后台管理系统出现了严重分页 BUG:Selector 和 Rule 列表分页失效,始终只显示第一页,总数消失issue #6239)。作为 API 网关领域的核心组件,分页异常直接影响日常运维和平台体验。那么问题到底出在了哪里?又该如何优雅修复?本文带你一步步梳理、避坑、实践!


一、问题现象回顾

  • 主要表现:
    • Selector / Rule 的分页查询只能看到第一页,页码和总数全部失灵
    • 查询结果总是只有第一页内容

相关 issue:https://github.com/apache/shenyu/issues/6239
修复 PR:https://github.com/apache/shenyu/pull/6243


二、根本原因追溯 —— PageHelper 的使用陷阱

问题根因在于分页核心逻辑的隐藏陷阱

经典"陷阱"代码:

1
2
3
4
5
6
7
8
9
@Override
public PageInfo<SelectorVO> searchByPage(PageCondition<SelectorQueryCondition> pageCondition) {
doConditionPreProcessing(pageCondition.getCondition());
PageHelper.startPage(pageCondition.getPageNum(), pageCondition.getPageSize());
SelectorQueryCondition condition = pageCondition.getCondition();
condition.init();
// 这里searchByCondition的返回值不确定!!!
return new PageInfo<>(searchByCondition(pageCondition.getCondition()));
}

为什么是陷阱?

  • searchByCondition返回值类型是List<T>,但可能是普通的ArrayList,也可能是com.github.pagehelper.Page(PageHelper会返回Page对象,但如果下游有包裹、转换则可能不是)。

  • PageInfo 构造函数在源码如下:

    1
    2
    3
    4
    5
    6
    7
    public PageInfo(List<? extends T> list, int navigatePages) {
    if (list instanceof Page) {
    // 只有Page对象才能正常获取分页元数据
    } else {
    // 普通List没有分页信息
    }
    }
  • 一旦 searchByCondition 返回的不再是 Page,PageInfo 就只能包裹普通 List,无法拿到总数、页码等分页信息

案例回溯

  • 某些查询能保证 Mapper 直接返回 Page 对象(MyBatis + PageHelper联用,自动包装)。
  • 但由于调用链复用,searchByCondition可能返回值类型提前变成普通 List,分页能力被“吃掉”。

结论

new PageInfo<>(searchByCondition(…)) 属于易踩坑误导写法,只有拿到 Page 对象时才能安全用,复杂链路/多场景容易失效!


三、修复思路与改进实践

1. 提取 PageHelper 真分页能力

  • 必须保证分页 Bean 能准确拿到 Page 对象
  • 不能希望所有地方都用 PageHelper
  • 只动 Selector 和 Rule 模块(涉及分页失效,且最易受影响)

2. 优化代码实现

Selector 分页代码优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public PageInfo<SelectorVO> searchByPage(PageCondition<SelectorQueryCondition> pageCondition) {
doConditionPreProcessing(pageCondition.getCondition());
// 1. 正确开启分页
PageHelper.startPage(pageCondition.getPageNum(), pageCondition.getPageSize());
SelectorQueryCondition condition = pageCondition.getCondition();
condition.init();
// 2. 强制 Mapper 返回 Page
final Page<SelectorDO> page = (Page<SelectorDO>) selectorMapper.selectByCondition(condition);
// 3. 使用 Page 的 toPageInfo,才能正确获取所有分页元数据
PageInfo<SelectorVO> pageInfo = page.toPageInfo(SelectorVO::buildSelectorVO);
// 4. 补充展示信息
for (SelectorVO selector : pageInfo.getList()) {
selector.setMatchModeName(MatchModeEnum.getMatchModeByCode(selector.getMatchMode()));
selector.setTypeName(SelectorTypeEnum.getSelectorTypeByCode(selector.getType()));
}
return pageInfo;
}

Rule 分页同理,只需用 PageHelper 原生流程即可。

这样做的好处是:再也不用担心 searchByCondition 返回的是 ArrayList 还是 Page 对象,保证每次都能拿到核心分页能力!


四、相关源码片段及解释

PageHelper核心对象原理:

1
2
3
4
// PageHelper底层源码
public static <E> Page<E> startPage(int pageNum, int pageSize) {
// 通过分页拦截器自动包装Page对象
}

PageInfo构造函数要求:

1
2
3
4
5
6
7
public PageInfo(List<? extends T> list, int navigatePages) {
if (list instanceof Page) {
// 保留所有分页元数据
} else {
// 没有总数、页码,只是普通list
}
}

所以务必用Page对象!!,否则开发者每次都容易被误导踩坑。


五、陷阱总结与后续展望

易踩坑点

  • 各种“快捷”写法虽然方便,但实际返回类型往往不可控
  • PageHelper的拦截器只能保证最直接的 Mapper/SQL 返回 Page 类型
  • 任意再包一层 List,或被其他泛型原型包裹,会丢失分页元数据

修复建议与最佳实践

  • 必须保证分页被 Mapper 端页面级拦截并返回 Page
  • 只对核心场景做更改(Selector 和 Rule),兼顾解耦与通用性
  • 其他地方如用到 searchByCondition 需要仔细校验,不要“偷懒”滥用包裹写法
  • 可以考虑统一分页处理工具类或代码注释,减小误导风险

六、更多高级改造建议

  • 可以用全局分页工具进行类型判断,自动降级为 List 但告警
  • 定义统一分页接口约束,后续新模块统一走 Page
  • 建议社区维护统一分页文档,供团队协作时查阅

七、PR详情与链接

本次修复 PR 👉 #6243

  • 由 VampireAchao 提交
  • 只改动 Selector/Rule 分页逻辑,其他模块暂不变动
  • 彻底解决官方后台分页失效“史诗大坑”

八、总结

分页逻辑看似简单,实则暗藏“类型陷阱”。本次 ShenYu 的 issue #6239PR #6243,不仅修复了实际线上BUG,更为后续所有基于 PageHelper 的开发场景敲响了警钟:一定要确保 Page 对象贯穿全流程,避免隐式 List 误包。

面对海量数据与高并发场景,只有把分页基础做好,才能为平台稳定和高效打好坚实基础!


欢迎讨论更多ShenYu相关疑问与最佳实践,也欢迎关注PR进展和后续优化。