简介:这份压缩包面向正在使用 SpringBoot 与 MyBatis-Plus 3.0 的 Java 开发者、课程学习者以及需要快速搭建持久层脚手架的项目人员,重点解决 Mapper 接口、XML 映射、Service 接口与实现类、Controller 等基础代码重复编写的问题。内容以逆向工程自动生成代码案例为主线,结合 3.0 版本相较 2.3 版本在代码结构、性能优化、JDK 1.8 以上新特性、Lambda 表达式、Lombok、主键生成策略和日期映射策略等方面的变化,便于理解新版用法与 SpringBoot 整合方式。包内共 28 个文件,涵盖 java 源码、xml 配置与映射、properties 配置、class 编译文件以及 Maven、Eclipse 工程文件,压缩后约 66KB,目录紧凑清晰,适合导入 IDE 后对照分层结构与生成结果,并据此改造为自己的业务项目。已有 572 人学习下载,可用于参考代码生成器配置、分层组织方式和常见集成的排错思路,减少脚手架搭建与重复编码成本。
1. 一个真实的需求:为什么用 MyBatis-Plus 3.0 做代码反向生成,而不是手写
某公司后台管理系统二期要接入 40 多张业务表,三个人手写 Entity、Mapper、XML、Service、Controller,写了一周还没对齐;更麻烦的是表结构一改,三个地方要同步改,漏一处就在联调时出错。后来把 MyBatis-Plus 3.0 的代码反向生成引进来,从数据库表结构直接生成五层骨架代码,一天之内把 40 张表全部跑通,剩下的时间全部用来写业务逻辑。
这套方案解决的核心问题是「表结构到代码」的重复劳动,适合三类人:正在做毕设或课设、需要快速搭出一套能跑的后端骨架的学生;接手老项目、要批量补齐 CRUD 层的开发者;以及想给团队定一套统一代码规范的技术负责人。关键点在于,MyBatis-Plus 3.0 的生成器和 2.x 不是同一套东西,依赖、配置对象、模板体系全变了,照旧教程抄会直接出问题。
2. 版本与依赖对齐:3.0 的生成器为什么和 2.x 不是一回事
2.1 生成器从单体包里拆了出来
2.x 时代,生成器类AutoGenerator直接躺在mybatis-plus单体包里,引一个依赖什么都有。3.0 做了模块拆分,mybatis-plus-boot-starter只负责运行时增强,代码生成能力被挪到独立的mybatis-plus-generator模块。这意味着你只引 starter,编译时根本找不到AutoGenerator,IDE 会提示类不存在,很多人第一次上手就卡在这一步。
拆分的另一个后果是模板引擎变成显式依赖。3.0 的生成器本身不带模板实现,它只定义了一套模板接口,具体渲染交给 Velocity、Freemarker 或 Beetl 之一。默认走 Velocity,如果不引velocity-engine-core,运行时会抛模板初始化异常,报错信息指向 classpath 下找不到模板,容易误判成模板文件写错了。
通常的做法是三个依赖一起引:starter 管运行时、generator 管生成、velocity 管渲染,三者版本号保持一致。版本不一致是后面避坑章节要重点讲的问题,这里先记住一条:generator 的版本必须和 starter 的版本严格对齐,不要一个 3.0.7.1 一个 3.1.0。
2.2 依赖坐标与版本对照
把依赖关系摊开看会更清楚。下面这张表是 3.0 案例里最小可用的依赖集合,缺任何一个都跑不起来:
| 依赖 | groupId / artifactId | 作用 | 缺失后果 |
|---|---|---|---|
| 运行时增强 | com.baomidou / mybatis-plus-boot-starter | 提供 BaseMapper、条件构造器、插件 | 生成的 Mapper 无法继承 BaseMapper |
| 代码生成器 | com.baomidou / mybatis-plus-generator | 提供 AutoGenerator 与配置对象 | 编译期找不到 AutoGenerator |
| 模板引擎 | org.apache.velocity / velocity-engine-core | 渲染 .vm 模板 | 运行期模板初始化失败 |
| 数据库驱动 | mysql / mysql-connector-java | 读取表结构元数据 | 连不上库,读不到 information_schema |
对应的 Maven 片段如下,版本统一用属性管理,避免三处各写一个版本号:
<properties> <mybatis-plus.version>3.0.7.1</mybatis-plus.version> </properties> <dependencies> <!-- 运行时:BaseMapper、条件构造器、分页插件都来自这里 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- 生成器:3.0 起独立成模块,必须单独引入 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- 模板引擎:生成器不自带,默认用 Velocity --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.0</version> </dependency> <!-- 驱动:生成器靠它读 information_schema --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> <scope>runtime</scope> </dependency> </dependencies>这里有个容易忽略的点:generator 依赖只在开发期用,理论上可以设成provided或放进 profile,但很多毕设项目图省事直接打进运行时包,问题不大,只是会让最终 jar 体积变大。我一般的做法是单独建一个generator子模块或者一个独立的CodeGenerator类放在test目录下,生成完就不参与打包,干净一些。
2.3 配置骨架:六件套的职责边界
3.0 的AutoGenerator通过六个配置对象组装,理解每个对象管什么,后面改配置才不会到处翻文档:
GlobalConfig:输出目录、作者、是否覆盖同名文件、主键策略、是否生成 ResultMap。DataSourceConfig:JDBC URL、驱动类名、账号密码。PackageConfig:父包名、模块名、各层子包名、XML 输出位置。StrategyConfig:表名过滤、表前缀、命名策略、逻辑删除字段、乐观锁字段、Lombok 开关。TemplateConfig:指定自定义模板路径,或把某一类文件设为 null 表示不生成。InjectionConfig:往模板里注入自定义变量,做二次扩展时用。
这个划分的好处是职责清晰:想改包结构只动PackageConfig,想改生成哪些表只动StrategyConfig,不用在几十行配置里大海捞针。后面第 4 章的完整代码就是按这六块组织的,可以对照着看。
3. 总体设计:四类生成物与一套可复用的配置骨架
3.1 分层与模块职责划分
生成器产出的是标准三层结构,每一层的职责要在动手前定清楚,否则生成出来的代码会互相打架:
- entity:和表一一对应的 POJO,只放字段、注解、Lombok 注解,不写业务方法。这一层是最纯粹的映射,改表结构时只需要重新生成它。
- mapper:接口层,继承
BaseMapper<T>,单表 CRUD 全部继承而来,只在有复杂 SQL 时补自定义方法。 - xml:
mapper接口对应的 SQL 映射文件,存放多表关联、动态条件这类 BaseMapper 覆盖不到的场景。 - service / serviceImpl:业务层,
IService<T>和ServiceImpl<Mapper, Entity>提供批量保存、链式查询等能力,业务逻辑写在这里。 - controller:暴露 HTTP 接口,
RestController风格,只做参数接收、调用 service、返回结果三件事。
这种划分的价值在于「生成的部分」和「手写的部分」有明确边界。entity、mapper、xml 这三层基本可以全量重新生成,service 和 controller 生成一次之后就以手写为主,重新生成时用文件覆盖开关控制,避免把写好的业务逻辑冲掉。
3.2 生成物清单与目录约定
以一张订单表t_order为例,生成器跑一遍会产出六个文件,落在下面的位置:
| 生成物 | 包路径 | 文件示例 | 是否建议重复生成 |
|---|---|---|---|
| 实体类 | com.example.demo.order.entity | Order.java | 是 |
| Mapper 接口 | com.example.demo.order.mapper | OrderMapper.java | 是 |
| Mapper XML | resources/mapper/order | OrderMapper.xml | 视情况 |
| Service 接口 | com.example.demo.order.service | IOrderService.java | 否 |
| Service 实现 | com.example.demo.order.service.impl | OrderServiceImpl.java | 否 |
| Controller | com.example.demo.order.controller | OrderController.java | 否 |
目录约定要在PackageConfig里一次性定好。注意setModuleName("order")会在父包和子包之间插一层模块名,最终包名变成com.example.demo.order.entity,XML 的路径也会跟着带上模块名。如果项目本来就是按业务模块分包的,这一层很合适;如果是传统三层分包,把moduleName留空即可。
3.3 命名策略与表前缀处理
命名策略是StrategyConfig里最值得提前定的参数。3.0 提供NamingStrategy.underline_to_camel,把order_no转成orderNo、t_order转成Order。但表名带前缀时,只配命名策略是不够的,实体类会变成TOrder,很难看。
正确的做法是同时配setTablePrefix("t_"),生成器会先把前缀剥掉再转驼峰,得到干净的Order。如果项目里前缀不统一,比如既有t_又有biz_,可以用数组形式传多个前缀,或者干脆用setInclude明确列出要生成的表,避免误伤。
字段命名同理,setColumnNaming(NamingStrategy.underline_to_camel)保证create_time映射成createTime。这两个参数不配的后果是生成一堆带下划线的属性名,后面写 Lambda 条件构造器时全是Order::getOrder_no这种反常规写法,团队评审通不过。
4. 核心实现:从表结构到 Controller 的完整生成链路
4.1 生成器主程序:六件套组装的完整代码
下面这段是 3.0 版本可以直接跑起来的主程序,按六件套顺序组装,每一块都加了注释说明改哪里:
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; public class CodeGenerator { public static void main(String[] args) { // 1. 全局配置:输出目录、作者、覆盖策略 GlobalConfig globalConfig = new GlobalConfig() // 输出到当前工程的 src/main/java,多模块项目改成具体模块路径 .setOutputDir(System.getProperty("user.dir") + "/src/main/java") .setAuthor("code-gen") .setOpen(false) // 生成后不自动打开目录,服务器上跑也不会报错 .setFileOverride(true) // 覆盖同名文件,配合版本控制使用 .setIdType(IdType.AUTO) // 主键策略,自增表用 AUTO,雪花 ID 换成 ASSIGN_ID .setBaseResultMap(true) // 生成 resultMap,复杂映射时省事 .setBaseColumnList(true) // 生成基础列清单,XML 里复用 .setActiveRecord(false) // 不用 ActiveRecord 模式就关掉 .setEnableCache(false); // 二级缓存默认关,避免踩坑 // 2. 数据源配置:生成器靠它读表结构,不是运行时数据源 DataSourceConfig dataSourceConfig = new DataSourceConfig() .setUrl("jdbc:mysql://127.0.0.1:3306/demo" + "?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC") .setDriverName("com.mysql.cj.jdbc.Driver") .setUsername("root") .setPassword("root"); // 3. 包配置:决定生成物落在哪个包、XML 落在哪个目录 PackageConfig packageConfig = new PackageConfig() .setParent("com.example.demo") .setModuleName("order") // 按业务模块分包,传统三层分包就留空 .setEntity("entity") .setMapper("mapper") .setService("service") .setServiceImpl("service.impl") .setController("controller") .setXml("mapper"); // XML 包名,实际落盘位置由 mapper-locations 决定 // 4. 策略配置:生成哪些表、怎么命名、带哪些增强注解 StrategyConfig strategyConfig = new StrategyConfig() .setCapitalMode(true) // 类名首字母大写 .setEntityLombokModel(true) // 实体加 @Data .setRestControllerStyle(true) // 控制层用 @RestController .setNaming(NamingStrategy.underline_to_camel) // 表名转驼峰 .setColumnNaming(NamingStrategy.underline_to_camel)// 字段转驼峰 .setInclude("t_order", "t_order_item") // 只生成这两张表 .setTablePrefix("t_") // 剥掉表前缀再转驼峰 .setLogicDeleteFieldName("is_deleted") // 逻辑删除字段 .setVersionFieldName("version") // 乐观锁字段 .setEntityTableFieldAnnotationEnable(true) // 实体加 @TableName/@TableField .setControllerMappingHyphenStyle(true); // URL 用连字符风格 // 5. 模板配置:只覆盖实体和控制层,其余用默认模板 TemplateConfig templateConfig = new TemplateConfig() .setEntity("/templates/entity.java") // classpath 下的自定义模板 .setController("/templates/controller.java") .setXml(null); // XML 已手工维护,不再生成 // 6. 组装并执行 new AutoGenerator() .setGlobalConfig(globalConfig) .setDataSource(dataSourceConfig) .setPackageInfo(packageConfig) .setStrategy(strategyConfig) .setTemplate(templateConfig) .execute(); } }几个参数需要展开说。setOutputDir在多模块项目里必须指向具体模块的src/main/java,指向根目录会把代码生成到工程外面。setIdType要和表结构一致,表里是auto_increment就用AUTO,用雪花 ID 就换成ASSIGN_ID,配错了插入数据时主键为 null,报错信息指向 SQL 而不是配置,很难查。
setInclude和setTablePrefix是一对组合拳。只写include不写tablePrefix,生成的类名带前缀;只写tablePrefix不写include,会把库里所有表全生成一遍,包括那些中间表和日志表。两个都写才是可控的。
setXml(null)表示不生成 XML。这个开关在「XML 由 DBA 统一维护」或者「全用注解写 SQL」的项目里很有用,避免生成器把手写的复杂 SQL 覆盖掉。反过来,如果要生成 XML,就传一个模板路径,默认是/templates/mapper.xml。
4.2 策略配置:逻辑删除、乐观锁与自动填充
生成器只负责把注解写进实体,真正让这些注解生效的是运行时的三处配置,缺一处功能就是哑的。
逻辑删除需要在配置文件里声明删除值和未删除值,否则@TableLogic注解不会生效:
mybatis-plus: global-config: db-config: logic-delete-value: 1 # 已删除标记 logic-not-delete-value: 0 # 未删除标记 logic-delete-field: isDeleted # 3.0 支持全局指定,实体上就不必逐个加注解 mapper-locations: classpath*:/mapper/**/*.xml乐观锁需要注册插件,version字段在更新时会自动带上版本条件并自增:
@Configuration public class MybatisPlusConfig { /** 乐观锁插件:配合实体上的 @Version 字段使用 */ @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } /** 分页插件:不注册的话 Page 参数不会生效,返回全量数据 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } }自动填充要单独实现MetaObjectHandler,生成器只在实体字段上标@TableField(fill = FieldFill.INSERT),填充逻辑得自己写:
@Component public class FillHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 插入时同时填充创建时间和更新时间 this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("updateTime", new Date(), metaObject); } @Override public void updateFill(MetaObject metaObject) { // 更新时只刷新更新时间 this.setFieldValByName("updateTime", new Date(), metaObject); } }这三处是生成器管不到的地方,也是验收时最容易被问到的点。逻辑删除不配全局值,删除操作会变成物理删除;分页插件不注册,列表接口永远返回全表数据;填充处理器不实现,create_time一直是 null。生成的是骨架,这些是让骨架动起来的关节。
4.3 自定义模板与字段类型映射
默认模板能覆盖 80% 的场景,剩下 20% 通常集中在这几件事上:统一加 Swagger 注解、统一加参数校验、把Date换成LocalDateTime、给 Controller 套一层统一返回体。这些都要改模板。
模板放在src/main/resources/templates/下,命名保持和默认一致(entity.java.vm、controller.java.vm),TemplateConfig里传不含.vm后缀的路径即可。改模板时只动需要改的行,不要整份重写,否则升级生成器版本时对不齐。
字段类型映射是另一个高频改动点。3.0 默认把datetime映射成java.util.Date,把tinyint(1)映射成Integer而不是Boolean。要改这两条,需要在StrategyConfig里通过setColumnNaming之外的类型转换配置处理,或者在模板里做判断。我一般的做法是不改生成器内部映射,而是在实体模板里统一加一段类型转换逻辑,把明确表示布尔语义的字段单独列出来,生成完再人工确认一遍。
这里给一个朴素但有效的判断标准:凡是表字段以is_开头、类型是tinyint(1)、注释里写着「是否」的,一律当布尔处理;其余tinyint保留整型。这条规则写进模板注释里,后来人一看就懂,比在文档里写三大段说明管用。
4.4 多模块与批量生成
单模块项目跑上面的主程序就够了,多模块项目要改两个地方:outputDir指向目标模块,PackageConfig的父包名带上模块专属前缀。如果一次要给多个模块生成,把六件套封装成一个方法,表名和模块名当参数传进去:
/** 按模块批量生成,tables 为该模块下的表名列表 */ private static void generate(String moduleName, String... tables) { GlobalConfig globalConfig = new GlobalConfig() .setOutputDir(System.getProperty("user.dir") + "/" + moduleName + "/src/main/java") .setAuthor("code-gen") .setFileOverride(true) .setIdType(IdType.AUTO); PackageConfig packageConfig = new PackageConfig() .setParent("com.example.demo") .setModuleName(moduleName) .setXml("mapper"); StrategyConfig strategyConfig = new StrategyConfig() .setEntityLombokModel(true) .setRestControllerStyle(true) .setNaming(NamingStrategy.underline_to_camel) .setColumnNaming(NamingStrategy.underline_to_camel) .setInclude(tables) .setTablePrefix("t_") .setLogicDeleteFieldName("is_deleted"); new AutoGenerator() .setGlobalConfig(globalConfig) .setDataSource(dataSourceConfig()) // 数据源抽成独立方法复用 .setPackageInfo(packageConfig) .setStrategy(strategyConfig) .execute(); } public static void main(String[] args) { generate("order", "t_order", "t_order_item"); generate("user", "t_user", "t_user_role"); }批量生成时最容易出的问题是setFileOverride(true)把已有业务代码覆盖掉。稳妥的做法是生成前先提交一次代码或建一个临时分支,生成后git diff逐个文件确认,只保留需要更新的部分。这一步多花五分钟,能省掉半天找回代码的时间。
5. 避坑与排查:反向生成最容易翻车的五个现场
5.1 报 NoSuchMethodError 或 ClassNotFoundException
现象:主程序一跑就抛NoSuchMethodError,指向AutoGenerator.setGlobalConfig之类的方法;或者直接ClassNotFoundException: com.baomidou.mybatisplus.generator.AutoGenerator。
原因:generator 依赖没引,或者 generator 和 starter 版本不一致。3.0 内部方法签名在 3.0.x 和 3.1.x 之间有调整,混用会出现方法找不到。
解决:把两个依赖的版本号统一抽成 Maven 属性,只写一处。引了依赖还报类找不到,检查是不是被provided作用域挡在了运行期之外,或者本地仓库里的包下载不完整,删掉对应目录重新拉一次。
5.2 模板初始化失败,提示找不到 .vm 文件
现象:Cannot find template location或 Velocity 初始化异常,堆栈指向模板引擎。
原因:三种可能——没引velocity-engine-core;自定义模板路径写错(TemplateConfig里传的路径不需要.vm后缀,引擎会自动补);模板文件没放在resources/templates下,classpath 扫不到。
解决:先确认模板引擎依赖在;再确认传的路径是/templates/entity.java这种以斜杠开头的 classpath 绝对路径;最后检查打包后target/classes/templates/下是否真的有文件。三步走完基本能定位。
5.3 生成的 XML 打包后丢失
现象:本地跑得好好的,打成 jar 部署后报Invalid bound statement (not found)。
原因:XML 生成到了src/main/java下的包目录里。Maven 默认只把src/main/java里的.java编进 classpath,.xml会被忽略。
解决:两种方式二选一。要么在pom.xml里给src/main/java加 resources 配置把**/*.xml包含进去,要么把 XML 的输出目录改到src/main/resources/mapper下。后者更规范,配合mapper-locations: classpath*:/mapper/**/*.xml使用。
5.4 实体类名带了表前缀
现象:生成出来的类叫TOrder,Lambda 写法变成TOrder::getOrderNo,很别扭。
原因:只配了NamingStrategy.underline_to_camel,没配setTablePrefix。命名策略只负责下划线转驼峰,不负责剥前缀。
解决:在StrategyConfig里补setTablePrefix("t_")。如果前缀有多种,传数组;如果前缀规则混乱,直接改用setInclude精确列出表名,放弃前缀配置。
5.5 重新生成冲掉了手写业务代码
现象:第二次跑生成器,发现ServiceImpl里写了半天的业务方法没了。
原因:setFileOverride(true)会无差别覆盖同名文件,而ServiceImpl和Controller恰恰是手写改动最多的地方。
解决:给这两类文件单独关掉覆盖,或者干脆在TemplateConfig里把serviceImpl和controller设为 null,只在第一次生成时临时打开。我习惯的做法是生成前先建分支,生成后逐个文件对比,只合并 entity 和 mapper 的变更。
6. 验收与答辩:用三张对照表证明生成结果真的能进项目
验收这套反向生成方案,不要只看「文件生成出来了」,那只是及格线。要证明它能进真实项目,我一般会准备三张对照表,答辩时直接摆出来。
第一张是表结构到实体的字段对照表。挑一张字段最多的表,逐行列出「数据库字段名 / 数据库类型 / 实体属性名 / 实体类型 / 注解」四列,重点看三处:下划线转驼峰是否正确、is_开头的布尔字段是否映射成Boolean、逻辑删除和乐观锁字段的注解有没有加上。这张表能证明生成器读懂了表结构,而不是机械套模板。
第二张是接口能力对照表,验证生成出来的代码实际能跑:
| 验收项 | 操作 | 预期结果 |
|---|---|---|
| 单表新增 | 调 POST 接口 | 主键自增正常,create_time 自动填充 |
| 分页查询 | 传 page 和 size | 返回总数正确,SQL 带 LIMIT |
| 逻辑删除 | 调 DELETE 接口 | 数据仍在,is_deleted 变为 1 |
| 乐观锁 | 两个请求并发更新同一行 | 后者返回更新失败,version 递增 |
| 条件构造 | 用 LambdaQueryWrapper 拼条件 | 生成的 SQL 字段名正确 |
第三张是重复生成幂等性检查。把生成器连续跑两遍,对比两遍产出的文件差异。理想结果是 entity、mapper、xml 三类文件完全一致,service 和 controller 因为手写过会有差异。如果有意外差异,说明模板里有随机因素或者时间戳,这种问题上线后才暴露就很被动。
答辩环节常被问的一个问题是「为什么不直接用现成的脚手架」。我的回答通常落在可维护性上:脚手架是别人按通用场景定死的,表前缀、包结构、注解风格都不一定匹配;自己掌握这套六件套配置,改一个参数就能适配新项目,成本比改脚手架低得多。
说个血泪经验。我最早做这类生成器时,图快直接在主干上跑,fileOverride开着,跑完发现前一天写的三个业务方法没了,翻了半天 git 记录才找回来。从那以后我每次跑生成器之前都强制走一遍流程:先提交当前代码,再建临时分支,生成完用git diff逐个文件确认,确认无误才合回主干。这个习惯看起来慢,实际上省下来的返工时间远超那几分钟。希望这套结构和验收方法,能帮到正在做毕设或要交付方案的你。
本文还有配套的精品资源,点击获取