1. 为什么需要扩展Mybatis Plus的分页排序功能Mybatis Plus作为Mybatis的增强工具已经内置了强大的分页功能。但在实际开发中我们经常会遇到一些特殊的分页排序需求比如多字段动态排序前端传递多个排序字段和排序方式ASC/DESC复杂SQL排序需要对关联查询结果或计算字段进行排序混合排序需要同时使用Mybatis Plus的分页和PageHelper的排序特性原生Mybatis Plus的Page对象虽然支持addOrder()方法添加排序条件但在复杂SQL场景下存在局限性。而PageHelper作为另一个流行的分页插件其排序语法更加灵活。这就是为什么我们需要在Mybatis Plus基础上扩展PageHelper的排序功能。2. 核心实现原理与技术选型2.1 Mybatis Plus分页机制解析Mybatis Plus的分页基于拦截器实现核心类是PaginationInnerInterceptor。它会在SQL执行前对语句进行改写添加数据库特定的分页语法如MySQL的LIMIT。分页参数通过Page对象传递PageUser page new Page(1, 10); // 当前页每页数量 page.addOrder(OrderItem.asc(create_time)); userMapper.selectPage(page, queryWrapper);2.2 PageHelper排序原理PageHelper通过ThreadLocal保存分页参数其排序语法更加灵活PageHelper.startPage(1, 10) .setOrderBy(create_time asc, age desc);这种字符串形式的排序条件更易于从前端接收和动态构建。2.3 混合使用的技术挑战直接同时使用两个插件会导致分页逻辑冲突双重分页线程安全问题SQL解析异常我们的解决方案是保持Mybatis Plus的分页机制只借鉴PageHelper的排序语法通过自定义拦截器实现。3. 完整实现步骤3.1 添加依赖确保pom.xml中包含dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.2/version /dependency3.2 创建自定义Page对象扩展Mybatis Plus的Page类添加PageHelper风格的排序支持public class PageHelperPageT extends PageT { private String orderBy; public PageHelperPage(long current, long size) { super(current, size); } public PageHelperPageT setOrderBy(String orderBy) { this.orderBy orderBy; return this; } public String getOrderBy() { return this.orderBy; } }3.3 实现排序拦截器创建拦截器将PageHelper排序语法转换为Mybatis Plus的OrderItemIntercepts(Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public class PageHelperSortInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { Object parameter invocation.getArgs()[1]; if (parameter instanceof Map) { Map?, ? map (Map?, ?) parameter; for (Object value : map.values()) { if (value instanceof PageHelperPage) { PageHelperPage? page (PageHelperPage?) value; if (StringUtils.isNotBlank(page.getOrderBy())) { String[] orders page.getOrderBy().split(,); for (String order : orders) { String[] parts order.trim().split(\\s); if (parts.length 2) { page.addOrder(asc.equalsIgnoreCase(parts[1]) ? OrderItem.asc(parts[0]) : OrderItem.desc(parts[0])); } } } } } } return invocation.proceed(); } // 其他必要方法... }3.4 注册拦截器并测试在Mybatis配置中注册拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); interceptor.addInnerInterceptor(new PageHelperSortInterceptor()); return interceptor; } }使用示例Test public void testPageWithSort() { PageHelperPageUser page new PageHelperPage(1, 10) .setOrderBy(create_time desc, age asc); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getDeleted, 0); userMapper.selectPage(page, wrapper); page.getRecords().forEach(System.out::println); }4. 高级应用场景与优化4.1 动态排序接收前端参数Controller层可以这样接收排序参数GetMapping(/users) public ResultIPageUser listUsers( RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String sort) { PageHelperPageUser page new PageHelperPage(pageNum, pageSize); if (StringUtils.isNotBlank(sort)) { page.setOrderBy(sort); // 如create_time desc,age asc } // 查询逻辑... }4.2 SQL注入防护需要对排序字段进行白名单校验public class SafeOrderByParser { private static final SetString ALLOWED_COLUMNS Set.of( create_time, age, name // 允许排序的字段 ); public static String parse(String orderBy) { if (StringUtils.isBlank(orderBy)) return null; return Arrays.stream(orderBy.split(,)) .map(String::trim) .filter(expr - { String[] parts expr.split(\\s); return parts.length 0 ALLOWED_COLUMNS.contains(parts[0]); }) .collect(Collectors.joining(,)); } }4.3 多表关联查询排序对于关联查询需要处理表别名// 在Service层处理 String orderBy u.create_time desc, d.dept_name asc; page.setOrderBy(orderBy); // SQL中使用别名 Select(SELECT u.*, d.dept_name FROM user u LEFT JOIN dept d ON u.dept_id d.id) IPageUser selectUserWithDept(PageUser page, Param(ew) WrapperUser wrapper);5. 性能优化与问题排查5.1 分页性能优化避免大偏移量当页码较大时使用WHERE id ? LIMIT ?代替LIMIT offset, size合理使用索引确保排序字段有适当索引缓存分页结果对静态数据使用缓存5.2 常见问题排查问题1排序失效检查拦截器是否正确注册检查SQL中是否有GROUP BY或DISTINCT影响排序检查字段名是否正确包括大小写问题2分页结果不正确确保没有其他地方重复调用了PageHelper.startPage()检查是否有其他拦截器修改了SQL问题3内存溢出避免一次性查询大量数据即使分页参数很小检查是否有N1查询问题6. 替代方案对比6.1 原生Mybatis Plus排序优点与Mybatis Plus深度集成类型安全通过Lambda表达式缺点动态排序不够灵活多字段排序代码冗长6.2 纯PageHelper方案优点排序语法灵活支持复杂SQL缺点与Mybatis Plus分页机制冲突需要手动管理ThreadLocal6.3 自定义扩展方案本文结合两者优点 ✓ 保持Mybatis Plus分页机制 ✓ 支持PageHelper风格排序 ✓ 线程安全 ✓ 易于与现有代码集成7. 实际项目中的经验分享排序字段映射我们经常遇到前端字段名与数据库列名不一致的情况。解决方案是创建字段映射表MapString, String fieldMap Map.of( createTime, create_time, userName, name ); String dbOrderBy Arrays.stream(orderBy.split(,)) .map(expr - { String[] parts expr.trim().split(\\s); return fieldMap.getOrDefault(parts[0], parts[0]) (parts.length 1 ? parts[1] : ); }) .collect(Collectors.joining(,));默认排序策略建议始终指定默认排序避免分页结果随机变化public PageHelperPageT withDefaultSort(String defaultSort) { if (StringUtils.isBlank(this.orderBy)) { this.orderBy defaultSort; } return this; }分页最大限制防止恶意请求导致的大分页public PageHelperPageT withMaxSize(int maxSize) { if (this.getSize() maxSize) { this.setSize(maxSize); } return this; }Kotlin扩展方法如果是Kotlin项目可以添加扩展方法使调用更优雅fun T PageHelperPageT.by(sort: String): PageHelperPageT this.apply { orderBy sort } // 使用 val page PageHelperPageUser(1, 10) .by(create_time desc) .withDefaultSort(id desc)