阿坝藏族羌族自治州网站建设_网站建设公司_表单提交_seo优化
2026/1/7 11:59:55 网站建设 项目流程

1. 前言:分页插件的发展与现状

在Java持久层开发中,分页是一个高频需求。目前主流的分页解决方案主要有两种:PageHelperMybatisPlus分页。两者各有优劣:

  • PageHelper:老牌分页插件,支持物理分页和内存分页,使用简单

  • MybatisPlus分页:MybatisPlus自带的分页功能,与MP深度集成

但在实际项目中,我们经常会遇到这样的困境:

  • 项目历史遗留代码使用PageHelper

  • 新模块希望使用MybatisPlus

  • 需要统一分页返回格式

  • 需要兼容两种分页方式

本文将详细介绍如何打造一个兼容两者的增强版分页方案。

2. PageHelper与MybatisPlus分页原理解析

2.1 PageHelper工作原理

java

// PageHelper核心原理:基于Mybatis拦截器 @Intercepts(@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )) public class PageInterceptor implements Interceptor { // 1. 拦截Executor.query方法 // 2. 解析分页参数 // 3. 生成count查询和分页查询SQL // 4. 执行查询并封装结果 }

2.2 MybatisPlus分页原理

java

// MybatisPlus分页拦截器 @Component @Order(0) @Intercepts({@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )}) public class PaginationInterceptor implements Interceptor { // 1. 拦截StatementHandler.prepare // 2. 检测是否需要分页 // 3. 改造SQL语句 // 4. 支持多种数据库方言 }

2.3 两者差异对比

特性PageHelperMybatisPlus分页
启动方式PageHelper.startPage()配置PaginationInterceptor
返回值PageInfoIPage
线程安全使用ThreadLocal,需手动清除每次new Page对象
SQL解析使用JSqlParser使用自研解析器
多租户支持需要额外配置内置支持
性能较好优秀
功能扩展插件体系内置功能丰富

3. 兼容性设计方案

3.1 设计目标

  1. 无缝兼容:无需修改现有代码

  2. 统一API:提供一致的使用体验

  3. 灵活配置:支持按需启用/禁用

  4. 性能优化:避免双重分页

3.2 整体架构设计

text

┌─────────────────────────────────────────┐ │ 业务层调用 │ ├─────────────────────────────────────────┤ │ 统一分页门面(PageHelper/MP自适应) │ ├───────────────┬─────────────────────────┤ │ PageHelper适配器 │ MybatisPlus适配器 │ ├───────────────┴─────────────────────────┤ │ 原生Mybatis执行层 │ └─────────────────────────────────────────┘

4. 核心实现代码

4.1 统一分页配置类

java

@Configuration @ConditionalOnClass({SqlSessionFactory.class, MybatisPlusInterceptor.class}) @AutoConfigureAfter({MybatisPlusAutoConfiguration.class}) public class UnifiedPaginationConfig { /** * 统一分页拦截器 * 优先使用MybatisPlus分页,降级使用PageHelper */ @Bean @ConditionalOnMissingBean public MybatisPlusInterceptor mybatisPlusInterceptor( @Autowired(required = false) PaginationProperties properties) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 自定义分页插件 PaginationInnerInterceptor paginationInterceptor = new EnhancedPaginationInnerInterceptor(); // 设置数据库类型 paginationInterceptor.setDbType(DbType.MYSQL); // 设置分页参数合理化 paginationInterceptor.setOverflow(true); paginationInterceptor.setMaxLimit(1000L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } /** * PageHelper配置(条件注册) */ @Bean @ConditionalOnProperty(name = "pagehelper.enabled", havingValue = "true") @ConditionalOnMissingBean public PageInterceptor pageInterceptor() { Properties properties = new Properties(); properties.setProperty("reasonable", "true"); properties.setProperty("supportMethodsArguments", "true"); properties.setProperty("params", "count=countSql"); properties.setProperty("autoRuntimeDialect", "true"); PageInterceptor pageInterceptor = new PageInterceptor(); pageInterceptor.setProperties(properties); return pageInterceptor; } }

4.2 增强版分页拦截器

java

/** * 增强版分页拦截器 - 兼容PageHelper和MP */ public class EnhancedPaginationInnerInterceptor extends PaginationInnerInterceptor { private static final ThreadLocal<Page<?>> PAGE_INFO = new ThreadLocal<>(); @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 检测是否启用了PageHelper if (isPageHelperEnabled(parameter)) { // 兼容PageHelper调用 handlePageHelperCompatibility(parameter); return; } // 正常MP分页流程 super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); } /** * 检测PageHelper分页 */ private boolean isPageHelperEnabled(Object parameter) { try { // 通过反射检测PageHelper的ThreadLocal变量 Class<?> pageHelperClass = Class.forName("com.github.pagehelper.PageHelper"); Field field = pageHelperClass.getDeclaredField("LOCAL_PAGE"); field.setAccessible(true); Object localPage = field.get(null); if (localPage instanceof ThreadLocal) { ThreadLocal<?> threadLocal = (ThreadLocal<?>) localPage; return threadLocal.get() != null; } } catch (Exception e) { // 忽略异常,表示PageHelper未启用 } return false; } /** * 处理PageHelper兼容性 */ private void handlePageHelperCompatibility(Object parameter) { try { // 获取PageHelper的分页参数 Class<?> pageClass = Class.forName("com.github.pagehelper.Page"); Class<?> pageHelperClass = Class.forName("com.github.pagehelper.PageHelper"); Method getPageMethod = pageHelperClass.getMethod("getLocalPage"); Object page = getPageMethod.invoke(null); if (page != null) { // 转换为MP的Page对象 Method getPageNumMethod = pageClass.getMethod("getPageNum"); Method getPageSizeMethod = pageClass.getMethod("getPageSize"); int pageNum = (int) getPageNumMethod.invoke(page); int pageSize = (int) getPageSizeMethod.invoke(page); // 创建MP Page对象 Page<?> mpPage = new Page<>(pageNum, pageSize); PAGE_INFO.set(mpPage); // 设置到参数中 if (parameter instanceof Map) { ((Map) parameter).put("page", mpPage); } } } catch (Exception e) { throw new RuntimeException("PageHelper兼容处理失败", e); } } @Override public void afterQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, Throwable throwable) { // 清理ThreadLocal PAGE_INFO.remove(); super.afterQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql, throwable); } }

4.3 统一分页工具类

java

/** * 统一分页工具类 * 支持PageHelper和MybatisPlus两种方式 */ public class PageUtils { private static final boolean MP_EXISTS; private static final boolean PAGE_HELPER_EXISTS; static { MP_EXISTS = ClassUtils.isPresent( "com.baomidou.mybatisplus.extension.plugins.pagination.Page", PageUtils.class.getClassLoader() ); PAGE_HELPER_EXISTS = ClassUtils.isPresent( "com.github.pagehelper.PageHelper", PageUtils.class.getClassLoader() ); } /** * 开始分页 - 自动选择分页方式 */ public static <T> void startPage(PageParam param) { if (MP_EXISTS && !PAGE_HELPER_EXISTS) { startPageByMP(param); } else if (PAGE_HELPER_EXISTS) { startPageByPageHelper(param); } else { throw new RuntimeException("未找到可用的分页插件"); } } /** * 使用MybatisPlus分页 */ private static <T> void startPageByMP(PageParam param) { Page<T> page = new Page<>( param.getPageNum(), param.getPageSize() ); // 设置排序 if (StringUtils.isNotBlank(param.getOrderBy())) { page.addOrder(param.getOrderBy(), param.isAsc()); } // 存储到请求上下文 RequestContextHolder.setPage(page); } /** * 使用PageHelper分页 */ private static void startPageByPageHelper(PageParam param) { try { Class<?> pageHelperClass = Class.forName("com.github.pagehelper.PageHelper"); Method startPageMethod = pageHelperClass.getMethod( "startPage", int.class, int.class, String.class ); startPageMethod.invoke(null, param.getPageNum(), param.getPageSize(), param.getOrderBy() ); } catch (Exception e) { throw new RuntimeException("PageHelper分页失败", e); } } /** * 转换分页结果为统一格式 */ public static <T> PageResult<T> convertToPageResult(Object pageObject) { if (pageObject == null) { return PageResult.empty(); } // 判断来源 if (pageObject instanceof IPage) { return convertFromMPPage((IPage<T>) pageObject); } else if (isPageHelperPage(pageObject)) { return convertFromPageHelper(pageObject); } else if (pageObject instanceof List) { return convertFromList((List<T>) pageObject); } else { throw new IllegalArgumentException("不支持的分页类型: " + pageObject.getClass()); } } /** * 从MP Page转换 */ private static <T> PageResult<T> convertFromMPPage(IPage<T> page) { PageResult<T> result = new PageResult<>(); result.setList(page.getRecords()); result.setTotal(page.getTotal()); result.setPageNum((int) page.getCurrent()); result.setPageSize((int) page.getSize()); result.setPages((int) page.getPages()); result.setHasNext(page.getCurrent() < page.getPages()); result.setHasPrevious(page.getCurrent() > 1); return result; } /** * 从PageHelper转换 */ private static <T> PageResult<T> convertFromPageHelper(Object pageObject) { try { Class<?> pageInfoClass = Class.forName("com.github.pagehelper.PageInfo"); Method getListMethod = pageInfoClass.getMethod("getList"); Method getTotalMethod = pageInfoClass.getMethod("getTotal"); Method getPageNumMethod = pageInfoClass.getMethod("getPageNum"); Method getPageSizeMethod = pageInfoClass.getMethod("getPageSize"); Method getPagesMethod = pageInfoClass.getMethod("getPages"); List<T> list = (List<T>) getListMethod.invoke(pageObject); long total = (long) getTotalMethod.invoke(pageObject); int pageNum = (int) getPageNumMethod.invoke(pageObject); int pageSize = (int) getPageSizeMethod.invoke(pageObject); int pages = (int) getPagesMethod.invoke(pageObject); PageResult<T> result = new PageResult<>(); result.setList(list); result.setTotal(total); result.setPageNum(pageNum); result.setPageSize(pageSize); result.setPages(pages); result.setHasNext(pageNum < pages); result.setHasPrevious(pageNum > 1); return result; } catch (Exception e) { throw new RuntimeException("PageHelper结果转换失败", e); } } }

4.4 统一分页注解

java

/** * 统一分页注解 * 支持在Controller方法上使用 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface UnifiedPageable { /** * 当前页码,默认第1页 */ int page() default 1; /** * 每页显示条数,默认20条 */ int size() default 20; /** * 排序字段,格式:字段名,asc|desc */ String[] sort() default {}; /** * 是否统计总数,默认true */ boolean count() default true; /** * 分页方式:auto-自动选择,mp-MybatisPlus,ph-PageHelper */ PageType type() default PageType.AUTO; enum PageType { AUTO, MP, PAGE_HELPER } } /** * 分页参数切面 */ @Aspect @Component @Slf4j public class UnifiedPageableAspect { @Around("@annotation(unifiedPageable)") public Object around(ProceedingJoinPoint joinPoint, UnifiedPageable unifiedPageable) throws Throwable { // 解析请求参数 Object[] args = joinPoint.getArgs(); PageParam pageParam = extractPageParam(args, unifiedPageable); // 开始分页 PageUtils.startPage(pageParam); try { // 执行方法 Object result = joinPoint.proceed(args); // 转换分页结果 return PageUtils.convertToPageResult(result); } finally { // 清理分页参数 if (PAGE_HELPER_EXISTS) { try { Class<?> pageHelperClass = Class.forName("com.github.pagehelper.PageHelper"); Method clearPageMethod = pageHelperClass.getMethod("clearPage"); clearPageMethod.invoke(null); } catch (Exception e) { log.warn("清理PageHelper分页参数失败", e); } } } } }

4.5 请求上下文管理

java

/** * 请求上下文管理器 */ public class RequestContextHolder { private static final ThreadLocal<Map<String, Object>> CONTEXT = ThreadLocal.withInitial(HashMap::new); /** * 设置分页对象 */ public static void setPage(Page<?> page) { getContext().put("page", page); } /** * 获取分页对象 */ @SuppressWarnings("unchecked") public static <T> Page<T> getPage() { return (Page<T>) getContext().get("page"); } /** * 清理上下文 */ public static void clear() { CONTEXT.remove(); } private static Map<String, Object> getContext() { return CONTEXT.get(); } }

5. 使用示例

5.1 Controller层统一使用

java

@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; /** * 方式1:使用注解自动分页 */ @GetMapping("/page") @UnifiedPageable(page = 1, size = 20, sort = {"createTime,desc"}) public PageResult<UserVO> queryUsers(UserQuery query) { // 直接返回Mapper或Service的查询结果 return userService.queryUsers(query); } /** * 方式2:手动分页 */ @GetMapping("/list") public PageResult<UserVO> listUsers( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "20") Integer pageSize, UserQuery query) { // 手动调用分页工具 PageUtils.startPage(new PageParam(pageNum, pageSize, "id desc")); List<UserVO> list = userService.listUsers(query); // 自动转换分页结果 return PageUtils.convertToPageResult(list); } }

5.2 Service层兼容两种方式

java

@Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; /** * 方案A:使用MybatisPlus原生分页 */ @Override public IPage<User> queryUsersByMP(UserQuery query) { // 创建MP的Page对象 Page<User> page = new Page<>( query.getPageNum(), query.getPageSize() ); // 构建查询条件 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(query.getName()), User::getName, query.getName()); wrapper.eq(query.getStatus() != null, User::getStatus, query.getStatus()); wrapper.orderByDesc(User::getCreateTime); // 执行分页查询 return userMapper.selectPage(page, wrapper); } /** * 方案B:使用PageHelper分页 */ @Override public PageInfo<User> queryUsersByPageHelper(UserQuery query) { // PageHelper会自动分页 List<User> users = userMapper.selectByCondition(query); return new PageInfo<>(users); } /** * 方案C:使用统一分页接口 */ @Override public PageResult<UserVO> queryUsers(UserQuery query) { // 统一分页工具会自动处理 List<User> users = userMapper.selectByCondition(query); // 转换VO List<UserVO> vos = convertToVO(users); // 返回统一格式 return new PageResult<>(vos); } }

5.3 Mapper层适配

java

@Mapper public interface UserMapper extends BaseMapper<User> { /** * 方式1:使用MP分页(推荐) */ IPage<User> selectPageByCondition( @Param("page") IPage<User> page, @Param("query") UserQuery query ); /** * 方式2:使用PageHelper分页 */ List<User> selectByCondition(@Param("query") UserQuery query); /** * 方式3:使用自定义SQL(兼容两种) */ @Select("<script>" + "SELECT * FROM user WHERE 1=1" + "<if test='query.name != null'> AND name LIKE CONCAT('%',#{query.name},'%')</if>" + "<if test='query.status != null'> AND status = #{query.status}</if>" + "</script>") List<User> selectByCondition2(@Param("query") UserQuery query); }

6. 配置管理

6.1 配置文件

yaml

# application.yml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 # 分页配置 page: enable-unified: true # 启用统一分页 default-page-size: 20 # 默认每页条数 max-page-size: 1000 # 最大每页条数 overflow: true # 溢出总页数后处理 optimize-count: true # 优化count查询 # PageHelper配置(可选) pagehelper: enabled: false # 默认禁用,按需开启 helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql

6.2 配置类

java

@ConfigurationProperties(prefix = "page") @Data public class PageProperties { /** * 是否启用统一分页 */ private boolean enableUnified = true; /** * 默认分页大小 */ private int defaultPageSize = 20; /** * 最大分页大小 */ private int maxPageSize = 1000; /** * 溢出处理 */ private boolean overflow = true; /** * 优化COUNT查询 */ private boolean optimizeCount = true; /** * 分页方式 */ private PageMode mode = PageMode.AUTO; public enum PageMode { AUTO, MP, PAGE_HELPER } }

7. 高级特性

7.1 动态数据源分页

java

/** * 多数据源分页支持 */ public class DynamicDataSourcePaginationInterceptor extends PaginationInnerInterceptor { @Autowired private DynamicDataSource dataSource; @Override public Dialect getDialect(Executor executor) { // 根据当前数据源选择方言 String dsKey = DynamicDataSourceContextHolder.getDataSourceKey(); DataSource dataSource = this.dataSource.getDataSource(dsKey); // 获取数据库类型 String dbType = getDbType(dataSource); return DialectFactory.getDialect(dbType); } }

7.2 分页缓存优化

java

/** * 分页缓存管理器 */ @Component @Slf4j public class PageCacheManager { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 缓存分页结果 */ public <T> PageResult<T> cachePageResult(String cacheKey, Supplier<PageResult<T>> supplier, long ttl, TimeUnit unit) { // 尝试从缓存获取 PageResult<T> cached = getFromCache(cacheKey); if (cached != null) { return cached; } // 查询并缓存 PageResult<T> result = supplier.get(); putToCache(cacheKey, result, ttl, unit); return result; } /** * 生成分页缓存键 */ public String generatePageCacheKey(String prefix, Object query, int pageNum, int pageSize) { String queryHash = DigestUtils.md5DigestAsHex( JSON.toJSONString(query).getBytes() ); return String.format("%s:%s:%d:%d", prefix, queryHash, pageNum, pageSize); } }

7.3 分页性能监控

java

/** * 分页性能监控 */ @Aspect @Component @Slf4j public class PagePerformanceAspect { private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>(); @Around("execution(* *..*Mapper.*Page*(..))") public Object monitorPageQuery(ProceedingJoinPoint joinPoint) throws Throwable { START_TIME.set(System.currentTimeMillis()); try { return joinPoint.proceed(); } finally { long cost = System.currentTimeMillis() - START_TIME.get(); if (cost > 1000) { // 超过1秒记录警告日志 log.warn("分页查询耗时过长: {}ms, 方法: {}", cost, joinPoint.getSignature().getName()); } START_TIME.remove(); } } }

8. 常见问题与解决方案

8.1 问题1:PageHelper与MP分页冲突

现象:同时启用两个分页插件,导致分页失效或重复分页

解决方案

java

@Configuration public class PaginationConflictSolution { /** * 方案1:互斥配置 */ @Bean @ConditionalOnMissingBean(PageInterceptor.class) public MybatisPlusInterceptor mybatisPlusInterceptor() { // 仅当PageHelper不存在时注册MP分页 } /** * 方案2:统一接管 */ @Bean public UnifiedPaginationInterceptor unifiedInterceptor() { // 使用统一拦截器接管所有分页逻辑 } }

8.2 问题2:分页参数传递问题

现象:Controller接收的分页参数无法传递到Service层

解决方案

java

/** * 分页参数自动绑定 */ @ControllerAdvice public class PageParamAdvice implements WebMvcConfigurer { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new PageParamArgumentResolver()); } static class PageParamArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().equals(PageParam.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); String pageNum = request.getParameter("pageNum"); String pageSize = request.getParameter("pageSize"); String orderBy = request.getParameter("orderBy"); return new PageParam( StringUtils.isNotBlank(pageNum) ? Integer.parseInt(pageNum) : 1, StringUtils.isNotBlank(pageSize) ? Integer.parseInt(pageSize) : 20, orderBy ); } } }

8.3 问题3:复杂SQL分页性能问题

现象:多表关联查询分页性能差

解决方案

java

/** * 分页优化策略 */ public class PageOptimizationStrategy { /** * 策略1:使用延迟关联 */ public PageResult<UserDTO> queryUsersWithOptimization(UserQuery query) { // 1. 先分页查询主键 Page<Long> idPage = userMapper.selectPageIds(query); if (CollectionUtils.isEmpty(idPage.getRecords())) { return PageResult.empty(); } // 2. 根据主键查询完整数据 List<UserDTO> users = userMapper.selectByIds(idPage.getRecords()); // 3. 组装结果 return new PageResult<>( users, idPage.getTotal(), idPage.getCurrent(), idPage.getSize() ); } /** * 策略2:使用覆盖索引 */ @Select("SELECT id, name FROM user WHERE status = #{status} LIMIT #{offset}, #{limit}") List<UserSimpleVO> selectSimpleByPage(@Param("status") Integer status, @Param("offset") Long offset, @Param("limit") Integer limit); }

9. 最佳实践建议

9.1 项目迁移建议

  1. 渐进式迁移

    • 新模块使用统一分页方案

    • 老模块逐步改造

    • 设立兼容层

  2. 统一规范

    • 定义统一的分页返回格式

    • 制定分页参数命名规范

    • 建立分页性能标准

9.2 性能优化建议

  1. SQL层面

    sql

    -- 避免使用 SELECT * SELECT id, name, age FROM user LIMIT 0, 20; -- 使用覆盖索引 SELECT id FROM user WHERE status = 1 ORDER BY create_time DESC LIMIT 10000, 20;
  2. 应用层面

    • 合理设置分页大小

    • 实现分页缓存

    • 监控慢分页查询

9.3 代码规范建议

  1. Controller层

    java

    // ✅ 推荐 @GetMapping @UnifiedPageable public PageResult<UserVO> list(UserQuery query) { return userService.listUsers(query); } // ❌ 不推荐 @GetMapping public Map<String, Object> list(@RequestParam int page, @RequestParam int size) { // 手动处理分页逻辑 }
  2. Service层

    java

    // ✅ 推荐:使用统一分页工具 public PageResult<UserVO> listUsers(UserQuery query) { PageUtils.startPage(query.toPageParam()); List<User> users = userMapper.selectList(query); return PageUtils.convertToPageResult(users); }

10. 总结

本文详细介绍了如何打造一个兼容PageHelper和MybatisPlus的增强版分页方案。关键要点:

  1. 统一抽象:通过适配器模式统一两种分页方式

  2. 无缝兼容:支持现有代码平滑迁移

  3. 灵活配置:支持按需选择分页策略

  4. 性能优化:提供多种优化方案

  5. 监控完善:内置性能监控和问题排查

实际项目中,建议根据团队技术栈和项目需求选择合适的方案。对于新项目,推荐直接使用MybatisPlus分页;对于老项目迁移,可以使用本文的统一分页方案作为过渡。

核心价值

  • 降低学习成本:团队成员只需掌握一套API

  • 提高开发效率:减少重复的分页代码

  • 便于维护:统一的分页逻辑和监控

  • 平滑迁移:支持渐进式改造

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询