资讯详情

RuoYi代码生成器改造:让生成代码完美适配MyBatis-Plus

📅 2026/10/18 4:41:43 | 华诺云谱 👁 阅读
RuoYi代码生成器改造:让生成代码完美适配MyBatis-Plus
1. 定位问题默认代码生成器吐出来的代码和MyBatis-Plus不在一个频道上1.1 先弄清楚“脚本格式”到底指什么RuoYi前后端分离版自带的代码生成器本质是一条流水线你把数据库里的表导入生成菜单它读取表结构元数据套用一组基于Velocity的vm模板最后产出一整套Java代码和前端Vue代码。大家平时说“生成的代码脚本”指的就是这套模板渲染出来的Controller、Service、Mapper、XML、实体类文件。默认模板的设计思路是配合原生MyBatis PageHelper的所以生成出来的东西有一个非常明显的特征Controller里用startPage()开分页Service实现类里手写一堆CRUD方法Mapper接口里列了一长串方法XML里则充斥着resultMap和一大段一大段的动态SQL条件。这套代码放在原生MyBatis环境下没毛病但项目一旦换成MyBatis-Plus问题就来了BaseMapper本身就带了单表CRUD和简单分页能力生成器却还在按老套路输出一堆冗余方法PageHelper的分页方式在MP里根本不生效你调了startPage()再走MP查询SQL里连limit的影子都见不着。我这次做的优化说白了就是把这套vm模板改掉让生成器直接输出MP风格的代码Mapper继承BaseMapperService继承IServiceServiceImpl继承ServiceImpl实体类去掉BaseEntity父类依赖分页统一走MP的Page对象。改完之后你导入一张表生成的代码能直接用不用每次生成完再手工改一遍和MyBatis-Plus相关的部分。这篇文章就是完整记录我当时改模板的思路、改法、踩过的坑适合正在做RuoYi MP整合的团队参考。1.2 默认生成代码中和MP冲突的细节逐个过一遍我把默认生成代码从头到尾审视了一遍真正影响MyBatis-Plus使用的不是一两个地方而是贯穿整个持久层骨架归纳起来有这几个第一个是实体类继承BaseEntity。RuoYi的BaseEntity里带着searchValue、createBy、createTime、updateBy、updateTime、remark这一串公共字段。表里如果有这些字段还好说没有的话生成出来的实体类凭空多出一大堆空属性MP在查询时虽然不会自动映射这些不存在的列但代码看起来很脏而且序列化之后前端会拿到一堆无意义的空字段。第二个是Mapper接口的方法列表。默认模板里每个Mapper都会生成insert、update、delete、selectById、selectByXxx这一整套方法在原生MyBatis里这是必须要写的因为XML要对应实现。可换成MP之后BaseMapper已经提供了差不多的方法全集新生成的接口完全可以直接 extends BaseMapper一个方法都不用声明。第三个是XML文件。默认的mapper.xml里会生成BaseResultMap和一系列SQL片段包括增删改查和分页查询的动态条件。到了MP体系里单表操作基本不需要XML条件拼接交给Wrapper就能解决。继续保留这一大坨XML维护成本高还容易和MP的注解、逻辑删除等能力互相打架。第四个是分页方式。RuoYi默认的Controller模板里是startPage(); List list xxxService.selectXxxList(xxx)这个startPage是PageHelper的ThreadLocal机制。MyBatis-Plus用的是PaginationInnerInterceptor Page对象两种分页是两套体系。如果项目引入MP但模板没改生成出来的Controller在运行时不会报错但分页日志和实际SQL完全对不上total永远是0或者干脆不分页。第五个是逻辑删除字段。默认生成代码里没有任何TableLogic注解MP的全局逻辑删除配置也没法自动生效导致删除操作还是物理删除。第六个是重复生成时的覆盖问题。这个严格来说不算代码风格冲突但很影响体验。同一张表如果调整过结构再生成一次默认模板会直接覆盖同名文件你之前手工加的查询方法全没了。1.3 定义优化边界不推翻RuoYi的返回结构改模板有个很大的诱惑就是恨不得把生成出来的东西全部按自己理想状态重写。我一开始也这么想后来翻了一下前端代码老老实实把边界收住了。RuoYi前端的表格组件、分页组件、导入导出功能硬性依赖后端返回的TableDataInfo结构也就是{code, msg, total, rows}这套格式。Controller里用getDataTable方法包装查询结果前端才能正常渲染。如果我把Controller模板改成只要返回MP的Page对象前端就得跟着改一堆东西那不是优化那是给自己挖坑。所以这次优化我坚持三个原则第一Controller的返回结构保持TableDataInfo不变前端一行代码都不用动第二权限注解、日志注解、Excel导入导出注解全部保留因为这些是RuoYi生态的红利丢掉可惜第三只动持久层相关代码的生成形态也就是实体类、Mapper接口、XML、Service接口、ServiceImpl以及Controller中的分页查询写法。这样改动范围可控验证成本也低。2. 准备阶段模板文件在哪、改之前先解决版本管理2.1 代码生成模块的模板目录与对应关系RuoYi的代码生成器集中在ruoyi-generator模块里模板文件路径一般是src/main/resources/vm/java和src/main/resources/vm/xml。初次打开这个目录时会发现一堆vm文件每个文件对应一类生成产物搞清楚映射关系再动手不然改着改着自己先迷路。核心的几个文件分别是domain.java.vm对应实体类mapper.java.vm对应Mapper接口service.java.vm对应Service接口serviceImpl.java.vm对应Service实现类controller.java.vm对应Controllermapper.xml.vm对应MyBatis的XML映射文件另外还有一个menu.sql.vm是用来生成菜单初始化SQL的。模板里那一堆${...}占位符就是Velocity的变量比如${ClassName}是转换后的类名${packageName}是包路径${tableName}是原始表名${columns}是字段列表。代码生成器读取表元数据之后把这些变量塞进VelocityContext再渲染模板。所以优化代码生成格式本质上就是改这些vm文件的骨架结构而不是去动Java代码里的生成逻辑。如果你只是改一两个文件的格式直接原地改就行。但如果像我这样整组模板大换血建议先建一个backup目录把原始模板完整复制一份存着。理由很简单模板是团队共享资产你改到一半发现方向不对回滚成本非常高。有备份在手至少能保证随时退回去。2.2 改模板前必须确认的几个生成参数改模板之前别急着动vim先把代码生成菜单里的几个配置参数确认一遍。这些参数会直接影响模板里${ClassName}和${packageName}这些变量的取值而且很多生成结果的诡异问题根本不是模板问题是配置没填对。第一个是表前缀。如果表名是t_order、t_user这种带前缀的表前缀这里必须填上t_生成器才会把前缀剔除得到Order、User这种干净的类名。很多项目刚接入时压根不填结果生成出来的类名是TOrder字段映射和前端Vue文件全跟着跑偏。第二个是生成包名、模块名和功能名。这三个值决定了生成代码的包路径、权限标识、请求路径前缀。比如模块名填system功能名填order最终Controller的PreAuthorize(ss.hasPermi(system:order:list))和RequestMapping(/system/order)就靠模板里的组合逻辑拼出来。模板代码里会写大量的占位符拼接这些配置不清晰改出来的模板也白搭。第三个是生成方式。代码生成器支持下载压缩包和直接生成到模块目录。我调试模板阶段建议先选下载到压缩包生成完在本地解压对比代码是否符合预期模板稳定之后再改回直接生成到项目里。这一步能省掉很多反复重启项目的时间。2.3 我的模板改版习惯复制一份再动手关于怎么组织模板的改版我个人的习惯跟改业务代码不太一样。业务代码有Git管理改错了随时revert模板这东西改起来没有编译期保护渲染出来的代码长什么样要生成之后才知道所以我一向是先用复制的方式隔离风险。具体做法是在ruoyi-generator模块里把vm目录完整复制一份命名成vm-mp然后把代码生成器配置里的模板路径指向vm-mp。这样原始模板和MP版模板并存切换成本极低。团队成员如果还不适应MP风格可以继续用老的等新模板验证没问题再把目录统一切换过来。改模板之前我会拿一张有代表性的表做“测试表”。这张表最好有十来个字段、一两个日期类型、一个decimal类型、一个主键覆盖常见的字段类型和注解场景。每改完一个vm文件就重新生成一次这张表的代码肉眼检查输出结果。如果所有类别的字段都能正确处理基本可以认为模板大方向没问题。3. 实操阶段把vm模板逐文件改成MP风格3.1 Mapper接口模板从一屏方法缩成一行extendsMapper接口的vm模板是最好改的一个文件。原始模板生成出来大概是这一大套Mapper注解加上selectXxxList、selectXxxById、insertXxx、updateXxx、deleteXxxById这一排方法声明。改成MP风格之后整个接口体就一句话。理论上生成的代码应该长这样Mapper public interface OrderMapper extends BaseMapperOrder { }对应的模板文件核心就是Mapper public interface ${className}Mapper extends BaseMapper${className} { }为什么敢把方法全部去掉很简单BaseMapper自带的selectById、selectBatchIds、insert、deleteById、updateById、selectList、selectCount、selectPage这些方法已经覆盖了单表CRUD的绝大多数场景。以后遇到真正的复杂查询直接在接口里手动加方法配合MP的注解SQL或者Wrapper都行没必要让生成器替我们预设那堆用不上的空方法。这里要插一个经验去掉方法列表之后原先生成代码里依赖的那些XML方法调用会全部失效。所以改Mapper模板必须和改XML模板同步进行否则生成的代码一运行就报Invalid bound statement。3.2 XML模板建议大家保留空壳而不是直接删掉很多从原生MyBatis切换到MP的团队喜欢把XML彻底干掉觉得BaseMapper加Wrapper天下无敌。这个想法理论上没错但实操阶段坑很多。RuoYi的代码生成器在生成时会固定产出XML文件如果项目里配置了MapperLocations扫描路径比如classpath*:mapper/**/*.xml当XML缺失时Spring Boot启动阶段虽然不一定报错但某些容器环境下日志会一直警告找不到某个Mapper对应的XML映射文件。更稳妥的做法是保留一个空壳XML只有mapper根标签和namespace里面不写任何SQL语句。?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespace${packageName}.mapper.${className}Mapper /mapper对应的vm模板只需要保留根标签和namespace把原来的resultMap、sql片段、CRUD语句全部剪切掉。这样生成出来的XML不会干扰MP运行后续如果确实遇到MP不好处理的复杂SQL也有个现成的落笔之处。我最初也想过一步到位直接把XML生成从模板里去掉后来发现很多同事已经习惯了打开XML写联表查询空壳方案更好接受。如果你所在的团队比较激进确认所有Mapper都不需要XML那删除生成也可以但要知道这和空壳方案只是取舍问题没有本质优劣。3.3 Service层和Controller模板分页逻辑是重头戏Service接口和ServiceImpl的模板改动很大因为原来的代码是基于“手写CRUD方法”设计的。原模板里Service接口会生成一长串方法声明ServiceImpl里则是每个方法配一套SQL调用中间还夹着startPage()这种PageHelper特有的分页逻辑。改成MP风格之后Service接口模板可以精简为public interface OrderService extends IServiceOrder { }ServiceImpl模板对应Service public class OrderServiceImpl extends ServiceImplOrderMapper, Order implements OrderService { }如果你能够接受把Controller模板也换掉那么Service层可以做到完全零自定义方法。IService里自带的save、saveBatch、removeById、getById、list、page配合LambdaQueryWrapper可以覆盖业务里九成需求。Controller的模板改造反而是整个优化里最值得花时间的地方。原模板的查询逻辑是依赖startPage()的startPage(); ListOrder list orderService.selectOrderList(order); return getDataTable(list);这个写法在MyBatis-Plus体系里跑不通因为startPage是PageHelper的静态方法它通过拦截器去改写后续的SQL跟MP的BaseMapper方法完全不联动。改完之后的Controller模板生成效果应该这样PageOrder page orderService.page(new Page(pageNum, pageSize), new LambdaQueryWrapper()); return getDataTable(page.getRecords(), page.getTotal());这里getDataTable需要支持传入List和total两个参数RuoYi的AjaxResult里已经有这个方法所以我们不需要改后端公共类只改Controller模板的调用方式。有一点必须说清楚Controller模板里生成的查询条件默认是一个空的LambdaQueryWrapper。为什么生成器没法自动生成like判断、eq判断因为它只会读表结构和字段名并不知道前端查询表单里有哪些筛选条件。我的选择是生成基础分页复杂的条件查询留给开发人员在生成后补几行Wrapper代码。这不算妥协而是把模板职责收敛到它该管的范围。3.4 实体类模板去掉BaseEntity加上MP注解实体类模板是改动最细碎的文件。默认的domain.java.vm模板会生成public class Order extends BaseEntity然后列出所有字段属性和一堆Excel注解日期字段上带着JsonFormat。改模板时要处理三件事去掉父类继承、加上MP注解、保留RuoYi自己的注解。去掉BaseEntity之后实体类不再自动拥有searchValue、createBy、createTime等公共字段。很多人在这一步会犹豫那审计字段怎么办我的建议是如果你确实需要这些字段就在业务表里把它们建出来MP的自动填充功能可以接管createTime、updateTime如果表里本来就没有这些列那就不该继承保持实体干净。实体类模板里需要加上三类MP注解类上的TableName指明数据库表名主键字段上的TableId标注主键策略其他字段按需加TableField列名和属性名不一致时用value属性纠正。模板片段大概长这样TableName(t_order) public class Order { TableId(value id, type IdType.AUTO) private Long id; }考虑到RuoYi代码生成器默认开启了驼峰映射其实很多字段不加TableField也能正常映射但显式标注的好处是列名变更时一眼能看出对应关系所以我生成的每一个属性上都带着TableField注解。Excel注解、JsonFormat注解这两个RuoYi的既有注解要原封不动保留前端的导入导出和日期格式化功能全指望它们。改模板的时候特别注意别手滑删掉这些注解否则生成出来的代码加了一堆“形式正确但实际残废”的坑。4. 效果验证同一张表改造前后生成代码对比4.1 新旧代码逐层对比为了直观看到变化我把同一张业务表分别在改模板前后生成了一遍得到的新旧代码放在一起对比。这里列出各层的差异你可以一眼看出改动覆盖了哪些层面。层级旧生成代码新生成代码ControllerstartPage selectXxxList getDataTable(list)orderService.page(new Page(...)) getDataTable(records, total)Service接口十几个自定义方法声明extends IServiceServiceImpl手写CRUD方法方法内部调用Mapperextends ServiceImplMapper接口Mapper 十几个方法声明Mapper extends BaseMapperXML文件resultMap 动态SQL CRUD语句空壳XML只有namespace实体类extends BaseEntity 无MP注解TableName TableId TableField权限注解PreAuthorizePreAuthorize保留Excel注解ExcelExcel保留对比完你会发现一个规律真正被干掉的是“和原生MyBatis绑定的那套样板代码”而RuoYi框架自身的特性比如权限控制、Excel导入导出、日志注解全部原封不动保留。这就是我反复强调的“收敛边界”在落地时的效果。4.2 拿测试表跑一遍完整流程模板改完之后我拿之前准备的那张测试表验证了整个流程。先在代码生成菜单导入表配置表前缀为t_模块名选system功能名按业务含义填好生成方式选下载压缩包。生成完成后解压里面的包结构和之前保持一致Entity、Mapper、Service、ServiceImpl、Controller、XML各就各位。把生成的代码复制到对应模块的源码目录启动项目打开菜单配置好路由点进列表页。这里我重点验证了三件事第一列表能正常加载total不等于0第二点击分页按钮翻页SQL日志里出现limit语句说明MP分页插件生效第三导出Excel功能正常Excel注解保留生效。实际跑下来有一个小意外第一次生成时我忘了把MapperLocations里mapper.xml的命名和项目里扫描路径对齐空壳XML没放对位置项目日志报了提示但代码能跑。原因是MP的BaseMapper不走XML映射那个警告只是扫描器的信息不影响功能。不过为了日志干净我最后还是按照原路径把空壳XML放了回去。4.3 生成后仍需要手工调整的少量场景改完模板千万不要产生一种幻觉生成的代码零修改直接上线。我把话说得保守点经过这轮优化生成代码的“可运行度”从原来的40%提升到大概90%剩下10%还是需要人工处理的。第一类是中等复杂的业务查询。模板只能根据表结构生成基础分页和主键查询如果你要按订单状态、创建时间区间筛选生成之后要在Controller里补一个LambdaQueryWrapper条件。第二类是多表关联查询。BaseMapper再强也解决不了join这类查询要么在Mapper接口里手动加方法要么回到空壳XML里写SQL。第三类是特殊类型字段。比如JSON类型字段、自定义枚举生成的模板无法预判业务语义需要在实体类上手动调整typeHandler。知道了剩余工作量在哪用这套模板就不会有心理落差。它的价值在于把重复劳动消灭掉而不是把思考本身也消灭掉。5. 常见问题与排查实录5.1 表前缀没配置生成了一堆带前缀的类名这是我在调试模板过程中反复撞见的问题。模板已经改成MP风格了但如果代码生成菜单里表前缀一栏是空的生成出来类名就特别奇怪。比如表名t_order类名会变成TOrder而不是Order。更迷惑的是如果表名本身带下划线分段比如t_bus_order类名直接整成TBusOrder看着像是模板写错了其实是前缀配置没设置。解决办法很简单在代码生成菜单中把表前缀填成t_同时确认模板里表名转类名的逻辑走的是RuoYi提供的驼峰转换工具。这样生成器在把tableName渲染成${ClassName}时才会正确剥离前缀。5.2 改完模板不生效输出还是老样子这个问题最坑因为它不会报错只是让你有一种“模板白改了”的错觉。我遇到过两种情况。第一种是改的是源码src下vm但运行时的ruoyi-generator模块已经被打成jar包并缓存到本地Maven仓库应用加载的是jar包里的模板。这种场景必须重新mvn clean package或者mvn install然后再启动项目。第二种是Velocity自身的模板缓存如果代码生成器模块开了缓存改完文件后进程不重启不会生效。排查技巧有一个直接在生成的压缩包里看文件名如果包里的Mapper还是老骨架基本可以断定当前运行的代码并没有加载新模板。先确认配置指向的模板目录在不在classpath里再确认有没有重新构建这两步能解决九成问题。5.3 分页查询没有limit查出来的却是全表数据模板改完后Controller确实生成了new Page(pageNum, pageSize)的代码调用也没报错但SQL日志里没有limit分页无效。这个问题的根源几乎不在模板而在MyBatis-Plus分页插件有没有真正注册到Spring容器里。RuoYi默认注入的是MyBatis原生配置如果你在自定义MybatisPlusConfig里只加了MybatisPlusInterceptor却没有加PaginationInnerInterceptor或者加了但数据库类型没配分页插件不会生效。我当时的配置里还遇到过一个隐藏点项目里同时存在PageHelper的自动配置它跟MP分页插件在同一个拦截器链里互相干扰抽掉PageHelper的依赖或者排除相关自动配置之后limit语句才正常出现。排查确认拦截器配置没问题之后再回头看模板生成的SQL基本都能定位到原因。这里要提醒一点MP分页插件要求使用分页时必须传入IPage对象不带Page参数的普通查询不会自动分页这跟PageHelper“碰见查询就改SQL”的行为是两个逻辑。5.4 逻辑删除字段、tinyint类型这样的元数据坑MP做逻辑删除需要实体字段上带TableLogic注解或者开启MP全局逻辑删除配置。我一开始图省事在application.yml里配置了全局logic-delete-field结果所有表的所有查询都被自动拼上deleted0条件那些根本没有deleted字段的表全部报错。后来回到最稳妥的方案生成模板时遇到字段名为deleted或is_deleted的列自动在实体类字段上生成TableLogic注解而不是靠全局配置一把梭。tinyint(1)字段也值得单独说。MySQL里tinyint(1)常常被MyBatis映射成Boolean如果你字段实际含义是状态码0/1/2生成的实体里变成Boolean就彻底乱套。这个问题可以通过JDBC连接串里加tinyInt1isBitfalse解决也可以在生成模板时对tinyint类型做一次特殊的类型映射。我更推荐后者因为模板判定更可控不至于影响其他SQL查询。5.5 重复生成把自定义代码覆盖了代码生成器默认输出路径和Java类名是一一对应的同一张表第二次生成时同名文件会被直接覆盖。这个问题在模板改造后依然存在一旦碰到就只能对着Git历史找补。我的经验是每次生成代码前在生成方式里选择“下载压缩包”在解压目录里和现有代码做一遍对比确认没有覆盖掉手工添加的方法再把新文件拷进项目。这个流程多花两分钟但比覆盖之后再修复要省太多事。从团队协作角度看我建议生成代码之后立刻提交一次Git把自定义修改和生成基线分离开后面任何人重新生成都不怕误伤。还有一个相关的小技巧复杂业务查询的SQL尽量写在XML或Service实现类中不要往Controller层塞。因为Controller是生成器每次都会输出的文件而Service实现类被修改的频率相对低一些这样能减少重复生成时的合并冲突面。最后聊点模板改造的心里话这套模板改完之后我在实际项目里连续跑了两轮新的业务表生成。第一轮发现小数位比较多的字段在实体类里没有匹配上合适的Java类型第二轮又发现存在联合主键的表生成主键注解时会重复加TableId这些问题都是靠实际业务反馈才逐步暴露出来的。所以如果你想照着我这套思路改造自己项目里的生成模板我建议做好一个心理准备模板不会一次成型它和业务代码一样需要持续修。每次用测试表跑通新模板之后把这种“先验证再铺开”的习惯固化下来比任何一次性的完美模板都更有价值。我这边后续还会继续打磨这套MP风格的模板让更多边缘情况在模板层面就处理掉而不是留给开发人员手工擦屁股。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑