SSM+EasyUI datagrid分页实战:从数据契约到性能优化与排错

发布时间:2026/9/3 19:28:19
SSM+EasyUI datagrid分页实战:从数据契约到性能优化与排错 简介面向Java Web开发者的SSMEasyUI分页Demo基于Maven构建整合Spring、SpringMVC、MyBatis与EasyUI演示从数据库查询到前端表格分页的完整链路。压缩包共122个文件包含12个Java源码、12个XML配置、8个SQL脚本、JSP页面、JS/CSS样式以及40余张PNG运行截图整体仅1.68MB目录结构清晰便于按模块对照学习。已有260人学习下载。项目内置分页拦截器、分页工具类和用户管理模块可学习MyBatis分页插件原理、SpringMVC处理Ajax请求、EasyUI datagrid分页配置等核心知识点附带的说明文档能帮助快速搭建环境并理解SSM整合细节控制器、服务层与DAO层的分层写法也为规范项目组织提供了参考适合课程设计、毕业设计或框架入门实战。 分页这件事在SSM项目里算是老生常谈但每次接手或者回顾EasyUI datagrid的分页实现总能看到一堆人踩在同一批坑上。尤其是Spring SpringMVC MyBatis骨架的老项目前端用EasyUI做后台管理界面分页数据接不上、参数名对不上、总条数显示不对问题层出不穷。这篇文章就把我实际维护这类项目时积累的做法和教训完整拆一遍重点落在分页的数据契约、后端插件选型、前端对接细节以及几个高频排错场景给准备做或者正在做SSMEasyUI分页的朋友一份可以直接参考的实操笔记。1. 分页方案整体设计与数据流转思考1.1 为什么这套老组合仍然值得梳理很多人会觉得SSM配EasyUI已经是过时技术栈了但回到真实工作场景大量存量后台管理系统就是这套结构尤其是传统企业里的运营后台、管理平台SpringMVC负责路由分发MyBatis处理数据库交互前端用EasyUI的datagrid直接渲染表格。这些系统不会因为你学了Spring Boot就立刻迁移更多时候是在原有骨架上加功能、修Bug分页自然成了绕不开的能力点。EasyUI的datagrid本身对分页支持得相当完整它自带分页工具栏只要后端按要求返回特定结构的JSON表格组件就能自动渲染页码、总条数、上一页下一页。关键是搞清楚EasyUI默认发送什么参数、期望接收什么返回结构并且让后端按照这个约定来设计接口。只要把这个数据契约理顺分页这件事就成功了一半。1.2 核心数据契约page、rows与total、rowsEasyUI datagrid发起请求时默认会带上两个分页参数page当前页码默认从1开始和rows每页条数默认10。服务端需要返回的JSON格式是固定的两层结构{ total: 100, rows: [ {id: 1, name: 张三}, {id: 2, name: 李四} ] }这里有个极其容易踩坑的点返回结果的字段名必须是total和rows前端才认。很多新手后端喜欢返回count、data、list这类字段结果就是表格永远显示不出来控制台报数据格式错误。理解了这一层就能理解为什么后端要单独封装一个分页结果对象而不是直接把List丢回前端。我在处理这类需求时第一步永远是先确定这个数据契约前后端各守一边后面所有代码都是围绕这个约定展开的。2. 后端分页实现MyBatis分页插件与统一返回结构2.1 分页插件选型PageHelper的引入与配置SSM项目里做分页主流方案有两种一种是手写LIMIT #{offset}, #{pageSize}Oracle用ROWNUM另一种是引入分页插件。手写SQL的缺点是每个查询都要单独维护分页语句数据库方言一换全盘改写而分页插件我用的是PageHelper可以在不改SQL的前提下通过拦截器自动拼接数据库方言完成分页对代码侵入性极小。基于Maven构建的项目引入PageHelper很简单dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.3/version /dependency然后在Spring的MyBatis配置文件中注册这个拦截器bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameplugins array bean classcom.github.pagehelper.PageInterceptor property nameproperties value helperDialectmysql reasonabletrue supportMethodsArgumentstrue /value /property /bean /array /property /bean这三个配置项要重点说明一下helperDialect指定数据库方言。MyBatis无法自动判断你用的什么数据库所以手动指定MySQL就写mysqlOracle就写oracle。reasonable开启合理化。作用是当页码小于1时自动显示第一页页码超过总页数时自动显示最后一页避免出现空数据的情况。supportMethodsArguments支持从Mapper方法的参数中直接读取分页参数这个在参数名规范的前提下很有用。2.2 统一分页返回对象的封装按照前面说的数据契约后端必须返回total和rows两个字段所以封装一个通用的分页结果类很有必要。我自己常用的是一个简单的泛型类public class PageResultT { private long total; private ListT rows; public PageResult(long total, ListT rows) { this.total total; this.rows rows; } // getter / setter }有人会问直接用PageHelper的PageInfo行不行PageInfo里确实有total和list但字段名对不上EasyUI的要求除非加JSON序列化注解改名否则还是要做一层转换。与其绕来绕去不如自己封装代码更直白别人接手也一眼能看懂。2.3 Service与Controller的对接规范接下来是实战代码。Service层负责查询调用PageHelper.startPage然后查询数据并封装返回。这里有一个被反复强调的坑PageHelper.startPage()方法必须紧跟第一条Mapper查询语句中间不能插入其他查询否则分页会作用到错误的SQL上。原则是分页之前不查询查询之前不分页。Override public PageResultUser getUserPage(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListUser userList userMapper.selectUserList(keyword); PageInfoUser pageInfo new PageInfo(userList); return new PageResult(pageInfo.getTotal(), pageInfo.getList()); }Controller层只需要接收前端传的参数并调用Service返回结果Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(/list) ResponseBody public PageResultUser list( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int rows, String keyword) { return userService.getUserPage(page, rows, keyword); } }这里有个设计细节前端传的是rows后端接收参数名也要叫rows不要随手改成pageSize除非你在前端配置里重写参数名。保持两侧命名一致能省掉后续排查联调的大量时间。3. 前端EasyUI datagrid分页对接实操3.1 页面表格初始化与分页工具栏开启EasyUI里启用分页非常直观只需要在datagrid配置里加上pagination: true表格底部就会出现分页工具栏。一个最简配置如下$(#userTable).datagrid({ url: /user/list, method: get, queryParams: { keyword: $(#keyword).val() }, pagination: true, pageNumber: 1, pageSize: 10, pageList: [10, 20, 50, 100], columns: [[ { field: id, title: ID, width: 60 }, { field: name, title: 姓名, width: 100 }, { field: dept, title: 部门, width: 120 } ]] });url指向后端接口method我习惯用get数据量不大、纯查询场景下更省事。pageList是供用户选择的每页条数选项这里直接影响分页工具栏下拉框。EasyUI会自动把page和rows追加到请求参数里后端收到的就是这两个值。3.2 查询条件与分页的联动处理最让新手困惑的一点是点查询按钮后如何让表格带着关键词刷新并且页码回到第一页。做法是先销毁旧的datagrid或者重新load并重置分页参数。我记得早期有人直接调reload不重置页码结果搜索时页码还在第5页数据自然对不上。我常用的做法是function doSearch() { $(#userTable).datagrid(load, { keyword: $(#keyword).val() }); }load方法会重新请求url并把传入的对象合并到queryParams中。页码问题不用担心因为EasyUI在调用load时默认会从第一页开始加载。如果你想保留当前页码搜索也可以用reload但大多数管理后台的搜索预期都是从第一页开始所以load更顺手。清空查询条件时也同样调用load把keyword传空字符串然后后端查询时做非空判断。3.3 表格刷新与选中数据获取的细节除了分页本身后台系统里表格的刷新和选中数据获取也是高频操作。刷新很简单function doRefresh() { $(#userTable).datagrid(reload); }获取选中行需要注意EasyUI的getSelections返回的是一个数组因为开启了多选时可能选中多行。如果只允许单选应该先用getSelected它直接返回单行对象如果处理批量操作比如批量删除才用getSelections遍历。这里我踩过一个坑用getSelected做批量删除结果永远只删了第一行选中的数据后来才发现方法用错了。选中行的字段获取也要注意EasyUI的字段名对应的是后端返回JSON里rows数组中每个对象的key所以id、name这些字段名必须和后端实体属性名对应否则拿到的值是undefined。4. 分页性能问题与Redis优化思路4.1 分页查询变慢的常见原因分页功能上线后迟早会遇到数据量增长带来的性能问题。页面上数据量一大SQL执行时间飙升接口响应变慢页面转圈圈。常见原因有三个第一深分页问题。比如用户翻到第100页数据库需要先扫描并丢弃前990条记录再取10条。MySQL的LIMIT 1000, 10和LIMIT 0, 10执行成本差别巨大翻页越深越慢。第二关联查询没有走索引。分页的ORDER BY字段或WHERE条件字段如果没建索引数据库只能全表扫描再排序慢是必然的。第三JOIN了太多不必要的表。很多mapper为了省事一个大查询JOIN了七八张表每行数据都带着大量冗余字段传输和解析成本都上去了。4.2 Redis缓存分页数据的思路与适用场景Redis优化分页是个老话题但真正落地要考虑场景。最容易做的是条件固定、数据更新不频繁的列表页比如某些字典表、配置表的分页查询。可以把第一页到某个固定页数的结果序列化后存进Redis设置一个合理的过期时间比如5分钟下次请求直接命中缓存减少数据库压力。更精细的方案是缓存每页的数据集合以page:页码:pagesize:条数作为keyString key user:list:page: pageNum :size: pageSize; String cacheValue redisService.get(key); if (cacheValue ! null) { return JSON.parseObject(cacheValue, PageResult.class); } PageResultUser result userService.getUserPage(pageNum, pageSize, keyword); redisService.setex(key, 300, JSON.toJSONString(result));但我要提醒一个细节这种方案的前置条件是查询条件必须非常固定一旦带了用户输入的关键词key的设计会变得很复杂缓存命中率直线下降反而可能因为缓存维护成本过高而得不偿失。遇到带关键词的查询场景优先做的还是SQL层面的优化比如为WHERE条件字段建立合适的联合索引、避免SELECT *、把深分页改成基于ID的游标查询。游标查询是我个人比较推荐的一种经验手段把LIMIT offset, pageSize改成WHERE id #{lastId} ORDER BY id LIMIT #{pageSize}这样每次查询都从上次最后一条记录的ID往后取翻页性能非常稳定。缺点是用户无法直接跳转到指定页码但对于下一页模式的浏览场景完全够用。5. 常见问题与排错技巧实录5.1 高频问题速查表我梳理这几年积累的排查经验最常遇到的问题基本集中在这几个方面现象可能原因排查方法表格能加载但总条数不对PageHelper.startPage后执行了多余查询检查startPage与第一条select之间是否有其他查询点击下一页无数据reasonable未开启或传参page为0检查分页插件配置确认page从1开始返回数据结构不一致后端返回了PageInfo或Page而非totalrows确认接口返回的是封装后的PageResult搜索时页码未重置使用了reload而不是load查询按钮改用datagrid(load)方法分页插件完全不生效插件未注册或mapper接口不属于被扫描范围检查sqlSessionFactory的plugins配置传pageSize无效前端参数名是rows后端却接收pageSize统一参数名或者在RequestParam指定名称5.2 排查分页插件不生效的完整思路分页插件不生效是这类项目里让人最头疼的问题因为没有报错但SQL就是没有LIMIT。遇到这种情况我习惯按顺序检查三个环节。第一步确认Mapper接口是否被Spring容器管理。PageHelper通过MyBatis拦截器工作自动分页的SQL只对走SqlSessionFactory执行的方法生效。如果Mapper没有纳入Spring管理或者xml文件没有正确扫描插件自然拦不到。第二步确认startPage的调用位置。如果方法内先在startPage前执行了任意查询或者startPage和分页查询之间插入了其他Mapper调用都会导致分页错乱或失效。我见过最离谱的是在循环里调用startPage结果分页只对最后一次循环生效。第三步检查MyBatis全局配置里是否有自定义拦截器互相干扰。如果系统里还有其他自定义MyBatis拦截器拦截顺序可能会影响PageHelper的执行上下文。可以通过在日志配置里开启MyBatis SQL输出查看最终执行语句是否包含LIMIT来验证logging: level: com.example.mapper: debug看到类似 Preparing: SELECT ... LIMIT ?就是插件生效了。5.3 分页与排序组合的坑EasyUI的datagrid支持点击表头排序开启方式是在列配置里加sortable: true。但这里有个很容易被忽略的问题默认排序字段是列对应的field与数据库表字段名可能不一致。比如前端列field叫deptName后端存储的字段叫dept_name如果不做映射排序SQL就会变成ORDER BY deptName直接报字段不存在。我的处理办法是在后端统一做好字段白名单映射避免SQL注入风险private static final MapString, String SORT_COLUMN_MAP new HashMap(); static { SORT_COLUMN_MAP.put(deptName, dept_name); SORT_COLUMN_MAP.put(createTime, create_time); } String orderByCol SORT_COLUMN_MAP.getOrDefault(sortField, id); String orderBy orderByCol (desc.equals(sortOrder) ? desc : asc);前端传sort和order两个参数后端拿到后先查白名单再拼SQL。直接拼接前端参数是大忌等于把ORDER BY注入的漏洞敞开着这一点做管理后台时尤其要注意。6. 几个补充的实操心得再说两个我在实际项目中用到的小技巧虽然不能直接套用到所有项目但思路值得参考。第一个是关于Oracle分页的处理。如果有一天项目需要从MySQL迁移到Oracle或者新项目直接用Oracle数据库PageHelper同样支持只要把helperDialect改成oracle即可。Oracle的分页语法ROWNUM和MySQL完全不同手写SQL的话ROWNUM是硬编码在SQL里的换了数据库就得改SQL。用分页插件可以把方言差异隔离在插件层这也是我在老项目改造中坚持用PageHelper的原因之一。第二个是MyBatis字段映射的坑尤其是在使用Select注解写SQL时字段大小写、下划线转驼峰这些问题极易导致查询结果为空。MyBatis默认只做简单的属性映射deptName和dept_name默认不会自动对应。解决办法是在mybatis-config.xml里开启驼峰映射settings setting namemapUnderscoreToCamelCase valuetrue/ /settings开启后dept_name会自动映射到deptName少写无数个resultMap。这个小配置能极大提升开发效率很多项目里的字段映射Bug都是因为这个开关没有开启。7. 写在最后SSM EasyUI的分页实现表面看就是前端调接口、后端查数据库、中间封装一层JSON的问题但真正动手做一遍会发现细节远比想象中多。从数据契约的确定到分页插件的配置再到前后端参数名对齐每一步都藏着不少经验坑。我这两年因为分页问题帮同事排查的次数粗略算下来也有十几次问题来源几乎都是这几类参数名不对、返回结构不对、startPage位置不对、排序字段没做白名单。把这四个点提前堵住分页这块基本就稳了。最后再分享一个小经验分页功能虽然看着简单但它是对整个SSM项目结构理解的一个缩影。当你把一条数据从数据库查出来经过Mapper、Service、Controller、JSON序列化最终渲染到前端表格的完整链路摸清楚后后面再接触Spring Boot、MyBatis-Plus的新项目分页方案理解起来会快得多因为分页的核心永远都是那一套数据流转逻辑。本文还有配套的精品资源点击获取