简介:一套基于MyBatis-Plus 3.0的代码生成器完整案例,面向熟悉SpringBoot、希望提升数据层开发效率的Java工程师。案例演示了逆向工程的典型用法:通过代码生成器自动产出Mapper接口、XML映射文件、Service接口及实现、Controller,并与SpringBoot完成项目整合;3.0版本相比2.3优化了性能和代码结构,支持JDK8以上新特性、Lambda表达式、Lombok,以及主键生成和日期映射策略,能有效减少重复建模与手写CRUD代码。资源包共28个文件,大小仅66KB,以Java源码、XML配置、Properties属性文件为主,同时包含class编译产物、Maven包装器mvnw、IDE工程描述文件与Git忽略配置,目录划分清晰,便于导入IDE后对照研究生成流程。已有572人学习下载,适合需要掌握MyBatis-Plus逆向工程参数配置、代码模板结构,以及SpringBoot整合思路、快速搭建数据访问层的开发者,可直接复用其中的生成策略与工程组织方式。
1. 项目定位:一份能直接跑起来的 MyBatis-Plus 3.0 全量案例
我最早用 MyBatis 写单表 CRUD,一张表一个 Mapper XML,里面全是复制粘贴的 selectById、insert、update,字段一多改起来就是半宿。后来我把订单模块翻成 MyBatis-Plus 3.0,BaseMapper 把 insert、update、selectList、page 全内置了,单表操作一行 SQL 都不用自己写;再配合代码反向生成,把 entity、mapper、service、controller 从数据库表一次扫出来,半小时搭好模块骨架。这套「mybatis-plus 3.0 案例」就是能直接照跑的完整工程:Spring Boot + MyBatis-Plus 3.x + MySQL,覆盖条件构造器、分页、逻辑删除、自动填充、乐观锁和代码生成器。适合两类人:还在手写 Mapper XML 想换姿势的,以及项目要迁到 3.0、缺完整案例打底的人。
2. 工程骨架搭建:依赖、目录结构与配置一次配齐
2.1 依赖引入:启动器、生成器、模板引擎三个坐标
Spring Boot 项目接入 MyBatis-Plus 3.x 不需要像老 MyBatis 那样手动拼 mybatis 和 mybatis-spring 两个包,官方启动器已经帮你做好了版本整合。我本地验证用的是 3.5.3.1,这段 pom 直接照抄就行,坐标com.baomidou(苞米豆社区)从 3.0 至今没变过。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency>这三个坐标缺一不可:mybatis-plus-boot-starter负责运行时能力,也就是 BaseMapper、IService、分页插件这些核心功能;mybatis-plus-generator是代码反向生成器,单独拆出来的模块,不会污染主工程的 classpath;freemarker 是生成器的模板引擎,3.0 时代默认用的是 Velocity,3.5 之后默认切到了 Freemarker,不显式声明会在执行生成器时直接抛模板引擎缺失的异常。再加一个spring-boot-starter-web和mysql-connector-java就行,这两个太常规我就不贴了。
提示:如果你手头是 3.0.x 的老工程,
mybatis-plus-boot-starter的版本号直接换成 3.0.7.1 即可,业务代码基本不用动,唯一要改的是分页插件配置,具体差别在第 3 章会讲到。
2.2 目录结构:controller / service / mapper 四层怎么摆
生成器默认会按四层结构出代码,所以工程目录从一开始就按这个约定摆,后面跑生成器才不会到处乱飞:
src/main/java/com/example/demo/ ├── DemoApplication.java ├── config/ │ └── MybatisPlusConfig.java ├── controller/ │ └── UserController.java ├── entity/ │ └── User.java ├── mapper/ │ └── UserMapper.java ├── service/ │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java ├── handler/ │ └── MyMetaObjectHandler.java └── generator/ └── CodeGenerator.java src/main/resources/ ├── application.yml └── mapper/entity 放数据库表映射,mapper 只写接口和自定义 SQL,service 接口接在 IService 上,service/impl 里的实现类继承 ServiceImpl,controller 只做参数接收和结果返回。handler 目录放自动填充处理器,generator 目录放代码生成器的 main 方法,这两个是我手动加的,生成器默认不创建。
启动类上要加@MapperScan,否则 mapper 接口不会被注册成 MyBatis 的代理 Bean,这是新手翻车率最高的地方:
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }@MapperScan的包路径必须和 mapper 接口所在包一致。如果你在 2.1 里改了 groupId 或者包名,这里要同步改。还有一种替代方案是给每个 Mapper 加@Mapper注解,但项目一多接口一多,注解方式又散又容易漏,我一般只在启动类上用@MapperScan一把梭。
2.3 application.yml:驼峰映射、逻辑删除、ID 策略都在这
MyBatis-Plus 的配置项不多,但每一项都直接影响行为,尤其是逻辑删除和 ID 策略这种改了全局就全变的。这份案例用的配置如下:
spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case必须开,数据库列名create_time才能自动映射到实体的createTime,不开的话要么全用下划线命名,要么每个字段手写@TableField,那还不如回到手写 XML 的时代。log-impl配成 StdOutImpl 是为了开发期在控制台直接看 SQL 和参数,上线前记得删掉,否则日志会刷屏。id-type: assign_id是 3.0 之后的默认主键策略,生成雪花 ID,而不是数据库自增,这点后面讲前端精度丢失的坑时还要再提。logic-delete-field是全局逻辑删除字段声明,只要实体里有对应的deleted属性,所有 select 会自动追加deleted=0条件,delete 语句会自动变成 update。
注意:
logic-delete-value默认就是 1,logic-not-delete-value默认是 0,日常可以不写。但如果团队习惯用 2 表示已删除,或者用 0/1 之外的值,就一定要显式配,不然框架会按默认值 1/0 去拼 SQL,删了等于没删。
3. 核心 CRUD 链路拆解:从实体映射到 Wrapper 再到分页
3.1 实体与 Mapper:三行代码拿到全表 CRUD
先看实体,这是整个链路的起点。数据库表sys_user映射成User,类的每一处注解都有它存在的理由:
@TableName("sys_user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; @TableField("email") private String mail; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableLogic private Integer deleted; }@TableName必须写,尤其在代码反向生成场景下,类名和表名有sys_前缀差异,不指定表名框架就按user去查,直接报表不存在。@TableId声明主键,IdType.ASSIGN_ID对应雪花算法,和全局配置的id-type: assign_id是同一个东西,实体上的注解优先级更高。@TableField("email")用在属性名和列名对不上的情况,实体里是mail,表里是email,不注解就会映射成mail列。fill = FieldFill.INSERT配合 MetaObjectHandler 做插入自动填充,createTime这种字段谁都不想在每条业务代码里手动 set。@TableLogic标注逻辑删除字段,全局配置里已经声明了logic-delete-field: deleted,这里再标一次属于双保险,生成器也会自动带上这个注解。
Mapper 接口则简单到不能再简单:
public interface UserMapper extends BaseMapper<User> { }继承BaseMapper<User>之后,insert、deleteById、updateById、selectById、selectList、selectCount、selectPage这些方法全部内置,泛型参数就是实体类型。它的底层原理是 MyBatis 的 MapperProxy 代理,在运行时根据方法名和泛型动态生成 SQL,所以不用写 XML。但注意,BaseMapper只覆盖单表操作,多表 join 或者复杂子查询仍然要自己在接口里加方法、写 XML,这是 MyBatis-Plus 和 JPA 的边界,别指望它帮你解决一切。
3.2 条件构造器:QueryWrapper 与 LambdaQueryWrapper 怎么选
条件构造器是 MyBatis-Plus 最常用的 API。QueryWrapper用字符串传列名,LambdaQueryWrapper用方法引用传字段,两者的差别在写错时才能体现:
// 反例:列名靠手写,编译期完全查不出来 QueryWrapper<User> qw = new QueryWrapper<>(); qw.eq("name", "张三"); // 正例:方法引用,字段名改了一处,编译直接报错 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>(); lqw.eq(User::getName, "张三") .ge(User::getAge, 18) .orderByDesc(User::getCreateTime); List<User> list = userMapper.selectList(lqw);eq是等于,ge是大于等于,orderByDesc是倒序,这些都是内部拼进 WHERE 条件的片段。QueryWrapper的优势是写动态表名场景更灵活,比如列名是从配置中心读出来的,但代价是列名拼错只在运行期报Unknown column,这个坑后面专章讲。LambdaQueryWrapper通过User::getName拿到的实际是 Lambda 解析出来的属性名,不走字符串,字段重命名时有编译器兜底,我这边新人我都不让用QueryWrapper写业务。
Service 层的链式写法更短,实际项目里用这种最多:
List<User> list = userService.lambdaQuery() .eq(User::getName, "张三") .ge(User::getAge, 18) .orderByDesc(User::getCreateTime) .list();lambdaQuery()返回一个 LambdaQueryChainWrapper,支持链式调用后直接list()、one()、count()、page()收尾。条件更新也有对应形态:
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.set(User::getAge, 30) .eq(User::getName, "张三"); userMapper.update(null, updateWrapper);这里第一个参数传null表示实体对象不参与更新,字段全部由set指定。如果你既传实体又传 wrapper,两边字段会叠加,一旦同名字段值不一致,结果会非常玄学,所以用 wrapper 更新时第一个参数固定传null是我给自己定的死规矩。
3.3 分页插件与乐观锁:3.0 与 3.4 两代配置都给出
分页在 MyBatis-Plus 里不是自动开启的,不注册分页插件的话,selectPage会返回全表数据然后内存里截断,这是最典型的假分页翻车现场。3.0 系列的分页插件配置有两代写法,老工程和新工程各用各的:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); OptimisticLockerInnerInterceptor optimistic = new OptimisticLockerInnerInterceptor(); interceptor.addInnerInterceptor(optimistic); return interceptor; } }MybatisPlusInterceptor是 3.4.0 之后的核心入口,它是一个拦截器链,分页、乐观锁、防全表更新、多租户都往链上挂。PaginationInnerInterceptor(DbType.MYSQL)需要显式指定数据库类型,它内部根据方言生成LIMIT语句;setMaxLimit(500L)是单页最大行数,防止有人page(new Page<>(1, 1000000))把数据库拖垮。OptimisticLockerInnerInterceptor是乐观锁拦截器,配合实体的@Version字段使用。
两代配置的差别我做了一张对照表,迁移的人直接对照着看:
| 版本 | 分页插件配置 | 说明 |
|---|---|---|
| 3.0.x | new PaginationInterceptor().setDialectType("mysql") | 单插件,直接注册成 Bean |
| 3.4.0+ | MybatisPlusInterceptor+PaginationInnerInterceptor | 链式结构,可叠加其他插件 |
调用分页的代码两种版本完全一样,不受影响:
Page<User> page = new Page<>(1, 10); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getDeleted, 0); userService.page(page, wrapper); long total = page.getTotal(); // 总记录数 List<User> records = page.getRecords(); // 当前页数据new Page<>(1, 10)第一个参数是页码从 1 开始,第二个是每页条数。page对象传入 service 方法后会被拦截器改写,查询完total、records、current、pages都会被回填,不用手动去 count 一遍。乐观锁的用法是实体里加@Version字段,update 时 SQL 自动变成SET version = version + 1 WHERE id = ? AND version = 旧值,影响行数为 0 说明数据被别人改过了,业务层自行决定重试还是抛异常。
4. 代码反向生成实战:AutoGenerator 从表结构扫出四层代码
4.1 生成器主流程:数据源、包名、策略三段式配置
代码生成器是这份案例里最提效的部分。它的原理不复杂:连上数据库,读取表结构和字段元数据,再套用模板把 entity、mapper、mapper.xml、service、serviceImpl、controller 一次性渲染出来。下面这套配置是 3.0 的经典写法,setter 风格,好懂好记:
public class CodeGenerator { public static void main(String[] args) { // 第一步:数据源,告诉生成器去哪里读表结构 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); dsc.setUsername("root"); dsc.setPassword("root"); dsc.setDbType(DbType.MYSQL); // 第二步:全局配置,代码输出位置和作者签名 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(System.getProperty("user.dir") + "/src/main/java"); gc.setAuthor("dev"); gc.setOpen(false); // 第三步:包配置,决定生成到哪个包路径下 PackageConfig pc = new PackageConfig(); pc.setParent("com.example.demo"); pc.setModuleName("system"); // 第四步:策略配置,哪些表参与生成 StrategyConfig sc = new StrategyConfig(); sc.setInclude("sys_user"); sc.setTablePrefix("sys_"); sc.setEntityLombokModel(true); sc.setRestControllerStyle(true); AutoGenerator mpg = new AutoGenerator(); mpg.setDataSource(dsc); mpg.setGlobalConfig(gc); mpg.setPackageInfo(pc); mpg.setStrategy(sc); mpg.execute(); } }四段配置分别解决四个问题:DataSourceConfig解决去哪里读表,GlobalConfig解决代码输出到哪个目录、类注释里签谁的名,PackageConfig解决包路径怎么组织,StrategyConfig解决哪些表生成、表前缀怎么剥、生成什么样的代码风格。执行main方法后,控制台会逐行打印生成的文件路径,看到sys_user对应的六七个文件全部生成成功,就说明这次反向生成完成了。
setOutputDir用的是System.getProperty("user.dir"),也就是当前工程根目录,后面拼/src/main/java。这个写法的坑在于生成器只能在工程根目录下启动,如果你在 IDE 里误把单个模块当启动目录跑,代码会生成到意想不到的位置,这个第 5 章再细说。setOpen(false)的意思是生成完不自动打开资源管理器,窗口弹来弹去很烦,开着反而容易误关 IDE 项目窗口。
4.2 策略配置拆解:表前缀、Lombok、逻辑删除与乐观锁识别
StrategyConfig是生成器里最值得花时间配置的部分,它直接决定生成出来的实体长什么样。我把案例里的配置项拆开说明:
| 配置项 | 案例值 | 作用 |
|---|---|---|
setInclude | "sys_user" | 只处理指定表,支持逗号分隔多表 |
setTablePrefix | "sys_" | 生成类名时剥掉前缀,sys_user变User |
setEntityLombokModel | true | 实体生成@Data注解,省掉 getter/setter |
setRestControllerStyle | true | Controller 生成@RestController而非@Controller |
setInclude是最常用的控制项。初期完全可以setInclude("sys_user","sys_role","sys_menu")三表一起生成,批量出模块骨架。setTablePrefix("sys_")本质是字符串替换规则,sys_user剥掉sys_变成user,再首字母大写变成User,如果你的表没有统一前缀,这项就留空。setRestControllerStyle(true)生成的 Controller 直接支持返回 JSON,省得每个方法都手加@ResponseBody。
3.0 的生成器还有一组专门识别逻辑删除和乐观锁的配置,在实际项目中几乎必须开:
sc.setLogicDeleteFieldName("deleted"); sc.setVersionFieldName("version");setLogicDeleteFieldName("deleted")会让生成的实体自动带上@TableLogic注解,setVersionFieldName("version")会让实体带上@Version注解。这两项开了之后,生成出的 entity 基本就是三章那个成品,不用再手补注解。如果不配,生成的实体只是个纯映射类,逻辑删除和乐观锁全靠手写,那就失去了生成器一半的意义。
4.3 生成后的第一轮检查清单
生成器不是点了就收工,生成完我建议按固定顺序做一轮检查,每项只要几十秒,但能挡掉大部分运行期报错。
第一步看实体注解。打开User.java,确认@TableName("sys_user")在、@TableId的type是ASSIGN_ID、@TableLogic在deleted上、@TableField(fill = FieldFill.INSERT)在createTime上。这四个注解在,说明策略配置生效了。
第二步看 XML 目录。如果表里有自定义类型或需要特殊映射,生成器会输出一个UserMapper.xml,确认它落到了src/main/resources/mapper/下而不是src/main/java下。没落到 resources 的话,mapper-locations: classpath*:/mapper/**/*.xml根本扫不到,运行期报Invalid bound statement。
第三步改数据源连接串里没有的库表。生成器连的是你数据源里已有的表,如果setInclude里的表名在数据库里不存在,执行时会直接抛异常,检查清单里加上这一条能避免对着屏幕发呆。
第四步跑一个最小验证:启动项目,直接 GEThttp://localhost:8080/user/1,看控制台 SQL 是否带了deleted=0条件,看返回的 JSON 里id是不是一长串雪花数字。这一步通过,说明实体映射、逻辑删除、主键策略、Controller 路由整条链路都通了。
5. 避坑记录:3.0 系列实战里的五个高频翻车点
5.1 分页查询失效,返回全表数据
现象:前端点第二页,返回的数据还是第一页的内容,控制台打出的 SQL 里根本没有LIMIT关键字,而接口返回的total是全表总数。
原因:PaginationInnerInterceptor没有注册进 Spring 容器。分页拦截器在 3.0 里不是默认开启的,没有拦截器,selectPage就退化成普通查询,先把全表捞进内存再截取,数据量一大接口直接卡死。
解决:确认MybatisPlusConfig被@Configuration注解且被 Spring Boot 扫描到,分页插件按第 3 章的配置挂进MybatisPlusInterceptor。另外注意 3.0.x 老版本要换成PaginationInterceptor,两个类都试过还不行,就看控制台启动日志里有没有打印拦截器注册信息。
5.2 雪花 ID 传到前端精度丢失
现象:列表接口返回的id是1523709435873669121这种 19 位数字,前端拿到之后显示的末尾几位变成 0,点击详情跳转时 ID 对不上,查出来的数据全是错的。
原因:雪花 ID 是 64 位 Long,超出 JavaScript Number 的安全整数范围(2 的 53 次方),浏览器解析 JSON 时直接把超出精度部分的低位抹掉了。这不是 MyBatis-Plus 的 bug,是所有服务端用 Long 当主键且前端口径不统一的通病。
解决:全局把 Long 序列化成 String,最省事的方式是配置一个 Jackson 定制器:
@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder -> builder .serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); } }serializerByType(Long.class, ToStringSerializer.instance)表示所有 Long 类型序列化成字符串,Long.TYPE对应基本类型 long,两个都要配否则漏掉一种。副作用是某些接口里本意要返回数字的字段也变字符串了,如果你只有主键需要转,更精确的做法是在实体主键上加@JsonSerialize(using = ToStringSerializer.class)。我一般直接全局转,因为前端 JS 数字精度问题不止主键一处会踩,全局转省心。
5.3 逻辑删除字段与唯一索引打架
现象:一条数据逻辑删掉之后,再插入一条业务上相同的数据(比如相同手机号),数据库报Duplicate entry唯一键冲突,插入失败。但明明删过了,为什么还占着位置?
原因:逻辑删除的本质是把deleted从 0 改成 1,记录本身还在表里,建立在(phone)上的唯一索引仍然被这条deleted=1的数据占着。新数据deleted=0与旧数据deleted=1在唯一索引字段上冲突,数据库只认索引,不认业务逻辑。
解决:三个方向,任选其一。第一,唯一索引改成联合索引,把deleted加进去,这样(phone, deleted)组合不同就能共存;第二,把deleted字段的值从固定 0/1 改成删除时间戳,每次删除写入当前毫秒时间戳,唯一索引天然不冲突;第三,业务层在插入前先查一次逻辑删除的数据,如有则做物理删除或复用旧主键。我倾向第一方案,索引改动小,查询性能也影响不大,唯一要注意老数据里有重复deleted=1的历史脏数据时,建联合索引前要先清理。
5.4 QueryWrapper 列名写错,编译期不报错运行期才炸
现象:接口启动正常,第一次调用查询时报SQLSyntaxErrorException: Unknown column 'create_time1',排查半天发现是QueryWrapper里手写列名时少写了一个字母。这种错在单测覆盖率低的项目里,经常上线后才被用户打出来。
原因:QueryWrapper.eq("create_time1", value)里的列名是纯字符串,编译器无法校验它是否真实存在,只有 MyBatis 真正拼 SQL 时才暴露。加上前后端联调时传参一多,很难一眼定位到是哪个字段写错了。
解决:无脑换LambdaQueryWrapper,用User::getCreateTime方法引用替代字符串列名。方法引用的字段来自实体编译后的字节码,实体里没有这个字段直接编译失败,把运行期错误提前到了编译期。实在要用QueryWrapper的字符串形态,就把列名下沉成常量类统一维护,别在业务代码里裸写字符串。
5.5 代码生成器输出目录配错,代码散落一地
现象:跑完生成器,User.java没出现在预期的src/main/java/com/example/demo/entity下,而是在系统盘用户目录、甚至工程的 target 目录里找到了一堆代码,IDE 里看不见也没法直接改。
原因:GlobalConfig.setOutputDir用的是相对当前工作目录的路径。IDE 里直接 runCodeGenerator的 main 方法时,工作目录不一定是你认为的工程根目录,尤其多个 Maven 模块共存时工作目录可能是某个子模块,user.dir取到的路径和预期完全不符。
解决:在生成器 main 方法第一行打印当前工作目录,确认System.getProperty("user.dir")指向哪里。多模块工程里我一般会显式写死输出路径,或者用生成器内置的/src/main/java拼接逻辑再手动纠正一次。生成完第一时间看控制台输出的文件路径列表,确认每个文件都落到预期目录再继续写业务。
6. 二次开发思路:把代码生成器改造成团队模块脚手架
6.1 定制模板:把 Swagger 注解和构造器注入写进生成代码
默认生成的 Controller 是裸的@RestController,方法上没有接口文档注解,也没有统一返回体,离团队规范差一大截。生成器支持通过TemplateConfig替换模板文件,我一般把自定义模板放在src/main/resources/templates/下,生成器跑的时候setController("/templates/controller.java")指向它。
下面这段是我常用的 Controller 模板片段,用 Freemarker 语法写的:
@RestController @RequestMapping("/${table.controllerMappingHyphenStyle}") @Api(tags = "${table.comment!}管理") public class ${table.controllerName} { private final ${entity}Service ${entity}Service; public ${table.controllerName}(${entity}Service ${entity}Service) { this.${entity}Service = ${entity}Service; } @PostMapping @ApiOperation("新增${table.comment!}") public Result<Void> save(@RequestBody @Validated ${entity} entity) { ${entity}Service.save(entity); return Result.success(); } @GetMapping("/{id}") @ApiOperation("按ID查询${table.comment!}") public Result<${entity}> get(@PathVariable Long id) { return Result.success(${entity}Service.getById(id)); } }模板里${table.controllerMappingHyphenStyle}、${table.controllerName}、${entity}都是生成器注入的变量,分别对应路由前缀、控制器类名、实体名。${table.comment!}的感叹号是 Freemarker 的空值兜底语法,表没有注释时输出空字符串而不是报错。构造函数注入替代@Autowired字段注入,是近年来团队里普遍更认可的方式,测试好 mock,依赖也看得清。Result<T>是统一返回体,模板里直接引用团队已有的类即可。
改了模板之后,生成的代码直接就是带 Swagger 注解、带统一返回体、带构造器注入的成品,新模块从建表到可提交的代码,跑一次生成器加二十分钟人工 review 就够。
6.2 扩展 DTO/VO:生成器不止能生成四件套
默认生成器只产 entity/mapper/service/controller 四件套,但实际业务里经常需要UserVO、UserCreateDTO这些参数对象。生成器的TemplateConfig支持自定义模板,做法是在 resources 下建templates/vo.java.ftl,然后在生成器的TemplateConfig里加一段:
TemplateConfig tc = new TemplateConfig(); tc.setEntity("/templates/entity.java"); tc.setController("/templates/controller.java"); mpg.setTemplate(tc);setEntity、setController这些方法把默认模板路径替换成自定义路径,tc里没设的项继续用默认模板。想新增一种文件类型,就在StrategyConfig里扩展相关配置,或者更简单的方式:生成完四件套之后,在 Service 层用BeanUtils.copyProperties做 DTO 转换,VO 类手写也不费劲。我的建议是模板只定制两个文件——Controller 和 ServiceImpl,这两个文件业务差异最大,Entity 和 Mapper 保持默认反而最稳。
6.3 验证方法:幂等生成与接入新表的固定流程
改造完生成器之后,团队里任何一个人都可能跑它,所以验证不能只在自己机器上过一遍。我习惯做两个验证:第一是幂等验证,同一张表连着跑两次生成器,第二次跑完用 Git diff 检查,除了首次生成的代码之外不应该有任何文件变化,如果有,说明模板里有随机内容或者时间戳之类的非确定性输出,必须清掉。第二是新表验证,建一张包含逻辑删除字段、逻辑版本字段、时间字段的测试表,跑完生成器之后重点检查三个点:@TableLogic是否落到了deleted上、@Version是否落到了version上、createTime是否带上了自动填充注解。
这三项检查其实就是把第 4 章的检查清单固化到验证流程里。从那以后,我接手新表的第一件事就是跑一遍生成器,然后核对分页插件、逻辑删除、ID 策略三件事,确认无误再写业务。希望帮到你。
本文还有配套的精品资源,点击获取