1. MyBatisPlus分页拦截器设计与实现
最近在重构一个老项目的分页功能时,发现直接使用MyBatisPlus的自动分页存在单页500条的限制问题。经过多次实践,我总结出一套完整的拦截器解决方案,不仅突破了默认限制,还能灵活适配各种分页场景。下面分享具体实现过程和踩坑经验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分页拦截器核心原理
2.1 MyBatis插件机制解析
MyBatis的插件基于责任链模式实现,通过拦截器(Interceptor)可以在执行SQL的四个关键节点进行拦截:
- Executor (update/query/commit/rollback)
- ParameterHandler (参数处理)
- ResultSetHandler (结果集处理)
- StatementHandler (SQL语句处理)
分页拦截主要作用于StatementHandler,在prepare阶段修改原始SQL。
2.2 分页SQL改写原理
以MySQL为例,分页本质是将:
sql复制SELECT * FROM table
改写为:
sql复制SELECT * FROM table LIMIT offset, size
拦截器需要:
- 识别分页请求
- 获取分页参数
- 重写SQL语句
- 返回总记录数
3. 完整实现步骤
3.1 基础拦截器定义
java复制@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
public class PaginationInterceptor implements Interceptor {
private static final ThreadLocal<Page<?>> PAGE_HOLDER = new ThreadLocal<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 实现细节见3.2
}
// 其他必要方法...
}
3.2 核心拦截逻辑
- 获取分页参数:
java复制StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(handler);
RowBounds rowBounds = (RowBounds) metaObject.getValue("delegate.rowBounds");
if (rowBounds == RowBounds.DEFAULT) {
return invocation.proceed(); // 非分页请求直接放行
}
- SQL重写:
java复制BoundSql boundSql = handler.getBoundSql();
String originalSql = boundSql.getSql();
// 使用JSqlParser解析SQL
Statement stmt = CCJSqlParserUtil.parse(originalSql);
Select select = (Select) stmt;
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// 添加LIMIT子句
Limit limit = new Limit();
limit.setOffset(rowBounds.getOffset());
limit.setRowCount(rowBounds.getLimit());
plainSelect.setLimit(limit);
String newSql = select.toString();
metaObject.setValue("delegate.boundSql.sql", newSql);
3.3 分页参数传递方案
推荐两种参数传递方式:
方案1:ThreadLocal方式
java复制public static <T> Page<T> startPage(int pageNum, int pageSize) {
Page<T> page = new Page<>(pageNum, pageSize);
PAGE_HOLDER.set(page);
return page;
}
// 拦截器中获取
Page<?> page = PAGE_HOLDER.get();
if (page != null) {
// 处理分页逻辑
PAGE_HOLDER.remove();
}
方案2:方法参数方式
java复制public interface UserMapper {
@Select("SELECT * FROM user")
List<User> selectPage(@Param("page") Page<User> page);
}
4. 高级功能实现
4.1 突破500条限制
MyBatisPlus默认配置:
yaml复制mybatis-plus:
global-config:
db-config:
max-limit: 500 # 默认单页最大条数
解决方案:
- 自定义配置类:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(){
@Override
protected void handlerLimit(DialectModel model) {
// 禁用默认限制
}
});
return interceptor;
}
- 或者在拦截器中直接处理:
java复制if (pageSize > 500) {
// 自定义大分页处理逻辑
log.warn("Large page size detected: {}", pageSize);
}
4.2 多数据源适配
针对不同数据库方言:
java复制public class DialectFactory {
public static String getLimitSql(String sql, String dialect) {
switch (dialect) {
case "mysql":
return sql + " LIMIT ?, ?";
case "oracle":
return "SELECT * FROM (" +
"SELECT ROW_.*, ROWNUM ROWNUM_ FROM (" +
sql + ") ROW_ WHERE ROWNUM <= ?" +
") WHERE ROWNUM_ > ?";
// 其他数据库方言...
}
}
}
5. 性能优化实践
5.1 Count查询优化
避免全表count:
java复制// 原始方式(性能差)
SELECT COUNT(1) FROM table
// 优化方案1 - 指定count列
SELECT COUNT(id) FROM table
// 优化方案2 - 使用缓存
@Cacheable(value = "pageCount", key = "#tableName")
public Long getCachedCount(String tableName) {
return mapper.selectCount(null);
}
5.2 大数据量分页方案
当offset大于100万时,推荐使用"游标分页":
java复制public List<User> selectByCursor(Long lastId, int size) {
return lambdaQuery()
.gt(User::getId, lastId)
.orderByAsc(User::getId)
.last("LIMIT " + size)
.list();
}
6. 常见问题排查
6.1 分页失效场景
-
XML中使用了$:
xml复制<!-- 错误示例 --> <select id="select" resultType="User"> SELECT * FROM user ${ew.customSqlSegment} </select> <!-- 正确写法 --> <select id="select" resultType="User"> SELECT * FROM user #{ew.customSqlSegment} </select> -
手动设置了RowBounds:
java复制// 会导致拦截器失效 List<User> users = mapper.selectList(null, new RowBounds(1, 10));
6.2 排序字段冲突
当同时存在:
java复制page.addOrder(OrderItem.asc("create_time"));
和SQL中的:
sql复制ORDER BY update_time DESC
时,会产生冲突。解决方案:
java复制// 清空默认排序
page.orders().clear();
7. 生产环境建议
- 监控配置:
java复制// 添加分页操作监控
@Aspect
@Component
public class PageMonitorAspect {
@Around("execution(* com..mapper.*.*Page*(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
Metrics.counter("page.query.count").increment();
Metrics.timer("page.query.time").record(cost, TimeUnit.MILLISECONDS);
}
}
}
- 防御性编程:
java复制// 最大分页大小限制
public Page<T> checkPage(Page<T> page) {
if (page.getSize() > MAX_PAGE_SIZE) {
throw new BusinessException("分页大小超过限制");
}
return page;
}
- 索引建议:
- 分页查询必须包含order by子句
- order by字段需要建立联合索引
- 避免在索引列上使用函数
在实际项目中,这套分页方案成功支撑了日均百万级的查询请求。最关键的是要处理好大数据量下的分页性能问题,必要时可以考虑使用Elasticsearch等专业搜索引擎来分担数据库压力。
