资讯详情

MyBatis-Plus 3.0代码反向生成:AutoGenerator配置与避坑实战

📅 2026/10/17 16:34:11 | 华诺云谱 👁 阅读
MyBatis-Plus 3.0代码反向生成:AutoGenerator配置与避坑实战
简介这是一份 MyBatis-Plus 3.0 逆向工程实战案例面向希望借助代码生成器提高效率的 SpringBoot 与 MyBatis-Plus 开发者。资源基于 baomidou 3.0 演示代码自动生成通过生成器一键输出 Mapper 接口、XML 映射文件、Service 接口及实现类、Controller 层省去大量重复 CRUD 与持久层样板代码相比 2.3 版本3.0 优化了性能整合 JDK 1.8 新特性支持 Lambda、Lombok、主键生成策略与日期映射策略案例内已写好相应配置和示例可直接迁移到个人项目中。同时资源内置了标准 Maven 工程骨架src 主代码与测试代码分目录存放并结合对应配置展示了 3.0 的代码生成流程读者可直接用 IDE 打开运行对照学习。资源包共 28 个文件以 Java 源码、XML 映射、properties 配置和编译后的 class 文件为主同时带有 Maven 包装器与工程、类路径描述文件整体仅 66KB结构紧凑且目录规范便于快速定位生成器主类与配套测试代码。目前已有 572 人学习该资源适合正在从 SSM 转向 SpringBootMyBatis-Plus 的中级开发者或团队搭建代码生成基础设施时参考。1. 为什么 MyBatis-Plus 3.0 案例的核心不只是 CRUD而是代码反向生成接手一个二十多张表的课程设计后台最崩溃的不是写业务接口而是反复复制粘贴实体类、Mapper 和 Service。我用 MyBatis-Plus 3.0 搭过一套题库管理后台二十几张表全部交给 AutoGenerator 反向生成十几分钟产出整套持久层代码后续所有增删改查都走统一基类几乎没有再手写过重复的 CRUD。这套方案适合谁你正在做毕设或课设数据库表多想省掉手工建实体和映射文件的重复劳动或者你已经听过 baomidou3.0 的代码反向生成但不确定生成器该怎么配、生成结果怎么改、怎么验收。这篇会按交付顺序给出一份完整方案从环境选型、总体分层、生成器实现、自定义模板一直写到最容易翻车的地方和答辩验收方法可以直接参考落地。2. 总体架构设计生成器、基础层与业务层如何分工2.1 技术选型为什么锁定 MyBatis-Plus 3.0这个项目场景是前后端分离的题库管理系统后端用 Spring Boot 2.x数据库用 MySQL持久层选 MyBatis-Plus 3.0.x。选它不是因为“网上都说简单”而是从交付角度算过账。先看工作量题库系统里题目表、试卷表、试卷题目关联表、用户表、答题记录表加起来超过二十张每张表都需要实体类、Mapper 接口、Service、ServiceImpl 和 Controller。如果手写每张表至少有五个文件二十张表就是上百个文件而且结构高度重复。MyBatis-Plus 的 BaseMapper 已经把 insert、delete、update、selectById、selectPage 这些最常用方法内置了再配合代码反向生成重复文件可以由工具一次性产出。第二点是字段级基础设施。后台表的创建时间、更新时间、逻辑删除字段几乎是标配如果每张表的 Service 里都手写一遍填充逻辑量变会变成灾难。MyBatis-Plus 3.0 提供自动填充和逻辑删除能力生成阶段就把字段标记好业务里基本不用关心这些横切逻辑。第三点是维护成本。课程设计不是写完就结束导师可能中途改需求加一张表、改一个字段类型、给某个表增加一个索引。如果代码都是手写的改表结构要同步改实体、改 XML、改查询条件如果走了反向生成改完表之后重跑生成器增量扩散非常快。2.2 两个模块generator 与 runtime整体工程拆成两个 Maven 模块这是最容易让代码生成器“干净落地”的结构。第一个模块叫 generator只负责启动代码生成内部放 AutoGenerator 的入口类和模板文件。第二个模块叫 runtime是真正运行的项目生成的实体类、Mapper、Service、Controller 都落在这里。两个模块拆开运行时依赖里就不会混入 mybatis-plus-generator 和模板引擎避免把生成器这种东西带到生产环境。runtime 内部按包名分层entity与数据库表一一对应的实体类字段上有注解映射mapper继承 BaseMapper 的接口复杂查询通过自定义方法或 QueryWrapper 完成service 与 serviceImpl事务边界和业务规则放这里Controller 不直接碰 Mappercontroller接收请求参数校验后调用 Service统一返回 Result 结构。这个分层的核心原则是Controller 保持薄Service 承载业务Mapper 只做数据访问。生成的代码本身就符合这个结构后面手动加的代码也要延续这个习惯否则生成器第二次跑出来的文件和手工代码会互相打架。2.3 表结构设计对生成结果的影响反向生成的质量一半取决于数据库表规范度。我一般先给项目定几条建表约束再写生成器配置。表名和字段名统一小写下划线分隔例如 question_type、create_time每张表必须有主键 id建议 bigint审计字段统一叫 create_time、update_time软删除字段统一叫 delete_flag类型 tinyint默认值为 0字段必须有 COMMENT 注释生成实体时注释会变成 JavaDoc接口文档里能直接看到含义。下面是一张题目表的建表语句片段可以作为示例参照CREATE TABLE question ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, question_type tinyint NOT NULL COMMENT 题目类型1单选 2多选 3判断, content varchar(512) NOT NULL COMMENT 题干, difficulty tinyint DEFAULT 1 COMMENT 难度1简单 2中等 3困难, create_time datetime DEFAULT NULL COMMENT 创建时间, update_time datetime DEFAULT NULL COMMENT 更新时间, delete_flag tinyint DEFAULT 0 COMMENT 逻辑删除标记, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT题库表;注意这种约定下实体类不会自动带 JSR 303 校验注解比如 NotBlank、Size。生成器生成的字段映射是正确的但业务校验需要在 Service 层或 DTO 层补这个要在方案说明里写清楚避免答辩时被问“为什么实体类上没有参数校验”。3. 关键实现AutoGenerator 配置、模块化生成与自定义模板3.1 生成器依赖与入口类先处理依赖。runtime 模块只需要引入 startergenerator 模块额外引入生成器和模板引擎。父 POM 统一管理版本号避免 2.x 和 3.0 的类混在一起。!-- runtime 模块 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency !-- generator 模块 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version${freemarker.version}/version /dependencygenerator 模块里放一个入口类main 方法直接运行。MyBatis-Plus 3.0 的 AutoGenerator 配置分成四段全局配置、数据源配置、包名配置、策略配置。public class MyCodeGenerator { public static void main(String[] args) { // 1. 全局配置 GlobalConfig globalConfig new GlobalConfig(); String projectPath System.getProperty(user.dir); globalConfig.setOutputDir(projectPath /runtime/src/main/java); globalConfig.setAuthor(student); globalConfig.setOpen(false); globalConfig.setSwagger2(true); globalConfig.setBaseResultMap(true); globalConfig.setBaseColumnList(true); globalConfig.setFileOverride(false); // 2. 数据源配置 DataSourceConfig dataSourceConfig new DataSourceConfig(); dataSourceConfig.setUrl(jdbc:mysql://localhost:3306/exam_db? useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai); dataSourceConfig.setDriverName(com.mysql.cj.jdbc.Driver); dataSourceConfig.setUsername(root); dataSourceConfig.setPassword(root); // 3. 包名配置 PackageConfig packageConfig new PackageConfig(); packageConfig.setParent(com.example.exam); packageConfig.setModuleName(question); packageConfig.setEntity(entity); packageConfig.setMapper(mapper); packageConfig.setService(service); packageConfig.setServiceImpl(service.impl); packageConfig.setController(controller); // 4. 策略配置 StrategyConfig strategyConfig new StrategyConfig(); strategyConfig.setInclude(question, exam_paper, paper_question); strategyConfig.setNaming(NamingStrategy.underline_to_camel); strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel); strategyConfig.setEntityLombokModel(true); strategyConfig.setRestControllerStyle(true); strategyConfig.setControllerMappingHyphenStyle(true); strategyConfig.setLogicDeleteFieldName(delete_flag); strategyConfig.setVersionFieldName(version); // 5. 组装并执行 AutoGenerator generator new AutoGenerator(); generator.setGlobalConfig(globalConfig) .setDataSource(dataSourceConfig) .setPackageInfo(packageConfig) .setStrategy(strategyConfig) .setTemplateEngine(new FreemarkerTemplateEngine()); generator.execute(); } }配置说明setSwagger2(true) 会让实体类带上 Swagger 注解方便后续接口文档自动生成setBaseResultMap 和 setBaseColumnList 会在 XML 里生成基础 resultMap 和通用列对于复杂联表查询很有用。setFileOverride(false) 是血泪教训换来的默认值第一次生成可以打开之后重跑生成器时千万不要无脑覆盖否则手工加的业务方法会丢。3.2 生成结果的包结构与执行顺序执行 main 方法后代码会落在 runtime 模块里。以 exam_paper 为例runtime/src/main/java/com/example/exam/question/ ├── entity │ ├── ExamPaper.java │ └── ... ├── mapper │ ├── ExamPaperMapper.java │ └── ... ├── service │ ├── ExamPaperService.java │ └── ... └── service/impl ├── ExamPaperServiceImpl.java └── ...同时注意Mapper 接口上需要加 Mapper 注解或者在 runtime 启动类上加 MapperScan(com.example.exam.question.mapper)。这两种方式二选一如果忘了项目启动时会出现找不到 Mapper 的报错。生成后的实体类是 Lombok 风格字段上有 TableId、TableField 这类注解。比如逻辑删除字段 delete_flag 会生成 TableLogic自动填充字段会生成 TableField(fill FieldFill.INSERT) 或 INSERT_UPDATE具体取决于生成时是否开启了这部分配置。这里要补一段自动填充的实现否则 create_time 只是注解不会真的写入时间。通常的写法是实现 MetaObjectHandlerComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这段代码的作用是当调用 insert 时自动填充 create_time 和 update_time调用 update 时自动刷新 update_time。没有它生成实体上的 fill 注解形同虚设。区分点在于 insert 需要同时写两个时间update 只改更新时间逻辑不要反过来。3.3 自定义模板让 Controller 输出统一 Result默认模板生成的 Controller 是裸接口返回类型五花八门前后端联调时很不舒服。我的做法是自定义一套模板让所有 Controller 都返回 Result 包装类分页查询统一从 Page 对象读取。先在 generator/src/main/resources 下建立 templates 目录放一个 controller.java.ftl。片段如下package ${package.Controller}; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import ${package.Service}.${table.serviceName}; import ${package.Entity}.${entity}; RestController RequestMapping(/${table.entityPath}) public class ${table.controllerName} { Autowired private ${table.serviceName} ${table.serviceName?uncap_first}; GetMapping(/page) public ResultPage${entity} page(Page${entity} page) { return Result.success(${table.serviceName?uncap_first}.page(page)); } GetMapping(/{id}) public Result${entity} detail(PathVariable Long id) { return Result.success(${table.serviceName?uncap_first}.getById(id)); } }然后修改生成器装配部分把模板指向自定义文件TemplateConfig templateConfig new TemplateConfig(); templateConfig.setController(/templates/controller.java.ftl); generator.setTemplate(templateConfig);说明两点模板里的 ${table.serviceName}、${table.entityPath} 是生成器提供的变量不需要自己建上下文?uncap_first 是 FreeMarker 语法表示把服务名首字母小写。如果你把文件后缀改成 .vm就要换成 Velocity 引擎并调整变量写法。自定义模板最关键的收益不是少写几行代码而是让所有 Controller 的入参、出参、分页参数都一致这对答辩演示和后续扩展都非常有帮助。3.4 分页插件的必要性生成模板里用了 Page 对象但 MyBatis-Plus 3.0 的分页功能不是默认开启的需要手动加拦截器。很多人在这一步翻车接口返回了数据但是分页参数完全没生效所有数据都一次性查出来。这不是玄学是缺少分页插件。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意类名和包名。3.0 早期版本用的是 PaginationInterceptor后来演进为 MybatisPlusInterceptor PaginationInnerInterceptor 的组合。如果项目里两个版本混用可能会出现分页失效或方法找不到的问题。配置完成后调用 selectPage 时框架会自动生成 count 查询和 limit 语句翻页才真正生效。4. 避坑指南MyBatis-Plus 3.0 案例里最容易翻车的五个点4.1 生成器版本与运行版本不一致现象AutoGenerator 启动时直接报 NoSuchMethodError 或 ClassNotFoundException明明依赖都加了但找不到 GlobalConfig、PackageConfig 这些类。原因runtime 模块用的是 mybatis-plus-boot-starter 3.0.xgenerator 模块却引到了 2.x 的 mybatis-plus-generator两个版本的 API 差异很大类路径互相污染。解决在父 POM 中统一维护 mybatis-plus.version 变量generator 和 starter 都引用同一个版本。如果是从网上下载的模板项目先检查版本号再决定代码里写的是 setGlobalConfig 还是 GlobalConfig。这一条是优先要查的版本错位导致的报错最容易浪费时间。4.2 逻辑删除字段没生成 TableLogic现象数据库中 delete_flag 字段存在生成的实体类里它只是一个普通 Integer 字段。调用 removeById 后数据从表里消失而不是被标记为已删除。原因策略配置里没有指定逻辑删除字段生成器不知道哪个字段承担这个职责。解决在 StrategyConfig 中加一行 setLogicDeleteFieldName(delete_flag)生成后检查实体类deleteFlag 字段上应该出现 TableLogic 注解。软删除开启后selectList 查询会自动追加 delete_flag0 条件remove 会转换成 update 语句。4.3 主键策略与数据库自增不一致现象新增数据时如果表是 AUTO_INCREMENT但仍生成的是雪花 id插入后主键非常长且数据库里的自增列完全没用到反过来如果主键是手动分配又可能插入空值报错。原因MyBatis-Plus 3.0 默认主键策略是 ASSIGN_ID适合分布式场景但不是每个课设表都需要雪花 id。解决先在建表阶段决定主键策略。单机课设优先用 AUTO_INCREMENT在生成器全局配置或 yml 中设置 id-type: auto如果表之间有跨库或分表需求再保留 ASSIGN_ID。关键是所有表保持一致不要有的表自增、有的表雪花。4.4 重跑生成器把手动代码覆盖掉现象第一次生成后在 ServiceImpl 里写了业务方法后来调整了表结构重跑生成器发现 ServiceImpl 里手工写的方法全部消失。原因默认模板覆盖模式下生成器会把目标目录中同名文件直接替换。setFileOverride(true) 对首次生成有意义但对已经进入开发阶段的代码就是定时炸弹。解决生成器入口中设置 setFileOverride(false)第一次生成后立刻把代码提交到 Git。之后每次重跑生成器只关注 entity、mapper 的增量变化然后用 git diff 检查改动再决定 Service 层要不要手工合并。另一条防线是不要用自定义模板覆盖 ServiceImpl而是把业务方法写到单独的业务类里让 ServiceImpl 保持纯生成状态。4.5 分页失效接口返回全部数据现象调用 page 接口前端传了 current 和 size但返回列表是全表数据count 也不准。原因分页拦截器没注册或者注册了但配置类没有被 Spring 扫描到。解决按前文写一个 MybatisPlusConfig确认它在启动类可以扫描到的包路径下。序列是先加 Configuration再加 MybatisPlusInterceptor最后注入 PaginationInnerInterceptor。验证方式也很简单看运行日志里是否出现 LIMIT 关键字没有就说明拦截器没生效。5. 验收与答辩从生成结果到演示稿的一条龙检查5.1 验收清单先跑冒烟用例再截图课程设计验收看的是“能跑、敢演示、说得清”。我习惯把验收拆成三层顺序固定花不了多少时间。第一层跑生成器。准备好三张测试表执行入口类检查实体类是否生成、字段注释是否完整、Mapper 是否扫描成功。这一层不过后面不用看。第二层跑基础 CRUD。用 Swagger 或 Postman 分别测新增、列表、详情、修改、软删除五类接口。新增时看 create_time 是否自动写入软删除后查列表确认记录没有真的消失分页接口确认返回的 total 和 records 是合理的。第三层跑业务场景。以题库系统为例最典型的场景是组卷从题目表选指定数量和难度的题目插入试卷表再建立试卷和题目的关联。这层主要验证手动写的业务代码和生成的基础代码是否配合正常。可以用一张表把这套验收动作固化下来检查项操作方法通过标准生成器执行运行 main 方法无异常文件数量正确字段映射查看实体类注释与注解下划线字段全部转驼峰新增数据Postman 调用 POST 接口create_time 自动填充软删除调用 DELETE 接口后查库delete_flag 变为 1数据在分页查询调用页面接口SQL 中出现 LIMITtotal 正确组卷场景多表联合插入与查询关联数据完整事务回滚正常这套清单同样适用于其他课设打印管理、图书管理、订单系统只要把建表和业务场景换掉验收思路完全一致。答辩时导师最容易问的问题就是“代码是自动生成的你做了哪些东西”。这时候不要只回答“我配置了生成器”要强调三类手工工作一是自定义 Controller 模板统一返回结构和分页行为二是补齐了自动填充、逻辑删除等横切逻辑三是业务层的多表关联合成。把这三条讲清楚自动生成就不再是减分项反而说明你有工程化思维。另一个高频问题是“既然每张表都有 create_time、delete_flag为什么不提取成公共实体”。你可以回答生成器支持设置 superEntity 公共字段实际工程里确实可以抽但课设里把公共字段留在各表会让单表查询少一次 JOIN也便于导师直接看表结构。如果导师范式设计要求再改为联合主表的方式。从那以后我每次跑完生成器都会强制走一遍 git diff重点看 entity 和 mapper 两个目录确认没有意外覆盖、没有字段丢失然后才继续写业务代码。这个习惯帮我避免了至少三次因为覆盖导致的返工。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑