1. 项目背景与核心需求在数据密集型的应用开发中分页查询是最基础也最频繁的操作之一。作为Java生态中最流行的ORM框架MyBatis虽然提供了强大的SQL映射能力但原生并未内置完善的分页支持。这导致开发者在不同业务场景中需要反复编写相似的分页逻辑既降低了开发效率也增加了维护成本。我曾在多个企业级项目中遇到过这样的困境有的团队在DAO层手动拼接LIMIT语句有的在Service层用内存分页甚至有些老系统在每个Mapper.xml里重复定义count查询。这些做法不仅违背DRY原则更会在数据量增长时暴露出性能问题。基于这些痛点我们需要设计一个与MyBatis深度集成的分页插件它应该具备以下核心能力零侵入不改动现有Mapper接口和SQL语句自动化自动识别分页查询补充count语句高性能避免内存分页确保数据库层面分页可扩展支持多种数据库方言和特殊场景2. 技术方案设计2.1 整体架构设计分页插件的核心实现基于MyBatis的插件机制Interceptor通过拦截Executor的query方法实现SQL重写。整体工作流程分为三个阶段拦截阶段通过Intercepts注解定义拦截点解析阶段识别分页参数解析原始SQL执行阶段改写为分页SQL并行执行count查询Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class PaginationInterceptor implements Interceptor { // 实现逻辑将在下文展开 }2.2 关键技术选型SQL解析器选用JSqlParser而非正则表达式原因在于精准识别SQL语法结构避免错误匹配支持复杂SQL语句如子查询、UNION提供AST树便于语法分析和改写线程安全通过ThreadLocal保证分页参数传递避免污染业务代码public class PageContext { private static final ThreadLocalPage? CONTEXT new ThreadLocal(); public static void setPage(Page? page) { CONTEXT.set(page); } // 其他工具方法... }3. 核心实现细节3.1 SQL改写逻辑对于MySQL方言的分页改写需要处理以下边界情况已包含LIMIT的语句不再处理嵌套查询需要在外层添加分页GROUP BY语句需要特殊处理// 示例改写逻辑 String originalSql SELECT * FROM users WHERE status 1; Page? page PageContext.getPage(); String paginatedSql originalSql LIMIT page.getOffset() , page.getPageSize();3.2 Count查询优化为避免全表扫描count查询需要移除不必要的ORDER BY和优化SELECT部分/* 原始分页查询 */ SELECT id, name, create_time FROM orders ORDER BY create_time DESC LIMIT 10 /* 优化后的count查询 */ SELECT COUNT(1) FROM orders3.3 多数据源适配通过Dialect抽象层支持不同数据库public interface Dialect { String buildPaginationSql(String originalSql, long offset, long limit); String buildCountSql(String originalSql); } // MySQL实现示例 public class MySqlDialect implements Dialect { Override public String buildPaginationSql(String sql, long offset, long limit) { return sql LIMIT offset , limit; } }4. 完整实现示例4.1 核心拦截器实现Override public Object intercept(Invocation invocation) throws Throwable { Object[] args invocation.getArgs(); MappedStatement ms (MappedStatement) args[0]; Object parameter args[1]; // 1. 检查是否分页查询 Page? page getPageParameter(parameter); if (page null) { return invocation.proceed(); } // 2. 获取原始SQL并解析 BoundSql boundSql ms.getBoundSql(parameter); String originalSql boundSql.getSql(); Statement stmt CCJSqlParserUtil.parse(originalSql); // 3. 执行count查询 int total executeCount(ms, parameter, boundSql); page.setTotal(total); // 4. 改写分页SQL String paginatedSql dialect.buildPaginationSql(originalSql, page.getOffset(), page.getPageSize()); resetSqlInBoundsql(boundSql, paginatedSql); return invocation.proceed(); }4.2 Page参数设计public class PageT implements Serializable { private int pageNum 1; private int pageSize 10; private long total; private ListT records; public long getOffset() { return (pageNum - 1) * pageSize; } // 其他getter/setter... }5. 使用示例与最佳实践5.1 基础用法// Service层 public PageUser listUsers(int pageNum, int pageSize) { PageUser page new Page(pageNum, pageSize); PageContext.setPage(page); userMapper.selectList(); // 无需修改Mapper接口 return page; }5.2 性能优化建议索引优化确保分页字段通常是id或create_time有索引延迟关联大数据量时先查ID再关联详情/* 优化前 */ SELECT * FROM large_table ORDER BY id LIMIT 100000, 10 /* 优化后 */ SELECT t.* FROM large_table t JOIN (SELECT id FROM large_table ORDER BY id LIMIT 100000, 10) tmp ON t.id tmp.id6. 常见问题排查6.1 分页失效检查清单Page参数未通过ThreadLocal传递SQL包含语法错误导致解析失败多数据源未正确配置方言拦截器未在MyBatis配置中注册6.2 性能问题分析当分页查询变慢时检查EXPLAIN确认是否走索引监控count查询耗时大数据量表需要特殊处理评估是否可以使用游标分页替代传统分页7. 高级功能扩展7.1 动态表名支持通过实现DynamicTableNameParser接口可以支持分表场景下的分页查询public interface DynamicTableNameParser { String parse(String originalSql, String tableNamePattern); } // 示例将user_#替换为user_01 String sql SELECT * FROM user_# WHERE...; String parsed dynamicTableNameParser.parse(sql, user_#);7.2 多租户适配结合TenantContext实现租户隔离String originalSql SELECT * FROM orders; String tenantId TenantContext.getCurrentTenant(); String filteredSql originalSql WHERE tenant_id tenantId ;在实现分页插件的过程中我发现几个值得注意的细节首先是对BoundSql的处理需要特别注意参数映射的保持其次是在高并发场景下要考虑PageContext的清理时机。实际测试时建议用JMeter模拟并发分页请求观察是否有内存泄漏或线程安全问题。