MyBatis 是如何进行分页的?分页插件的原理是什么?
2026年01月07日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常意在考察以下几个层面:
- 基础 API 掌握:你是否了解 MyBatis 提供的原生分页方式(
RowBounds)及其局限性。 - 实践与性能意识:你是否理解真正的生产级分页必须在数据库层面执行(物理分页),而非在应用内存中处理(逻辑分页),并清楚其性能影响。
- 框架扩展能力:你是否深入探究过 MyBatis 的插件(Interceptor)机制,这是理解主流分页插件(如 PageHelper)如何工作的核心。
- 源码探究与设计思想:面试官不仅仅想知道 “怎么用”,更是想通过此问题,评估你是否具备阅读主流框架源码、理解其扩展点设计思想的能力,这是区分普通开发者和资深开发者的关键。
核心答案
MyBatis 的分页主要分为两种方式:
- 原生/内存分页:使用
RowBounds参数,它本质上是在 SQL 查询出所有结果后,在应用内存中进行截取,效率低下,不适用于大数据量。 - 物理分页(推荐):通过 编写带有
LIMIT(MySQL) 或ROWNUM(Oracle) 等数据库方言的 SQL,在数据库层面直接返回指定页的数据。这是生产环境的标准做法。
而 分页插件(如 PageHelper)的原理 正是为了自动化物理分页:它基于 MyBatis 的 插件(拦截器)机制,在执行目标 SQL 前,动态地修改原 SQL,为其添加上数据库特定的分页语句(例如将 SELECT * FROM user 改写为 SELECT * FROM user LIMIT 0, 10),并可能额外执行一个 COUNT 查询以获得总记录数。
深度解析
原理/机制:插件如何 “拦截” 并 “改写” SQL
- MyBatis 插件机制:MyBatis 允许你编写拦截器,在四大核心对象(
Executor,StatementHandler,ParameterHandler,ResultSetHandler)的方法执行前后进行拦截。分页插件通常拦截StatementHandler.prepare方法。 - 动态代理与责任链:MyBatis 在启动时会为这些核心对象创建代理对象,所有插件按配置顺序形成一个拦截器链(
InterceptorChain)。当方法被调用时,会按顺序执行插件的逻辑。 - 分页插件的核心步骤:
- 拦截时机:插件拦截
StatementHandler.prepare(Connection, Integer)方法。 - 识别分页参数:从线程上下文的
ThreadLocal或方法参数中,获取前端传入的页码(pageNum)和每页大小(pageSize)。 - 改写 SQL:使用
MetaObject(MyBatis 提供的反射工具类)获取到当前StatementHandler中BoundSql对象里的原始 SQL 字符串,然后根据配置的数据库方言(Dialect),使用字符串拼接或 AST 解析的方式,将其改写为包含分页子句的物理分页 SQL。 - 执行 Count 查询(可选):如果需要获取总记录数,插件会使用同一个
Connection,先执行一条由原始 SQL 改写而来的COUNT(*)查询,并将结果存储起来供后续使用。
- 拦截时机:插件拦截
RowBounds 的原理与局限
// 示例:使用 RowBounds (不推荐用于生产)
List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.selectAll”, null, new RowBounds(10, 20)); // offset=10, limit=20
原理:MyBatis 的 DefaultResultSetHandler 在处理结果集时,会检查传入的 RowBounds。它虽然执行了 SELECT * FROM table 这样的全量查询,但在遍历 ResultSet 时,会跳过前面 offset 条记录,然后只读取 limit 条记录封装返回。
局限:全量数据会从数据库传输到应用服务器,并在 JDBC 驱动和 MyBatis 层面进行遍历和丢弃,对数据库、网络和应用内存都造成巨大压力。
最佳实践与常见误区
- 最佳实践:
- 始终使用物理分页插件,如 PageHelper 或 MyBatis-Plus 的内置分页功能。
- 在查询 大表 时,如果
COUNT(主键)仍然很慢,考虑不做精确计数,或使用异步加载、滚动加载(如 “加载更多”)等方案。 - 确保分页查询字段上有合适的索引,尤其是
ORDER BY和WHERE子句中的字段。
- 常见误区:
- 误以为
RowBounds是高效分页:这是最典型的误解,必须明确其内存分页的本质。 - 忽略
COUNT查询性能:在数据量极大或查询极复杂时,COUNT查询本身可能成为瓶颈。需要根据业务场景权衡是否必须精确总数。 - 忘记清理线程变量:PageHelper 使用
ThreadLocal存储分页参数。如果在异步或线程池环境下使用,必须在分页逻辑结束后调用PageHelper.clearPage(),否则参数可能污染后续无关查询。
- 误以为
总结
MyBatis 分页的核心思想是借助插件机制,将内存分页转化为高效的数据库物理分页;其实现的关键在于通过拦截器动态改写 SQL,这是 MyBatis 可扩展性设计的一个经典体现。