海口专业网站建设手机端网站建设广告词
海口专业网站建设,手机端网站建设广告词,页面设计怎么设计,网站建设公司网站定制开发若依框架升级MyBatis-Plus 3.4.2实战#xff1a;如何无缝兼容旧版分页功能#xff1f;
如果你正在维护一个基于若依框架的Java项目#xff0c;并且团队决定引入MyBatis-Plus来提升开发效率#xff0c;那么你很可能正面临一个棘手的挑战#xff1a;如何让MyBatis-Plus那套全…若依框架升级MyBatis-Plus 3.4.2实战如何无缝兼容旧版分页功能如果你正在维护一个基于若依框架的Java项目并且团队决定引入MyBatis-Plus来提升开发效率那么你很可能正面临一个棘手的挑战如何让MyBatis-Plus那套全新的分页机制与若依框架中原有的、基于PageHelper的分页逻辑和平共处这不仅仅是换个依赖、改个配置那么简单它涉及到插件冲突、参数解析、数据封装乃至团队开发习惯的平滑过渡。今天我们就来深入拆解这个升级过程分享一套经过实战检验的、能确保业务零感知的兼容方案。我的团队在最近一次技术栈升级中就踩遍了这里的坑。最初我们天真地以为引入MyBatis-Plus后只需简单替换分页调用方式即可。结果却是前端表格渲染错乱、排序功能失效甚至引发了潜在的SQL注入风险。经过几轮调试和方案重构我们最终找到了一条清晰、稳定且向后兼容的路径。本文将不仅告诉你每一步该怎么操作更会解释其背后的设计考量让你知其然更知其所以然从容应对升级过程中的各种“惊喜”。1. 依赖治理与冲突消解为和平共处奠定基础升级的第一步往往始于pom.xml。若依框架通常自带PageHelper而MyBatis-Plus也有自己的分页插件和底层依赖。强行共存极易引发类冲突最常见的就是jsqlparser版本冲突导致应用启动失败或运行时行为异常。我们的目标不是粗暴地移除PageHelper因为旧代码可能还在用它而是让两者在可控范围内协同工作。首先我们需要在项目最外层的POM文件中统一管理相关依赖的版本。一个关键技巧是将PageHelper依赖中与MyBatis-Plus冲突的传递性依赖排除掉。!-- 版本属性定义 -- properties pagehelper.boot.version1.4.6/pagehelper.boot.version mybatis-plus.version3.4.2/mybatis-plus.version /properties !-- PageHelper Starter排除冲突的MyBatis及jsqlparser依赖 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version${pagehelper.boot.version}/version exclusions exclusion groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId /exclusion exclusion groupIdorg.mybatis/groupId artifactIdmybatis/artifactId /exclusion exclusion groupIdcom.github.jsqlparser/groupId artifactIdjsqlparser/artifactId /exclusion /exclusions /dependency !-- MyBatis-Plus Starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency注意这里排除jsqlparser是因为MyBatis-Plus 3.4.2内部使用的是4.0.0版本而PageHelper 1.4.6可能依赖了更高的版本。强制排除后整个项目将统一使用MyBatis-Plus引入的版本避免因类加载器加载了不同版本的同一个类而导致的NoSuchMethodError或ClassCastException。对于若依这种多模块项目你还需要在common或framework这类被其他业务模块依赖的基础模块中添加MyBatis-Plus的依赖确保所有模块都能访问到相关的类。仅仅处理完依赖还不够我们还需要审视若依原有的MyBatis配置。通常若依会有一个MyBatisConfig类用于配置原生的MyBatis插件包括PageHelper。在引入MyBatis-Plus后这个类需要被完全禁用因为MyBatis-Plus的启动器mybatis-plus-boot-starter已经通过自动配置接管了SqlSessionFactory等核心Bean的创建。你可以在该类上直接添加Configuration(proxyBeanMethods false)并注释掉其内部的Bean方法或者更直接地在启动类扫描路径中排除它。2. 核心配置重塑打造安全高效的分页中枢禁用旧配置后我们需要建立一套新的、同时服务于MyBatis-Plus分页和兼容旧接口的配置体系。这不仅仅是注册一个分页插件那么简单更需要考虑Web层参数如何安全、正确地转换为分页对象。2.1 配置MyBatis-Plus分页插件在framework模块的config包下我们创建一个新的配置类MybatisPlusConfig。Configuration public class MybatisPlusConfig { /** * 核心插件配置。 * 添加分页插件并指定数据库类型为MySQL。 * 此处还可以添加其他插件如乐观锁插件、动态表名插件等。 */ Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 分页插件 PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); // 设置最大单页限制数量-1表示不受限制生产环境建议设置一个合理值 paginationInnerInterceptor.setMaxLimit(-1L); // 设置溢出总页数后是否进行处理默认false溢出后直接返回第一页 paginationInnerInterceptor.setOverflow(false); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }这个配置确保了MyBatis-Plus自身的Page对象在Mapper层查询时能够被正确识别并拼接分页SQL。2.2 实现自定义参数解析器关键步骤若依框架的前端分页请求参数如pageNum,pageSize,orderByColumn,isAsc与MyBatis-Plus的Page对象使用current,size,ascs,descs等参数并不直接匹配。更关键的是直接从前端接收排序字段存在SQL注入风险。因此我们需要一个自定义的HandlerMethodArgumentResolver来桥接这个鸿沟并增加安全过滤。创建一个MyBatisPlusPageArgumentResolver类Slf4j Component public class MyBatisPlusPageArgumentResolver implements HandlerMethodArgumentResolver { // 定义需要过滤的SQL关键字防止通过排序字段注入 private static final SetString SQL_KEYWORDS Stream.of( master, truncate, insert, select, delete, update, declare, alter, drop, sleep, union ).collect(Collectors.toSet()); /** * 判断是否支持处理该参数。这里我们支持若依原生的PageDomain和MyBatis-Plus的Page。 */ Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().equals(com.baomidou.mybatisplus.extension.plugins.pagination.Page.class) || parameter.getParameterType().equals(com.ruoyi.common.core.page.PageDomain.class); } /** * 核心解析方法将HTTP请求参数安全地转换为分页对象。 */ Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request webRequest.getNativeRequest(HttpServletRequest.class); // 处理MyBatis-Plus Page对象请求 if (parameter.getParameterType().equals(com.baomidou.mybatisplus.extension.plugins.pagination.Page.class)) { com.baomidou.mybatisplus.extension.plugins.pagination.Page? page new com.baomidou.mybatisplus.extension.plugins.pagination.Page(); // 解析 current 和 size (对应原pageNum和pageSize) String current request.getParameter(pageNum); // 兼容若依旧参数名 if (StringUtils.isEmpty(current)) { current request.getParameter(current); // 也支持MP原生参数名 } String size request.getParameter(pageSize); if (StringUtils.isEmpty(size)) { size request.getParameter(size); } if (StringUtils.isNotBlank(current)) { page.setCurrent(Long.parseLong(current)); } if (StringUtils.isNotBlank(size)) { page.setSize(Long.parseLong(size)); } // 安全地解析排序字段 String orderByColumn request.getParameter(orderByColumn); String isAsc request.getParameter(isAsc); ListOrderItem orderItems buildSafeOrderItems(orderByColumn, isAsc); page.addOrder(orderItems); // 同时也解析MP原生风格的排序参数作为补充 String[] ascs request.getParameterValues(ascs); String[] descs request.getParameterValues(descs); addSafeOrderItemsFromArrays(page, ascs, descs); return page; } // 如果需要这里也可以保留对若依原PageDomain的解析用于尚未改造的旧接口 // ... 解析PageDomain的代码 ... return null; } /** * 安全地构建排序项。核心安全校验逻辑在此。 */ private ListOrderItem buildSafeOrderItems(String orderByColumn, String isAsc) { ListOrderItem orderItems new ArrayList(); if (StringUtils.isBlank(orderByColumn)) { return orderItems; } // 防止SQL注入检查排序字段是否包含危险关键字 if (containsSqlKeyword(orderByColumn)) { log.warn(检测到潜在的SQL注入风险排序字段已被忽略: {}, orderByColumn); return orderItems; } // 驼峰转下划线是常见需求例如前端传createTime数据库字段是create_time String column StringUtils.toUnderScoreCase(orderByColumn); if (desc.equalsIgnoreCase(isAsc)) { orderItems.add(OrderItem.desc(column)); } else { // 默认升序 orderItems.add(OrderItem.asc(column)); } return orderItems; } /** * 检查字符串是否包含SQL关键字。 */ private boolean containsSqlKeyword(String str) { if (StringUtils.isBlank(str)) { return false; } String lowerCaseStr str.toLowerCase(); return SQL_KEYWORDS.stream().anyMatch(lowerCaseStr::contains); } /** * 处理MP原生数组格式的排序参数。 */ private void addSafeOrderItemsFromArrays(com.baomidou.mybatisplus.extension.plugins.pagination.Page? page, String[] ascs, String[] descs) { // 安全过滤并添加ascs Optional.ofNullable(ascs).ifPresent(array - Arrays.stream(array) .filter(field - !containsSqlKeyword(field)) .map(OrderItem::asc) .forEach(page::addOrder) ); // 安全过滤并添加descs Optional.ofNullable(descs).ifPresent(array - Arrays.stream(array) .filter(field - !containsSqlKeyword(field)) .map(OrderItem::desc) .forEach(page::addOrder) ); } }接下来我们需要将这个解析器注册到Spring MVC中。修改或创建Web配置类Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private MyBatisPlusPageArgumentResolver pageArgumentResolver; Override public void addArgumentResolvers(ListHandlerMethodArgumentResolver argumentResolvers) { // 将自定义分页参数解析器添加到最前面确保其优先级 argumentResolvers.add(0, pageArgumentResolver); } }2.3 配置YAML/Properties文件在application.yml中将原有的mybatis配置替换为mybatis-plus配置。MyBatis-Plus的配置项基本兼容原生MyBatis并提供了更多扩展。# 注释或删除原有的mybatis配置 # mybatis: # mapper-locations: classpath*:mapper/**/*Mapper.xml # 启用mybatis-plus配置 mybatis-plus: # mapper XML文件位置 mapper-locations: classpath*:mapper/**/*Mapper.xml # 全局配置 global-config: db-config: # 全局逻辑删除字段名若依常用delete_flag logic-delete-field: deleteFlag # 逻辑删除值1表示已删除根据你的数据库设计调整 logic-delete-value: 1 # 逻辑未删除值 logic-not-delete-value: 0 configuration: # 开启驼峰命名自动映射 map-underscore-to-camel-case: true # 配置日志实现方便调试SQL log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 生产环境建议使用Slf4jImpl # log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl3. 数据层与业务层改造拥抱新范式配置完成后我们需要对数据访问层进行改造以充分利用MyBatis-Plus的CRUD接口。3.1 实体类Entity改造为实体类添加MyBatis-Plus的注解。最重要的是TableName如果表名遵循驼峰转下划线规则且能匹配可以省略。import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; Data TableName(sys_user) // 指定数据库表名 public class SysUser { /** * 主键。若依常用Id和GeneratedValue需替换为MP注解。 * ASSIGN_ID 是MP的雪花算法ID生成策略。 */ TableId(type IdType.ASSIGN_ID) private Long userId; private String userName; private String nickName; // ... 其他字段 // 若依框架常用的逻辑删除字段和审计字段可以通过自动填充处理器处理见下文 private String deleteFlag; private String createBy; private Date createTime; private String updateBy; private Date updateTime; }3.2 Mapper接口改造让Mapper接口继承MyBatis-Plus的BaseMapper即可获得丰富的单表操作方法。import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.system.domain.SysUser; public interface SysUserMapper extends BaseMapperSysUser { // 原有的自定义查询方法可以保留 // ListSysUser selectUserList(SysUser user); }3.3 Service层改造Service层的改造能带来最大的效率提升。我们可以创建两套接口和实现一套用于新的、基于MyBatis-Plus的服务另一套暂时保留用于兼容旧业务调用。新的Service接口import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.system.domain.SysUser; public interface ISysUserPlusService extends IServiceSysUser { // 可以在此定义复杂的业务方法简单CRUD已由IService提供 PageSysUser selectUserPage(PageSysUser page, SysUser user); }新的Service实现Service public class SysUserPlusServiceImpl extends ServiceImplSysUserMapper, SysUser implements ISysUserPlusService { Override public PageSysUser selectUserPage(PageSysUser page, SysUser user) { // 使用LambdaQueryWrapper类型安全且直观 LambdaQueryWrapperSysUser lqw new LambdaQueryWrapper(); lqw.like(StringUtils.isNotBlank(user.getUserName()), SysUser::getUserName, user.getUserName()) .like(StringUtils.isNotBlank(user.getNickName()), SysUser::getNickName, user.getNickName()) .eq(user.getStatus() ! null, SysUser::getStatus, user.getStatus()) .orderByDesc(SysUser::getCreateTime); // 默认排序 return this.page(page, lqw); } }对于原有的ISysUserService和SysUserServiceImpl如果其中大量方法调用了原有的SysUserMapper自定义方法短期内可以不做修改让其继续运行。新的业务代码则直接注入ISysUserPlusService使用。这是一种渐进式的改造策略。3.4 实现自动填充审计字段若依框架通常有创建人、创建时间等审计字段。MyBatis-Plus提供了优雅的MetaObjectHandler接口来实现自动填充。Component Slf4j public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { log.debug(开始进行插入操作自动填充...); // 填充创建时间和更新时间 Date now new Date(); this.strictInsertFill(metaObject, createTime, Date.class, now); this.strictInsertFill(metaObject, updateTime, Date.class, now); // 填充创建人和更新人从安全上下文中获取 String currentUser getCurrentUsername(); this.strictInsertFill(metaObject, createBy, String.class, currentUser); this.strictInsertFill(metaObject, updateBy, String.class, currentUser); // 填充逻辑删除默认值如果字段存在 if (metaObject.hasSetter(deleteFlag)) { this.strictInsertFill(metaObject, deleteFlag, String.class, 0); } } Override public void updateFill(MetaObject metaObject) { log.debug(开始进行更新操作自动填充...); this.strictUpdateFill(metaObject, updateTime, Date.class, new Date()); this.strictUpdateFill(metaObject, updateBy, String.class, getCurrentUsername()); } /** * 获取当前登录用户名。这里需要适配你的权限框架如Spring Security, Shiro等。 */ private String getCurrentUsername() { try { // 示例若依框架中通常通过SecurityUtils获取 // LoginUser loginUser SecurityUtils.getLoginUser(); // return loginUser ! null ? loginUser.getUsername() : system; return admin; // 临时返回固定值实际项目需替换 } catch (Exception e) { // 在定时任务或无登录上下文中返回系统标识 return system; } } }别忘了在实体类的对应字段上添加TableField注解来标记需要自动填充TableField(fill FieldFill.INSERT) private String createBy; TableField(fill FieldFill.INSERT) private Date createTime; TableField(fill FieldFill.INSERT_UPDATE) private String updateBy; TableField(fill FieldFill.INSERT_UPDATE) private Date updateTime;4. 控制器层兼容与测试验证这是确保前端页面无需任何修改的关键一步。若依前端的表格组件通常期望后端返回一个特定格式的JSON包含code,msg,rows,total等字段。4.1 增强BaseController若依的BaseController中通常有一个getDataTable方法用于包装分页数据。我们需要重载一个方法使其能接受MyBatis-Plus的Page对象。public class BaseController { /** * 原方法用于包装PageHelper分页结果。 */ protected TableDataInfo getDataTable(List? list) { TableDataInfo rspData new TableDataInfo(); rspData.setCode(HttpStatus.SUCCESS); rspData.setMsg(查询成功); rspData.setRows(list); rspData.setTotal(list instanceof Page ? ((Page?) list).getTotal() : list.size()); return rspData; } /** * 新增方法专门用于包装MyBatis-Plus的Page对象。 * param page MyBatis-Plus的分页结果对象 * return 符合若依前端格式的TableDataInfo */ protected TableDataInfo getDataTable(com.baomidou.mybatisplus.extension.plugins.pagination.Page? page) { TableDataInfo rspData new TableDataInfo(); rspData.setCode(HttpStatus.SUCCESS); rspData.setMsg(查询成功); rspData.setRows(page.getRecords()); rspData.setTotal(page.getTotal()); return rspData; } }4.2 编写新的控制器方法现在我们可以在控制器中编写同时兼容新旧参数、并返回标准格式的分页查询接口了。RestController RequestMapping(/system/user) public class SysUserController extends BaseController { Autowired private ISysUserPlusService userPlusService; /** * 用户分页列表查询兼容MyBatis-Plus和旧参数 * 支持参数 * - 旧风格: pageNum, pageSize, orderByColumn, isAsc * - MP风格: current, size, ascs[], descs[] * param page MyBatis-Plus Page对象由自定义解析器自动注入 * param user 查询条件对象 * return TableDataInfo */ GetMapping(/list) public TableDataInfo list(com.baomidou.mybatisplus.extension.plugins.pagination.PageSysUser page, SysUser user) { // 直接调用我们改造后的Service方法 com.baomidou.mybatisplus.extension.plugins.pagination.PageSysUser resultPage userPlusService.selectUserPage(page, user); // 使用BaseController中新增的方法进行包装 return getDataTable(resultPage); } /** * 一个使用MyBatis-Plus LambdaUpdate进行更新的例子 */ PostMapping(/updateStatus) public AjaxResult updateStatus(RequestBody SysUser user) { if (user.getUserId() null) { return AjaxResult.error(用户ID不能为空); } boolean success userPlusService.update( new LambdaUpdateWrapperSysUser() .set(SysUser::getStatus, user.getStatus()) .set(SysUser::getUpdateBy, SecurityUtils.getUsername()) // 手动设置更新人 .eq(SysUser::getUserId, user.getUserId()) ); return success ? AjaxResult.success() : AjaxResult.error(更新失败); } }4.3 全面测试验证升级完成后必须进行全方位的测试确保功能无损。1. 基础分页查询测试使用Postman或前端页面发起请求分别测试新旧参数格式。GET /system/user/list?pageNum1pageSize10orderByColumncreateTimeisAscdescGET /system/user/list?current2size5ascs[]userNamedescs[]createTime检查返回的JSON结构是否正确rows数据是否对应请求的页码和条数total总数是否正确排序是否生效。2. SQL注入防御测试尝试在排序字段中注入SQL关键字例如GET /system/user/list?orderByColumncreateTime;select%201isAscdesc观察日志和返回结果。正确的实现应该忽略这个危险的排序字段或者返回一个默认排序而不会将select 1拼接到SQL中。3. 增删改查与自动填充测试创建一个新用户检查createBy和createTime是否被自动填充。更新一个用户检查updateBy和updateTime是否被自动更新。进行逻辑删除操作检查deleteFlag字段是否按配置变更。4. 与旧接口并行测试确保那些尚未改造、仍在使用原有PageHelper和旧Service的接口功能完全正常。这验证了我们的兼容性方案没有破坏现有功能。在测试过程中打开MyBatis-Plus的SQL日志输出非常有用可以直观地看到最终执行的SQL语句确认分页和排序逻辑是否正确拼接。mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl观察日志你应该能看到类似这样的SQL说明MyBatis-Plus的分页插件和我们的参数解析器工作正常SELECT COUNT(*) FROM sys_user WHERE delete_flag 0 AND user_name LIKE ? SELECT user_id, user_name, nick_name, ... FROM sys_user WHERE delete_flag 0 AND user_name LIKE ? ORDER BY create_time DESC LIMIT ?, ?整个升级过程从依赖冲突的精准排除到安全可控的参数解析器设计再到数据层和控制器层的渐进式改造最后通过严密的测试验证我们构建了一套稳健的兼容方案。这套方案的核心思想不是“替换”而是“桥接”和“共存”在引入强大新功能的同时最大限度地保障了系统的稳定性和团队的开发惯性。当你成功完成这一切你会发现不仅分页功能无缝兼容整个数据层的开发体验也因为MyBatis-Plus的加持而变得前所未有的高效。