1. 为什么需要分页插件?
在Web开发中,数据分页是个高频需求。想象一下电商平台的商品列表,如果一次性加载10万条记录会怎样?浏览器会卡死,服务器内存会爆掉,用户体验直接归零。这就是分页存在的意义——把大数据集拆分成小块按需加载。
传统分页需要手动计算:
java复制// 老式分页写法示例(不推荐)
int pageSize = 10;
int pageNum = 2;
int start = (pageNum - 1) * pageSize;
List<User> users = userMapper.selectAll(start, pageSize);
这种写法有三大痛点:
- 需要重复编写分页逻辑
- 不同数据库分页语法差异大(MySQL用LIMIT,Oracle用ROWNUM)
- 需要额外查询总数COUNT
PageHelper的价值就在于:
- 统一不同数据库的分页语法
- 自动处理分页参数计算
- 智能获取总记录数
- 与MyBatis无缝集成
提示:PageHelper底层通过MyBatis拦截器实现,在SQL执行前动态修改语句。这也是为什么它支持多种数据库——不同方言由插件内部处理。
2. 两种依赖引入方式详解
2.1 传统JAR包引入(适合老项目)
在Spring Boot 1.x时代或非Maven项目中,通常这样引入:
-
下载JAR包:
- 官网地址:https://github.com/pagehelper/Mybatis-PageHelper
- 最新稳定版:5.3.2(截至2023年7月)
-
手动添加到lib目录:
code复制your-project ├── lib │ ├── pagehelper-5.3.2.jar │ └── jsqlparser-4.5.jar(必须的依赖) -
Spring配置(XML方式):
xml复制<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value> helperDialect=mysql reasonable=true supportMethodsArguments=true </value> </property> </bean> </array> </property> </bean>
关键配置说明:
helperDialect:指定数据库方言(mysql/oracle/postgresql等)reasonable:分页参数合理化(pageNum<1时自动设为1)supportMethodsArguments:支持Mapper接口参数传递
2.2 Maven依赖引入(推荐方式)
现代Spring Boot项目更推荐通过pom.xml引入:
xml复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
优势对比:
| 特性 | JAR包方式 | Starter方式 |
|---|---|---|
| 自动配置 | 需要手动配置 | 开箱即用 |
| 依赖管理 | 手动处理 | Maven自动解决 |
| Spring Boot集成度 | 中等 | 深度集成 |
| 版本更新 | 需手动替换 | 修改version即可 |
踩坑提醒:避免同时引入pagehelper和pagehelper-spring-boot-starter,会导致类冲突。如果发现启动报
ClassNotFoundException,检查依赖树是否有重复。
3. 基础使用与实战示例
3.1 基本分页查询
Controller层典型写法:
java复制@GetMapping("/users")
public PageInfo<User> getUsers(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
// 关键点:调用startPage后第一个查询会被分页
PageHelper.startPage(pageNum, pageSize);
List<User> users = userService.selectAll();
// 用PageInfo包装结果(包含分页信息)
return new PageInfo<>(users);
}
PageInfo的核心属性:
java复制public class PageInfo<T> {
private int pageNum; // 当前页
private int pageSize; // 每页数量
private int total; // 总记录数
private List<T> list; // 结果集
private int pages; // 总页数
private boolean hasPreviousPage; // 是否有上一页
private boolean hasNextPage; // 是否有下一页
}
3.2 复杂查询分页
当需要多表关联查询时:
java复制public PageInfo<UserDTO> getUsersWithRole(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
// 这个查询包含join操作
List<UserDTO> users = userMapper.selectWithRole();
return new PageInfo<>(users);
}
性能提示:多表分页时,确保SQL有合适的索引。PageHelper只是修改SQL语句,查询效率取决于数据库优化。
3.3 参数传递方式
除了startPage,还有多种参数传递方式:
- 方法参数方式:
java复制// Mapper接口
List<User> selectByPage(
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
// 使用时
PageHelper.startPage(pageNum, pageSize);
userMapper.selectByPage(pageNum, pageSize); // 参数名需一致
- RowBounds方式(不推荐):
java复制RowBounds rowBounds = new RowBounds(pageNum, pageSize);
List<User> users = userMapper.selectAll(rowBounds);
4. 高级特性与避坑指南
4.1 分页插件配置详解
在application.yml中的完整配置示例:
yaml复制pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
page-size-zero: true
return-page-info: always
关键参数解析:
page-size-zero:当pageSize=0时返回全部结果params:COUNT查询的别名配置return-page-info:是否自动返回PageInfo
4.2 常见问题排查
问题1:分页失效
现象:调用startPage后没有分页效果
排查步骤:
- 检查是否在查询前调用startPage
- 确认startPage和查询语句在同一线程
- 查看最终执行的SQL(开启mybatis日志)
问题2:总数COUNT异常
现象:total值不正确
解决方案:
yaml复制pagehelper:
params: count=countSql # 明确指定COUNT语句别名
问题3:内存分页警告
日志出现WARN [PageHelper] 分页插件检测到内存分页操作...
原因:在应用层做了结果集过滤(如Java 8 stream过滤)
正确做法:应该在数据库层完成过滤
4.3 性能优化建议
- 复杂查询的COUNT优化:
java复制// 自定义COUNT查询
@Select("SELECT COUNT(*) FROM user WHERE status = 1")
long countActiveUsers();
// 使用时
PageHelper.startPage(pageNum, pageSize)
.setCount(() -> countActiveUsers());
- 避免分页大偏移量:
sql复制-- 低效(偏移量10万)
SELECT * FROM table LIMIT 100000, 20
-- [优化方案](https://taotoken.net?utm_source=general)(使用索引覆盖)
SELECT * FROM table WHERE id > 100000 LIMIT 20
- 分布式环境注意:
- PageHelper默认基于ThreadLocal
- 异步场景需要手动传递分页参数:
java复制Page<?> page = PageHelper.startPage(pageNum, pageSize); // 异步调用前保存参数 int pageNum = page.getPageNum(); // 异步回调后恢复 PageHelper.startPage(pageNum, pageSize);
5. 与其他技术的对比
5.1 对比MyBatis-Plus分页
MyBatis-Plus也提供分页功能,主要区别:
| 特性 | PageHelper | MyBatis-Plus Pagination |
|---|---|---|
| 原理 | MyBatis拦截器 | MyBatis拦截器 |
| 使用方式 | 需手动startPage | 自动分页 |
| 多表支持 | 较好 | 一般 |
| 自定义COUNT | 支持 | 支持 |
| 与MyBatis-Plus整合 | 需单独配置 | 原生支持 |
选择建议:
- 纯MyBatis项目:PageHelper
- 已用MyBatis-Plus:直接使用其分页
5.2 对比JPA分页
JPA的分页写法:
java复制Pageable pageable = PageRequest.of(pageNum - 1, pageSize);
Page<User> page = userRepository.findAll(pageable);
差异点:
- JPA页码从0开始,PageHelper从1开始
- JPA与Hibernate深度集成
- PageHelper对复杂SQL支持更好
6. 实际项目中的经验
- 统一分页响应格式:
java复制public class PageResult<T> {
private long total;
private List<T> rows;
public static <T> PageResult<T> of(PageInfo<T> pageInfo) {
PageResult<T> result = new PageResult<>();
result.setTotal(pageInfo.getTotal());
result.setRows(pageInfo.getList());
return result;
}
}
- 前端分页组件对接:
- Element UI分页组件示例:
javascript复制{ total: response.data.total, currentPage: params.page, pageSize: params.size, data: response.data.rows }
- 我遇到的真实坑:
- 在多数据源项目中,需要为每个SqlSessionFactory单独配置PageHelper
- 解决方案:
java复制@Bean @ConfigurationProperties(prefix = "pagehelper") public Properties pageHelperProperties() { return new Properties(); } @Bean public PageInterceptor pageInterceptor(Properties pageHelperProperties) { PageInterceptor interceptor = new PageInterceptor(); interceptor.setProperties(pageHelperProperties); return interceptor; } @Bean public SqlSessionFactory sqlSessionFactory1( @Qualifier("pageInterceptor") PageInterceptor interceptor) { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setPlugins(new Interceptor[]{interceptor}); // 其他配置... return factory.getObject(); }
- 性能监控建议:
java复制// 在拦截器中记录分页查询耗时
public class PageMonitorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) {
log.warn("Slow page query: {}ms", cost);
}
}
}
}
