解决MyBatis-Plus与PageHelper分页冲突的5种方案

📅 2026/7/18 2:31:44
解决MyBatis-Plus与PageHelper分页冲突的5种方案
1. 项目背景与问题定位在Java持久层开发中MyBatis-Plus和PageHelper都是高频使用的工具库。前者提供了强大的CRUD封装和条件构造器后者则是老牌的分页插件。但当两者在同一个项目中并存时开发者经常会遇到分页信息丢失或异常的棘手问题。最近在重构一个老项目时我就踩到了这个坑。场景是这样的使用PageHelper.startPage()设置分页参数后通过MyBatis-Plus的selectVoList方法查询数据结果发现返回的PageInfo对象中pageNum始终为1total总数也不正确。但奇怪的是查看执行SQL明明有LIMIT分页参数。通过DEBUG追踪发现问题出在MyBatis-Plus的自动填充机制上。当使用selectVoList这类扩展方法时MyBatis-Plus会在内部对查询结果进行二次处理导致PageHelper收集的分页上下文信息丢失。这种隐性的兼容性问题正是需要解决的痛点。2. 核心原理深度解析2.1 PageHelper的工作机制PageHelper通过MyBatis的Interceptor接口实现分页拦截。其核心流程分为三步参数拦截阶段调用PageHelper.startPage()时会将分页参数存入ThreadLocal// 实际存储结构 public class Page { private int pageNum; private int pageSize; private boolean count; // getters setters }SQL改写阶段在Executor.query()方法执行前通过方言类(Dialect)将原始SQL改写为分页SQL-- 原始SQL SELECT * FROM user -- 改写后 SELECT * FROM user LIMIT 0,10结果封装阶段查询结束后用PageInfo对结果集和分页参数进行包装2.2 MyBatis-Plus的分页实现MyBatis-Plus通过PaginationInnerInterceptor实现分页其特点包括基于MyBatis的Interceptor机制与Wrapper条件构造器深度集成返回自定义的Page对象而非PageInfopublic class PageT { private ListT records; private long total; private long size; private long current; // 其他分页参数... }2.3 冲突根源分析两者的兼容性问题主要来自执行顺序冲突PageHelper的拦截器优先级可能低于MyBatis-Plus的拦截器结果处理冲突MyBatis-Plus的自动填充会修改结果对象结构线程上下文竞争两者都依赖ThreadLocal存储分页参数3. 五种实战解决方案3.1 方案一统一使用MyBatis-Plus分页配置步骤添加分页插件配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }使用IPage接口进行分页查询PageUser page new Page(1, 10); QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(status, 1); page userMapper.selectPage(page, wrapper); // 获取分页信息 long total page.getTotal(); ListUser records page.getRecords();优势完全兼容MyBatis-Plus所有特性分页信息准确可靠注意事项需要统一项目中的分页方式Page对象与前端对接可能需要适配3.2 方案二改造PageHelper拦截器顺序实施步骤自定义拦截器配置Bean public ConfigurationCustomizer configurationCustomizer() { return configuration - { // 调整拦截器顺序 configuration.addInterceptor(new PageInterceptor()); configuration.addInterceptor(new MybatisPlusInterceptor()); }; }添加特殊注解标记PageHelperCompatible public interface UserMapper { Select(select * from user) ListUser selectAll(); }适用场景老项目渐进式改造需要保留PageHelper的特殊功能潜在风险复杂查询可能仍有兼容性问题需要严格测试各场景3.3 方案三自定义分页适配器实现代码public class PageHelperAdapter { public static T PageInfoT startPage(IPageT page) { PageInfoT pageInfo new PageInfo(); pageInfo.setPageNum((int)page.getCurrent()); pageInfo.setPageSize((int)page.getSize()); pageInfo.setTotal(page.getTotal()); pageInfo.setList(page.getRecords()); // 计算其他分页参数... return pageInfo; } }使用示例PageUser page new Page(1, 10); userMapper.selectPage(page, wrapper); PageInfoUser pageInfo PageHelperAdapter.startPage(page);优势保持PageHelper的接口风格底层使用MyBatis-Plus分页3.4 方案四AOP统一处理切面实现Aspect Component public class PageAspect { Around(annotation(compatiblePage)) public Object around(ProceedingJoinPoint joinPoint, CompatiblePage compatiblePage) throws Throwable { Object[] args joinPoint.getArgs(); // 解析分页参数 PageParam param parseParam(args); // 执行MyBatis-Plus分页 Page? page new Page(param.getPageNum(), param.getPageSize()); Object result joinPoint.proceed(new Object[]{page, args[1]}); // 转换为PageInfo return convertToPageInfo(page); } }注解定义Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface CompatiblePage { int pageNum() default 1; int pageSize() default 10; }3.5 方案五混合模式分页策略选择器public class PageStrategy { private static final ThreadLocalBoolean useMp new ThreadLocal(); public static void useMybatisPlus() { useMp.set(true); } public static void usePageHelper() { useMp.set(false); } public static boolean isUseMp() { return Boolean.TRUE.equals(useMp.get()); } }拦截器改造public class CompatibleInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { if (PageStrategy.isUseMp()) { // MyBatis-Plus分页逻辑 } else { // PageHelper分页逻辑 } } }4. 生产环境避坑指南4.1 常见问题排查表现象可能原因解决方案total为0分页插件未生效检查拦截器顺序pageNum不正确线程上下文污染清理ThreadLocal性能下降重复分页计算关闭count查询4.2 性能优化建议分页查询优化-- 低效写法 SELECT * FROM large_table LIMIT 1000000,10 -- 优化写法 SELECT * FROM large_table WHERE id 1000000 LIMIT 10Count查询优化// 关闭自动count查询 page.setSearchCount(false);线程池隔离Bean(destroyMethod shutdown) public Executor asyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setTaskDecorator(runnable - { // 清理分页上下文 PageHelper.clearPage(); return runnable; }); return executor; }4.3 兼容性测试要点测试各种Wrapper组合场景验证多线程并发情况检查事务传播行为模拟大数据量分页5. 架构设计思考对于新项目建议直接采用MyBatis-Plus的统一分页方案。对于遗留系统改造可以按以下路径演进过渡期使用方案三的适配器模式改造期逐步替换为方案一收尾期移除PageHelper依赖在微服务架构中建议在API层统一分页响应格式public class PageResultT { private Integer code; private String msg; private PageDataT data; public static class PageDataT { private ListT records; private long total; private long size; private long current; } }对于需要深度分页的场景可以考虑使用游标分页或ES等专业方案替代传统LIMIT分页。