MyBatis PageHelper + EasyUI 分页实践:从参数传递到性能优化

📅 发布时间:2026/9/2 20:08:20
MyBatis PageHelper + EasyUI 分页实践:从参数传递到性能优化
简介这是一份基于Maven构建的SSMSpringSpringMVCMyBatis与EasyUI整合的分页示例工程面向正在学习Java Web框架整合的开发者也可作为后台管理系统分页功能实现的参考资料。压缩包内共122个文件包括Java源码、XML配置与映射文件、SQL初始化脚本、JSP页面、JS与CSS前端资源及PNG截图说明整体约1.68MB目录结构清晰便于按模块查阅。目前已有260人学习使用附带的说明文档能够帮助理清SSM整合流程、分页拦截器的实现方式以及EasyUI分页组件和后台接口之间的数据交互。通过运行项目并阅读源码读者可以掌握MyBatis分页查询、SpringMVC处理Ajax请求、前端分页组件初始化等实用技能还能理解分页参数从页面传递到后台、再拼接SQL并返回JSON的完整过程适合SSM入门与进阶学习。1. 项目背景与技术选型思路这套组合在当年属于非常典型的“SSM 前端UI框架”打法Spring 管对象、SpringMVC 管请求路由、MyBatis 管数据库操作、EasyUI 管后台管理界面。现在回头看它确实不是最时髦的技术栈但胜在稳定、资料多、上手快尤其适合企业后台管理系统这类“增删改查占大头”的项目。而分页恰恰是这些系统里最难绕开、也最容易写烂的一个环节。我最初接手这类项目的时候最大的感受是分页本身不难难在“分页参数怎么在整个链路里不出错地流转”。从 EasyUI 表格点下一页到前端参数组装、Controller 接收、Service 处理、MyBatis 执行 SQL、再回传总条数给前端渲染分页条——这一整条链路里任何一环的参数名对不上、类型不匹配、返回结构不一致都会导致表格不渲染、分页条不出现、或者翻页失效。所以这篇文章不只是讲“怎么分页”而是把这条链路完整拆开讲清楚每一层该干什么、参数怎么传、坑在哪里。我当时的选型思路很直接既然数据访问层用了 MyBatis分页就不用手写 LIMIT 或 ROWNUM直接引入分页插件通过拦截器在 SQL 执行前自动拼接分页语句业务代码只需要关心“查数据”和“数总数”分页细节交给插件处理。前端用 EasyUI 的 DataGrid 自带分页能力服务端只要按它约定的格式返回 JSON分页条、页码、每页条数就全有了。基于这个思路下面从五个层面完整走一遍分页实现包括具体配置、代码结构、参数说明和我在实际项目中踩过的坑。如果你是刚接触 SSM 整合项目这篇文章可以直接当操作手册用如果你已经在维护类似项目重点看第 4 和第 5 部分里面有不少排查经验。2. 分页实现的技术选型与整体链路设计2.1 为什么选择 MyBatis 分页插件而不是手写 SQL在 MyBatis 里做分页绕不开一个核心问题数据库方言。MySQL 用LIMIT offset, sizeOracle 用ROWNUM或FETCH FIRSTSQL Server 用OFFSET FETCH或ROW_NUMBER()。如果每个查询都手动拼分页语句意味着每一条 SQL 都要写两套甚至三套某天要切换数据库改到怀疑人生。分页插件的思路是用 MyBatis 提供的拦截器接口在 Executor 执行 SQL 之前拦截下来解析出原始 SQL自动生成带分页方言的 SQL 和对应的 count 查询。业务代码干干净净SQL 文件里只写业务逻辑不掺分页杂质。我当时对比了 PageHelper 和手写拦截器两种方案最终选了前者原因有三使用成本低引入依赖后在 MyBatis 配置里加一个插件配置即可代码里只需要在查询前调用PageHelper.startPage(pageNum, pageSize)。兼容性好内置了 MySQL、Oracle、PostgreSQL 等主流数据库的方言实现切换数据库基本零成本。不侵入业务代码分页逻辑和业务查询逻辑分离代码可读性和可维护性都更高。这里有一个很重要的原则PageHelper.startPage只对紧接着的下一条查询生效。这是最常见的误用场景后面我会单独说。2.2 前端 EasyUI DataGrid 与服务端的对接约定EasyUI 的 DataGrid 在开启分页后向服务端发起请求时默认会带上两个参数page当前页码和rows每页行数。这是 EasyUI 的默认约定不需要额外配置。服务端返回的数据格式也有约定必须是一个包含total和rows两个属性的 JSON 对象total符合条件的总记录数用来计算总页数和生成分页条。rows当前页的数据列表用来渲染表格行。这个约定是前端和后端的“接口契约”。只要返回结构正确EasyUI 就能自动解析并展示分页条用户点击页码时它会带着新的page参数重新请求服务端。我在实际项目中见过不少新手把返回结构写成{ data: [...], totalCount: 100 }结果表格能出数据但分页条不显示原因就是字段名不匹配。EasyUI 的 DataGrid 默认从total和rows两个字段取值除非你显式配置pagination: true并且改掉默认字段映射。2.3 SpringMVC 层怎么接收和传递分页参数Controller 层的职责是接收前端传来的page和rows转成 Service 层需要的参数类型再调用 Service 查询最后把结果包装成 EasyUI 需要的 JSON 格式返回。我常用的做法是定义统一的返回对象比如用MapString, Object或者自定义一个PageResult类。如果项目里有多个列表页强烈建议抽一个公共的PageResult类避免每个 Controller 里重复拼 Map。Controller 接收参数的方式有两种常见写法效果等价RequestMapping(/list) ResponseBody public MapString, Object list( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int rows) { // ... }也可以用一个分页参数对象接收代码更干净public class PageParam { private int page 1; private int rows 10; // getter/setter }注意defaultValue的配置——如果前端忘记传参服务端不会因为null或空指针崩掉。这是我在生产环境里踩过坑的地方前端某个页面的 DataGrid 忘记开pagination结果请求根本没有page和rows参数接口直接 500。3. 核心配置与代码实现3.1 MyBatis 全局配置文件里的分页插件配置如果你用的是 MyBatis 的 XML 配置方式在mybatis-config.xml里加上插件配置configuration plugins plugin interceptorcom.github.pagehelper.PageInterceptor !-- 数据库方言不配置时 PageHelper 会自动检测 -- property namehelperDialect valuemysql/ !-- 合理化参数pageNum 0 时默认查询第一页 -- property namereasonable valuetrue/ /plugin /plugins /configuration如果你用的是 Spring MyBatis 整合方式也可以直接在 Spring 的 SqlSessionFactoryBean 里配置插件bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /bean这里有一个细节reasonable参数我建议设成true这样页码超出范围时 PageHelper 会返回第一页或最后一页而不是抛异常或返回空列表。对面向运营人员的后台系统来说这种容错行为比直接报错体验好得多。3.2 Maven 依赖引入在pom.xml中加入 PageHelper 依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.3/version /dependency因为项目是 SSM 架构MyBatis 和 Spring 的整合还需要mybatis-spring依赖这里就不赘述了。需要提醒的是 PageHelper 的版本和 MyBatis 的版本有兼容性要求旧版 MyBatis 用新版 PageHelper 会报NoSuchMethodError之类的异常。一般 MyBatis 3.5.x 对应 PageHelper 5.x 基本没问题。3.3 Service 层分页查询的标准写法Service 层的写法是分页实现里最关键的一环也是 PageHelper 使用最容易出错的地方。标准写法如下public PageResultUser queryUserList(String keyword, int pageNum, int pageSize) { // 1. 调用 startPage设置分页参数 PageHelper.startPage(pageNum, pageSize); // 2. 紧接着执行查询这条查询会被分页拦截 ListUser userList userMapper.selectUserList(keyword); // 3. 用 PageInfo 包装查询结果包含总条数、总页数等信息 PageInfoUser pageInfo new PageInfo(userList); // 4. 组装成统一返回结果 PageResultUser result new PageResult(); result.setTotal(pageInfo.getTotal()); result.setRows(pageInfo.getList()); return result; }核心就一句话PageHelper.startPage(pageNum, pageSize)之后必须紧跟一次 Mapper 查询。中间不能有任何其他 SQL 操作否则分页会作用到错误的查询上。这里我需要着重解释一下为什么会这样。PageHelper 的实现原理是startPage方法内部使用ThreadLocal保存分页参数当 MyBatis 执行下一次查询时拦截器从ThreadLocal中取出参数生成分页 SQL执行完毕后清空ThreadLocal。如果startPage之后没有立即执行查询而是先执行了其他数据库操作那么分页参数就会被那次操作消耗掉真正的查询反而没有分页。我见过一个真实的线上案例同事在startPage之后先调用了userMapper.selectCount()做数据统计再做分页查询结果分页没有生效数据全部查出来了页面直接卡死。排查了很久发现就是这一行的顺序问题。3.4 Mapper 层的 SQL 写法Mapper 接口public interface UserMapper { ListUser selectUserList(Param(keyword) String keyword); }Mapper XMLselect idselectUserList resultTypecom.example.entity.User SELECT * FROM t_user where if testkeyword ! null and keyword ! AND username LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY id DESC /select注意这里 SQL 不需要手写 LIMIT也不需要写 ROWNUM分页插件的拦截器会自动处理。这是 PageHelper 最方便的地方也是很多人第一次用的时候最不习惯的地方——总觉得少了点什么。3.5 Controller 层的组装与返回RequestMapping(/list) ResponseBody public MapString, Object list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int rows) { PageResultUser result userService.queryUserList(null, page, rows); MapString, Object map new HashMap(); map.put(total, result.getTotal()); map.put(rows, result.getRows()); return map; }实际在 JSON 序列化时PageResult里如果只有total和rows两个属性也可以直接把PageResult返回出去不用手动转 Map。我这里写 Map 是为了方便你直观地看到 EasyUI 需要的字段结构。4. EasyUI 前端分页对接与常见样式调整4.1 DataGrid 前端代码前端用 EasyUI DataGrid 加载数据并开启分页table iddg/table$(#dg).datagrid({ url: /user/list, method: get, pagination: true, // 开启分页 pageSize: 10, // 每页条数 pageList: [10, 20, 50], // 每页条数可选列表 columns: [[ { field: id, title: ID, width: 80 }, { field: username, title: 用户名, width: 120 }, { field: email, title: 邮箱, width: 200 }, ]] });这里的url指向的是后端 Controller 的接口地址。EasyUI 会自动把page和rows参数拼到请求上后端按这两个参数名接收即可。如果出现数据能加载但不分页的情况优先检查pagination: true是否配置了。有的项目是从别人代码里复制过来的pagination可能被注释掉了。4.2 查询条件的联动实际项目里几乎每个列表页都有搜索条件。搜索条件和分页参数的组合方式我提供一个通用写法function searchUser() { $(#dg).datagrid(load, { keyword: $(#keyword).val() }); }datagrid(load, params)方法会带着已有的分页参数额外拼接传入的查询条件重新请求服务端。后端 Controller 需要额外增加一个keyword参数来接收。这里有一个注意点搜索之后页码应该重置为 1否则用户在第一页搜索完翻到第 5 页数据后再次搜索分页条可能停留在高页码上显得很突兀。处理方式是在搜索时把分页条重置function searchUser() { $(#dg).datagrid(load, { keyword: $(#keyword).val(), page: 1 }); }后端这时也要配合如果搜到结果总数不足当前页码EasyUI 会请求空数据体验不好。reasonable: true能避免一部分这种问题但前端重置页码依然是必要的。4.3 前端分页与后端分页的区别前面讲的都是后端分页也就是每次只查当前页的数据返回给前端总条数单独统计。这是大数据量下的正确做法。与之相对的是前端分页也就是一次把所有数据查出来前端 JS 自己切页显示。EasyUI DataGrid 默认支持自动分页但如果你给它的data是一整包数据它做的只是“前端分页”——看起来有分页条但每次切换都不发请求全部数据早就加载完了。我当时接手过一个项目某一页表格数据量有几万条页面加载特别慢排查时发现后端接口一次把所有数据全返回了EasyUI 只是在前端做了分页显示。处理方式是改成分页查询接口只返回当前页数据联调完接口响应从 3 秒降到了 200ms 以内。5. 常见问题与排查技巧实录5.1 PageHelper 分页不生效这是遇到最多的问题现象是前端显示所有数据分页条显示总页数为 1。排查步骤检查startPage是否紧接着 Mapper 查询中间有没有被其他 SQL 操作打断。在 Mapper XML 的查询语句上打印 SQL 日志看生成的 SQL 尾部是否拼接了LIMIT 0, 10。如果没有说明拦截器没生效。检查 MyBatis 配置里插件是否注册成功Spring 配置文件里SqlSessionFactoryBean是否正确加载了mybatis-config.xml。检查是否在 Spring 配置文件里重复定义了SqlSessionFactoryBean导致查询走了未配置插件的那个工厂。5.2 查询总条数为 0但数据能查出来如果数据查出来了但total为 0多半是 count 查询出错。PageHelper 会生成一条SELECT COUNT(0)的语句如果原 SQL 里有复杂的 JOIN、GROUP BY、DISTINCT 等count 语句可能执行异常或统计不准。处理方式是使用 PageHelper 的count参数控制是否执行 count 查询或者手动指定 count 查询的 Mapper 方法。更简单的做法是只对简单查询使用 PageHelper复杂统计查询单独手写。5.3 Oracle 分页的兼容问题PageHelper 支持 Oracle识别方言后会自动生成基于ROWNUM的分页 SQL。但前提是helperDialect配置正确或者让 PageHelper 自动检测。如果你的项目同时连接多种数据库建议显式配置避免自动识别出错。Oracle 12c 及以上版本也可以结合OFFSET FETCH语法但 PageHelper 默认的 Oracle 方言仍然是基于ROWNUM的实现。这一点在性能调优时需要注意海量数据下ROWNUM的写法可能不是最优执行计划必要时可以手工优化分页 SQL。5.4 EasyUI 表格能显示数据但分页条不显示分页条不显示几乎是百分之百的返回结构问题后端返回的 JSON 里没有total字段或者字段名不是total。后端返回的是数组而不是对象EasyUI 找到total和rows自然失败。后端返回了total和rows但rows不是数组或者total不是数字类型。定位方法很简单浏览器 F12 打开 Network查看接口返回的 JSON 结构对比 EasyUI 的约定即可。5.5 分页参数丢失或注入异常有时候前端请求参数是page和rows后端 Controller 的参数名写成了pageNumber和pageSize没有加RequestParam别名导致接收不到值。这是最粗心但也最常见的错误。更隐蔽的一个坑Controller 参数之前加了其他注解比如在同一个方法里用了RequestBody接收 JSON 请求体又用RequestParam接收 URL 参数如果前端把分页参数放在了错误的请求位置也会出现参数丢失的问题。EasyUI DataGrid 默认以 GET 方式提交分页参数如果后端接口同时支持 POST务必测试两种方式下的参数传递是否正常。5.6 分页查询慢怎么用 Redis 优化这是一个被问烂但始终值得认真对待的问题。分页慢的根因不是分页本身而是数据库扫描了大量无用数据。以 MySQL 为例LIMIT 100000, 20要先查出 100020 行再丢掉前 100000 行数据量越大越慢。用 Redis 优化分页的常见思路有两种第一种把主键 ID 列表缓存到 Redis 里使用 Redis 的ZRANGE或LRANGE取出当前页的 ID 集合再去数据库按 ID 批量查询完整记录。数据量在百万级以内时这种方法比较有效而且排序逻辑可控。第二种缓存整个查询结果集到 Redis适合数据量不大但查询特别频繁的场景。但要注意缓存一致性问题——数据一更新缓存就要同步失效。实测下来最稳妥的方案是在 SQL 层面做优化覆盖索引、延迟关联、改写LIMIT等。Redis 缓存只是锦上添花不能解决 SQL 本身性能差的问题。我遇到过把 Redis 缓存加上了但因为缓存穿透导致数据库压力更大的案例所以任何优化都要有监控和测算不要盲目上缓存。6. 写在最后的一点实操心得这套 SSM EasyUI 分页组合虽然老但只要理解清楚“参数传递链”和“PageHelper 的 ThreadLocal 机制”用起来是非常顺手的。我在实际维护中养成了一个习惯把分页参数定义、返回对象、Maven 依赖版本、前端初始化的关键配置整理成一个独立文档每到一个新项目先对照检查一遍。这样能省掉很多无谓的踩坑时间。如果你正在从零搭建项目建议把分页功能作为一个通用模块来设计而不是在每个业务模块里各自写一遍。统一的分页返回对象和参数校验逻辑在项目规模变大之后能帮你省掉一大半的联调和排查成本。最后再分享一个小技巧在开发环境打开 MyBatis 的 SQL 日志输出看实际执行的分页 SQL 是否符合预期这是排查分页问题最快的切入点。本文还有配套的精品资源点击获取