mybatis interceptor 通过拦截 executor.query() 实现自动分页,核心是基于 threadlocal 管理分页参数、重写 boundsql 注入方言化分页 sql(如 mysql 的 limit/offset、oracle 的 rownum 包裹),并清理上下文防止脏数据。

MyBatis 的 Interceptor 可以在 Executor 执行 SQL 前后进行拦截,是实现自动分页(如 PageHelper 风格)的核心机制。关键在于:不修改原始 SQL,而是通过重写 BoundSql、替换 RowBounds、并动态拼接 LIMIT/OFFSET 或使用数据库特有分页语法(如 MySQL 的 LIMIT、Oracle 的 ROWNUM 包裹)。
明确拦截目标与签名
要拦截 Executor 的查询流程,需拦截 Executor.query() 方法。该方法有多个重载,最常用的是:
<e> List<e> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) </e></e>
Interceptor 的 plugin 方法中需按此签名匹配,避免误拦其他方法(如 update 或带 Cursor 的重载)。
判断是否需要分页并提取分页参数
不能对所有查询都分页,需约定触发条件,常见方式有:
- 检查参数对象是否含
Page/PageRequest类型字段(推荐,类型安全) - 检查
RowBounds是否为默认值(RowBounds.DEFAULT表示未手动分页),再结合 ThreadLocal 存储的分页上下文(如PageHelper.startPage(1, 10)) - 扫描
MappedStatement的 SQL 注解或 XML 中是否含@SelectPage等自定义注解(更灵活但需额外解析)
建议用 ThreadLocal + 静态工具类管理当前分页信息,例如:
private static final ThreadLocal<pageinfo> PAGE_CONTEXT = ThreadLocal.withInitial(() -> null);
public static void startPage(int pageNum, int pageSize) {
PAGE_CONTEXT.set(new PageInfo(pageNum, pageSize));
}
</pageinfo>重写 BoundSql 并注入分页 SQL
在 intercept 方法中获取原始 BoundSql,解析其 SQL 字符串,根据数据库方言生成分页包裹语句:
-
MySQL:在原 SQL 末尾追加
LIMIT #{pageSize} OFFSET #{offset},注意 offset = (pageNum-1) * pageSize -
PostgreSQL:同 MySQL,支持
LIMIT ... OFFSET ... -
Oracle:需将原 SQL 包裹为
SELECT * FROM (SELECT ROWNUM rn, t.* FROM (原SQL) t WHERE ROWNUM ? - 使用
MetaObject替换BoundSql的sql和parameterMappings,同时更新parameterObject添加pageSize、offset等参数
注意:不要直接修改原始 BoundSql,应创建新 BoundSql 实例(通过 new BoundSql(...) 或 copyFrom 工具方法),否则影响缓存和复用。
清理上下文与兼容性处理
分页逻辑执行完必须清空 ThreadLocal,防止线程复用导致脏数据:
try {
return invocation.proceed();
} finally {
PAGE_CONTEXT.remove(); // 必须放 finally
}
还需考虑:
- 跳过
selectKey查询(ms.getSqlCommandType() == SqlCommandType.SELECT && !ms.isSelectKeyStatement()) - 跳过已含
LIMIT/TOP等关键词的手写分页 SQL(简单正则检测,避免重复分页) - 支持
count查询自动补全:拦截时若发现是分页查询,可额外触发一次SELECT COUNT(*) FROM (原SQL)获取总记录数(PageHelper 即采用此模式)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











