1. SpringBoot整合Mybatis-PageHelper基础配置
在SpringBoot项目中集成Mybatis-PageHelper分页插件,首先需要在pom.xml中添加starter依赖。这里推荐使用pagehelper-spring-boot-starter,它会自动完成大部分配置工作:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency>对于SpringBoot 2.x及以上版本,建议使用1.4.x版本的starter。这个starter会自动配置以下内容:
- 自动检测使用的数据库类型
- 自动注册PageInterceptor拦截器
- 自动配置合理的默认参数
在application.yml中可以进行基础配置:
pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSql提示:helperDialect参数虽然可以自动检测,但在多数据源场景下显式指定更可靠。reasonable参数设置为true时,当页码超出范围会自动修正到合理范围。
2. 核心分页功能实现与原理
2.1 基础分页查询实现
在Service层实现分页查询的典型代码如下:
public PageInfo<User> findUsers(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> users = userMapper.selectAll(); return new PageInfo<>(users); }这段代码的工作原理是:
- PageHelper.startPage()方法将分页参数存入ThreadLocal
- 后续第一个MyBatis查询方法会被拦截器检测到
- 拦截器根据分页参数改写SQL,添加LIMIT子句
- 同时自动执行COUNT查询获取总记录数
- 最终返回的List实际是Page对象,包含分页信息
2.2 PageInfo对象详解
PageInfo是PageHelper提供的分页结果包装类,包含丰富的分页信息:
// 获取PageInfo后可以访问以下属性 pageInfo.getList(); // 结果集 pageInfo.getTotal(); // 总记录数 pageInfo.getPages(); // 总页数 pageInfo.getPageNum(); // 当前页码 pageInfo.getPageSize(); // 每页数量 pageInfo.isFirstPage(); // 是否第一页 pageInfo.isLastPage(); // 是否最后一页2.3 分页原理深度解析
PageHelper的核心拦截器是PageInterceptor,它的工作流程如下:
- 拦截Executor.query()方法
- 检查ThreadLocal中是否存在分页参数
- 存在参数时,使用JSqlParser解析原始SQL
- 生成COUNT查询SQL并执行获取总数
- 修改原始SQL添加数据库特定的分页语句
- 执行分页查询并封装结果到Page对象
对于MySQL数据库,PageHelper会将SQL从:
SELECT * FROM user WHERE age > 20改写成:
SELECT * FROM user WHERE age > 20 LIMIT 0,103. 高级功能与最佳实践
3.1 多数据源分页配置
在多数据源环境下,需要特殊配置确保分页正确:
pagehelper: autoRuntimeDialect: true closeConn: false关键参数说明:
- autoRuntimeDialect:运行时自动检测数据源类型
- closeConn:设置为false防止连接被提前关闭
3.2 复杂查询分页处理
对于包含JOIN或子查询的复杂SQL,PageHelper可能无法自动优化COUNT查询。此时可以:
- 自定义COUNT查询:
@Select({"<script>", "SELECT count(*) FROM user u WHERE u.age > #{age}", "</script>"}) Long countUsers(@Param("age") int age); @Select({"<script>", "SELECT * FROM user u WHERE u.age > #{age}", "</script>"}) List<User> selectUsers(@Param("age") int age, RowBounds rowBounds);- 使用PageHelper提供的ISelect接口:
Page<User> page = PageHelper.startPage(1, 10) .doSelectPage(() -> userMapper.selectComplexQuery(params));3.3 性能优化建议
- 对于百万级数据分页,推荐使用"延迟关联"优化技巧:
SELECT * FROM user INNER JOIN ( SELECT id FROM user ORDER BY create_time DESC LIMIT 100000,10 ) AS tmp USING(id)- 启用异步COUNT查询提升性能:
pagehelper: asyncCount: true asyncCountParallelism: 4- 对于不需要总数的情况,可以关闭COUNT查询:
PageHelper.startPage(1, 10, false); // 第三个参数设为false4. 常见问题排查与解决方案
4.1 分页失效问题排查
如果发现分页没有生效,可以按以下步骤排查:
- 检查是否在查询方法前调用PageHelper.startPage()
- 确保startPage()和查询语句之间没有其他查询
- 检查是否有多个PageInterceptor被注册
- 开启debug模式查看拦截器日志:
pagehelper: debug: true4.2 总数统计不准确
当发现分页的总数统计不准确时:
- 检查SQL是否包含GROUP BY,需要手动优化COUNT查询
- 对于复杂JOIN查询,考虑使用@SelectProvider自定义COUNT查询
- 检查是否有WHERE条件使用了不稳定函数如NOW()
4.3 内存溢出问题
处理大数据量分页时可能遇到OOM,解决方案:
- 使用流式查询代替分页:
@Select("SELECT * FROM large_table") @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000) void streamLargeData(ResultHandler<User> handler);- 采用"游标分页"模式:
// 第一页 List<User> firstPage = userMapper.selectAfterId(0, 10); // 获取最后一行的ID Long lastId = firstPage.get(firstPage.size()-1).getId(); // 下一页 List<User> nextPage = userMapper.selectAfterId(lastId, 10);5. 实战案例:前后端分离分页实现
5.1 后端接口实现
典型的RESTful分页接口实现:
@GetMapping("/users") public Result<PageInfo<User>> getUserPage( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> users = userService.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(users); return Result.success(pageInfo); }5.2 前端分页参数处理
前端(如Vue)调用分页接口的示例:
async loadData(page = 1) { const res = await axios.get('/users', { params: { pageNum: page, pageSize: 10 } }); this.users = res.data.list; this.pageInfo = res.data; }5.3 统一分页响应格式
建议的统一响应格式:
{ "code": 200, "message": "success", "data": { "list": [...], "pageNum": 1, "pageSize": 10, "total": 100, "pages": 10 } }6. 扩展功能与自定义开发
6.1 自定义分页方言
对于特殊数据库,可以实现自定义方言:
public class CustomDialect extends AbstractHelperDialect { @Override public String getPageSql(String sql, Page page) { // 实现自定义分页逻辑 return sql + " LIMIT " + page.getStartRow() + "," + page.getPageSize(); } }然后在配置中指定:
pagehelper: dialectAlias: custom=com.example.CustomDialect6.2 分页拦截器扩展
可以扩展PageInterceptor添加自定义逻辑:
public class CustomPageInterceptor extends PageInterceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 前置处理 Object result = super.intercept(invocation); // 后置处理 return result; } }6.3 与MyBatis-Plus共存
当同时使用MyBatis-Plus和PageHelper时,需要注意:
- 确保PageInterceptor在MyBatis-Plus拦截器之前执行
- 不要在同一个方法中混用两种分页方式
- 可以在配置中明确指定拦截器顺序:
mybatis: configuration: interceptor-order: pageInterceptor,mybatisPlusInterceptor在实际项目开发中,PageHelper的分页功能已经能够满足绝大多数场景需求。关键是要理解其工作原理,这样才能在遇到特殊需求时能够灵活应对。我个人的经验是,对于常规分页直接使用PageHelper.startPage()最简单,对于复杂分页场景可以考虑手动实现COUNT查询或使用游标分页等优化方案。