☰
MyBatis-Plus 3.0 全链路案例:从代码生成到插件配置与踩坑指南
2026/10/17 16:34:19 网站建设 项目流程

简介:面向 Java 开发者的 MyBatis-Plus 3.0 逆向工程案例包,演示如何借助代码生成器自动产出 Mapper 接口、XML 映射文件、Service 接口与实现及 Controller 层代码,并完成与 SpringBoot 框架的整合。相比 2.3 版,3.0 在代码结构与性能上均有明显优化,集成 JDK1.8 以上新特性,支持 Lambda 查询和 Lombok 简化实体,同时内置主键生成策略与日期映射策略,适合正在从 2.x 迁移或想快速掌握 3.0 代码生成的初学者。压缩包共 28 个文件,以 Java 源码、XML 配置、properties 属性文件、class 编译产物及 Maven 工程描述文件为主,另含 IDE 工程配置与构建脚本,整体仅 66KB,便于直接导入开发环境查看完整工程结构。目前已有 572 人学习,资源内包含生成器入口、pom 依赖配置及示例输出文件,按目录划分清晰,可直接参考运行结果反推代码生成逻辑,也可对照配置文件逐项理解 MyBatis-Plus 3.0 的参数定义方式。

1. 项目定位:一套把 MyBatis-Plus 3.0 从生成到跑通的全链路案例

先说一个反直觉的结论:MyBatis-Plus 3.0 的代码反向生成器,五分钟就能把一张表变成 Controller / Service / Mapper / 实体全套代码,但项目真正拉开差距的地方,恰恰在生成之后的改造——逻辑删除注解配在哪、乐观锁插件注册在哪一步、自动填充为什么总是 null。这些细节在官方文档里分散在不同页面,等你上线才发现配置错了,回滚成本已经很高。这套「mybatis-plus 3.0 案例」解决的就是这个 gap:它把代码反向生成、条件构造器、分页插件、高级注解全部串在同一个 demo 工程里,生成后直接能启动、能跑接口、能看到 SQL 日志。适合三类人:想从手写 SQL/XML 切到 MP 3.0 的开发者,新项目想用生成器起手的人,以及维护老 MyBatis 项目但担心升级翻车的人。

2. 技术选型与资源拆解:为什么这套案例要落在 Spring Boot + MyBatis-Plus 3.0 上

2.1 3.0 升级后真正变化的点

很多项目从 2.x 升到 3.0 时,改动量比想象中大。MyBatis-Plus 3.0 最有感知的变化是插件体系重做了:2.x 时代各类插件直接 new 一个PaginationInterceptor注册到配置里就行,3.0 之后统一收敛到MybatisPlusInterceptor,由它分发到PaginationInnerInterceptor、OptimisticLockerInnerInterceptor这类内部拦截器。这个改动直接影响你写配置的方式,也是这套案例里单独给MybatisPlusConfig留位置的原因。

其次是实体和 Service 的泛型链路。3.0 的IService<T>内部做了一层链式调用封装,ServiceImpl<M, T>把 Mapper 和实体绑定得更紧,生成器生成的代码直接继承这套接口就能获得saveOrUpdate、lambdaQuery这类便捷方法,不用再自己写selectByXxx的 Mapper 方法。

还有一个容易被忽略的点:3.0 强化了 Lambda 表达式作为查询条件的支持。LambdaQueryWrapper在编译期就锁死列名,避免了字符串列名写错后线上才暴露的问题。案例里所有查询示例基本都以 Lambda 写法为主,这是 3.0 时代推荐的用法。

2.2 案例资源的目录与分层结构

拿到这份案例,第一步不是看代码,是先梳理目录。整个工程是标准的三层结构,但多出config和generator两个目录,这两块恰恰是普通 MyBatis 项目里没有的东西。

src/main/java/com/example/demo ├── controller # 对外接口层,生成器自动生成 ├── service │ ├── IUserService # 接口,继承 IService<User> │ └── impl │ └── UserServiceImpl # 继承 ServiceImpl<UserMapper, User> ├── mapper # Mapper 接口,继承 BaseMapper<User> ├── entity # 实体类,带 @TableName/@TableField ├── config │ ├── MybatisPlusConfig # 分页插件、乐观锁插件注册 │ └── MyMetaObjectHandler # 自动填充实现 └── generator └── CodeGenerator.java # 代码反向生成入口 src/main/resources ├── mapper # 自定义 SQL 的 XML 文件 └── application.yml

controller 和 service 层是生成器直接产物,改动最多的反而是config。案例把插件配置单独抽出来,是因为 3.0 的拦截器链顺序有讲究:分页和乐观锁都挂在同一个MybatisPlusInterceptor下,如果你后续加多租户插件,还要注意加入顺序。这块放后面避坑章节详细说。

2.3 数据源与 MyBatis-Plus 的核心配置

跑通案例之前,先把application.yml看明白。这份配置里几乎每个配置项都能在真实项目里找到对应场景,这比单独看文档有效得多。

spring: datasource: url: jdbc:mysql://localhost:3306/demo ?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 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 global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

几个关键参数拆开讲。

mapper-locations指定 XML 扫描路径,案例中统一放在resources/mapper下。如果你项目里 Mapper 接口和 XML 分开存放,这里务必配成对应路径,否则自定义 SQL 全部失效。

global-config.db-config是 3.0 的全局配置入口。2.x 时代这些项散落在properties或GlobalConfig里,3.0 收拢到db-config下。这里的logic-delete-field: deleted是全局逻辑删除字段声明,配合实体上的@TableLogic注解一起生效。网上很多帖子只教你在实体加注解,不教全局配置,结果字段名对不上就静默失效——案例里两个地方都给了。

map-underscore-to-camel-case: true是下划线转驼峰的开关。数据库字段user_name要映射到实体的userName,靠的就是这个配置。如果从老项目拷贝配置时漏了这一行,你会发现生成器生成的实体一切正常,但运行查询时所有带下划线的字段全是 null。

log-impl配成StdOutImpl是为了把 SQL 打到控制台。我一般建议初次跑案例时开着,能看到每条语句的完整参数预编译效果,排查问题效率高很多;生产环境记得换成Slf4jImpl或者关掉。

依赖方面,案例基于 Spring Boot 2.x 和mybatis-plus-boot-starter3.0 系列。需要注意的是,如果你用了代码生成器,mybatis-plus-generator要单独引入,它不在 starter 里。3.0 早期版本默认走 Velocity 模板,3.5.x 之后模板引擎改为可插拔,常见做法是引入freemarker依赖配合使用,否则运行生成器时容易报模板引擎找不到类。

3. 代码反向生成:从数据库表到 Mapper/Service/Controller 的完整链路

3.1 生成器主流程与数据源配置

案例的核心入口是generator/CodeGenerator.java。这个类的任务很简单:连上数据库,读表结构,然后用模板把表字段映射成 Java 代码。真正要关注的是三个配置对象:DataSourceConfig、GlobalConfig、StrategyConfig。

public class CodeGenerator { public static void main(String[] args) { // 1. 数据源配置:生成器直接连库读表结构 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/demo" + "?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8"); dataSourceConfig.setUsername(System.getenv("DB_USER")); dataSourceConfig.setPassword(System.getenv("DB_PASS")); dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver"); // 2. 全局配置:输出目录、作者、是否生成 Swagger 注解 GlobalConfig globalConfig = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); globalConfig.setOutputDir(projectPath + "/src/main/java"); globalConfig.setAuthor(System.getenv("USER")); globalConfig.setOpen(false); globalConfig.setSwagger2(true); // 3. 策略配置:要生成哪几张表、表前缀怎么处理 StrategyConfig strategyConfig = new StrategyConfig(); strategyConfig.setInclude("t_user", "t_order"); strategyConfig.setTablePrefix("t_"); strategyConfig.setEntityLombokModel(true); strategyConfig.setRestControllerStyle(true); // 4. 组装并执行 AutoGenerator generator = new AutoGenerator(); generator.setDataSource(dataSourceConfig); generator.setGlobalConfig(globalConfig); generator.setStrategy(strategyConfig); generator.execute(); } }

这里有几个参数值得展开。

setOutputDir决定生成代码落到磁盘哪个目录。案例里直接用System.getProperty("user.dir")取当前工程根目录,这样无论在哪台机器跑,生成代码都落在当前工程的src/main/java下。如果你想生成到独立目录再拷贝,改成绝对路径即可。

setOpen(false)是关闭生成后自动打开输出目录弹窗。如果你是命令行跑生成,弹窗反而碍事,我一般都会设成 false。

setSwagger2(true)会在实体类和接口上自动加 Swagger 注解。前提是项目里引入了springfox依赖,否则只生成注解不生效,也不算坏事——至少字段注释有了。

setInclude("t_user", "t_order")是核心筛选条件。它决定生成器连接数据库后读哪些表。注意如果不配setInclude,生成器会把整个库的所有表都生成一遍,这在生产库上是一场灾难。我见过有人直接在测试服执行生成器,十分钟后工程多了两百多个实体文件,删都删不过来。

setTablePrefix("t_")是表前缀处理。生成的实体名会去掉t_前缀,t_user变成User而不是TUser。数据库前缀五花八门,t_、tb_、sys_,实际项目里按你的规范配。

setEntityLombokModel(true)让实体使用 Lombok 的@Data注解,不生成一堆 getter/setter。这个强烈建议开启,否则每个实体几百行样板代码,根本没法看。

3.2 生成后的实体与 Mapper 长什么样

执行生成器之后,entity目录下会出现User.java,这是整个链路里最值得细读的文件。MyBatis-Plus 的注解设计把「数据库表」和「Java 对象」的映射关系全部收敛到了实体上,理解了它,你就理解了 Mapper 为什么不需要写 SQL。

@Data @TableName("t_user") public class User { @TableId(value = "id", type = IdType.AUTO) private Long id; @TableField("user_name") private String userName; @TableField("email") private String email; @TableField(value = "create_time", fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic @TableField("deleted") private Integer deleted; }

@TableName("t_user")把实体类绑定到物理表。如果生成时没去掉前缀,这里的表名会和你数据库里的不一致,跑起来直接报「表不存在」。@TableId声明主键,IdType.AUTO对应数据库自增主键。如果你的表用分布式 ID 或者业务主键,改成ASSIGN_ID或INPUT即可。

@TableField("user_name")完成字段映射。这里有个细节:即使map-underscore-to-camel-case开了,我仍然建议保留这个注解,尤其是在多表字段重名的场景下,显式声明比隐式依赖全局配置更安全。

createTime和updateTime上的fill = FieldFill.INSERT是自动填充的声明。它的意思是:插入时由 MyBatis-Plus 自动写入当前时间,不需要你在业务代码里手动setCreateTime。这片逻辑对应config目录下的MyMetaObjectHandler,后面专门讲。

@TableLogic是逻辑删除的核心。加了它之后,MP 在执行deleteById时会自动转成UPDATE t_user SET deleted = 1 WHERE id = ?,查询时自动追加AND deleted = 0。这样业务代码永远不需要手动拼deleted条件,也不会有「删了还能查出来」的问题。

生成的 Mapper 接口反而非常精简:

@Mapper public interface UserMapper extends BaseMapper<User> { }

继承BaseMapper<T>后,增删改查的基础方法全都有了。selectById、insert、updateById、deleteById、selectList,这些方法不需要写一行 SQL。如果你的业务查询需要多表 join 或者特殊 SQL,再在接口里加方法、在resources/mapper下写 XML 实现。案例里保留了一个UserMapper.xml作演示,展示自定义 SQL 怎么和 MP 生成的方法共存。

3.3 Service 层的链式能力

生成器还会产出IUserService和UserServiceImpl两个文件:

public interface IUserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { }

IService<T>比BaseMapper多了一层业务封装,提供saveOrUpdate、lambdaQuery、lambdaUpdate这类链式方法。Controller 里注入IUserService后,很多简单接口一行代码就能写完,比如:

@GetMapping("/list") public List<User> list(String keyword) { return userService.lambdaQuery() .like(StringUtils.isNotBlank(keyword), User::getUserName, keyword) .list(); }

这套写法省掉了 Mapper 层无脑转发的方法定义,真正的业务条件全部留在 Service 层拼装。案例里 Controller 的代码基本都是这个风格,这也是 3.0 相对 2.x 用起来最顺手的地方。

4. 核心功能模块逐段拆解:条件构造器、分页插件与自动填充的联动

4.1 条件构造器的正确打开方式

MyBatis-Plus 3.0 的条件构造器是从Wrapper体系上长出来的。案例里最常见的用法是LambdaQueryWrapper,它用方法引用代替字符串列名,编译期就能发现字段改名导致的错误。

public List<User> searchUsers(String userName, String email, LocalDateTime startTime, LocalDateTime endTime) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(userName), User::getUserName, userName) .like(StringUtils.isNotBlank(email), User::getEmail, email) .ge(startTime != null, User::getCreateTime, startTime) .le(endTime != null, User::getUpdateTime, endTime) .orderByDesc(User::getId); return userMapper.selectList(wrapper); }

这段代码的关键在两个地方。第一个是eq方法的第一个参数是布尔值,当这个条件为 false 时,MP 会自动跳过这个查询条件。这样前端传不传参都不影响 SQL 的正确性,不需要你在业务里堆满if (xxx != null)判断。四个条件写下来,方法体依然干净。

第二个是顺序问题。Wrapper的条件拼接顺序就是 SQL 拼装的顺序,eq、like、ge、le、orderByDesc连在一起,生成的可读 SQL 和手写几乎一致。排查问题时把 SQL 日志打开,能看到完整的预编译语句。

顺带一提,案例里没有用QueryWrapper的原因是它不禁用字符串列名。QueryWrapper.eq("user_name", "张三")能跑,但一旦实体字段改名而这里没同步改,线上才会暴露。用LambdaQueryWrapper时 IDE 会直接标红,从源头堵住这个坑。

4.2 分页插件:配置注册与常见误用

分页插件是 3.0 案例里配置最容易被低估的一块。很多人以为selectPage传个Page对象就能分页,实际上如果不注册插件,Page对象里只有传入的记录,total永远是 0,翻页更是不存在的。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor( new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

3.0 早期版本的写法是直接注入PaginationInterceptor,3.4 之后统一演进成上面的形式。案例注释里会把两种写法都标出来,新项目建议直接用MybatisPlusInterceptor这套。DbType.MYSQL声明数据库类型,MP 会基于它生成对应的分页方言,切到 PostgreSQL 或 Oracle 时改成对应枚举即可。

分页的业务代码长这样:

public IPage<User> pageUsers(int current, int size, String keyword) { Page<User> page = new Page<>(current, size); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(keyword), User::getUserName, keyword); return userMapper.selectPage(page, wrapper); }

调用端拿到IPage<User>后,getRecords()是当前页数据,getTotal()是总条数,getCurrent()是当前页码,getPages()是总页数,直接返回前端即可。

一个常见的翻车点是IPage和Page混用。selectPage要求的入参是Page<T>,返回是IPage<T>。有人把返回类型写成Page<User>,直接new Page<User>()去接,运行时类型不匹配会报错。接口里用IPage,实现里用Page,这个约定记牢。

4.3 自动填充:审计字段的一劳永逸方案

每张表都带create_time、update_time,如果每次插入都手动setCreateTime(now),代码里会涌出大量重复样板。案例用MyMetaObjectHandler把这件事收敛起来。

@Component 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()); } }

这段代码配合实体类上的@TableField(fill = FieldFill.INSERT)使用。当 MP 执行insert时,它检查实体里哪些字段声明了fill,再回调insertFill方法填充值。strictInsertFill方法名的含义是「只在字段值为 null 时才填充」,避免你业务里手动给createTime设了值还被覆盖掉。

这里有个容易忽略的设计点:FieldFill.INSERT_UPDATE和单独声明INSERT的区别。createTime只在插入时填一次,updateTime在插入和更新时都要填。如果用同一个INSERT_UPDATE去填createTime,每次 update 都会把创建时间刷新成当前时间,数据就坏了。案例里两个字段的fill值分得很开,这个细节值得照抄。

如果日志里发现自动填充没生效,先看两处:一是MetaObjectHandler有没有被 Spring 扫描到,@Component是必须的;二是实体字段名和 handler 里写的字符串是否完全一致。strictInsertFill的第二个参数是实体属性名,不是数据库列名,写错大小写直接静默跳过。

4.4 逻辑删除与乐观锁的联动配置

逻辑删除前面已说过注解层面的用法,这里补全配置侧。application.yml里的全局配置是兜底方案:

mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

实体上的@TableLogic是局部生效,yml 里的logic-delete-field是全局兜底。更稳妥的做法是两个都配,这样即使新表实体忘了加注解,全局配置还能兜住 deleted 字段。

乐观锁插件的联动方式和分页一致,挂在同一个拦截器链上:

interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

注意加入顺序。分页和乐观锁互不干扰,但如果你同时用了多租户插件,建议把多租户放在拦截器链最前面。拦截器链的执行顺序在 3.0 里是有语义的,靠后的拦截器处理的是前面拦截器处理过的 SQL。

实体上对应version字段加注解:

@Version @TableField("version") private Integer version;

@Version字段在updateById时会自动带上条件AND version = #{当前值},更新成功后把version值 +1。这样并发提交时后提交的那个事务由于版本号对不上,影响行数为 0,你就能在业务里感知到冲突。常见做法是在 Controller 里捕获这个返回值为 0 的场景,返回「数据已被他人修改」的提示给前端。

5. 常见问题与排查:从 3.0 升级与生成器翻车的真实场景

5.1 现象:生成后下划线字段全部为 null

t_user表的user_name字段映射到实体的userName后,查询出来一直是 null,其他字段正常。这种情况多半不是生成器的问题,而是运行时配置丢了。

原因:下划线转驼峰依赖map-underscore-to-camel-case: true,这个配置写在mybatis-plus.configuration下。老项目从 2.x 拷贝配置时,如果只把mybatis-plus前缀的配置搬过来,漏了这一行,MP 的字段映射就退化成「数据库列名直接等于实体属性名」。

解决:回到application.yml的configuration段补上配置。检查时还要确认实体上的@TableField有没有显式声明列名——显式声明优先于全局配置,如果某个字段想保留下划线命名,单独在注解里写即可。

5.2 现象:分页查询 total 一直为 0

调用selectPage后,page.getTotal()返回 0,getPages()也是 0,但getRecords()有数据。

原因:分页插件没注册。很多人以为 MP 的selectPage自带分页能力,实际它只是把Page对象作为参数传给 SQL,不带插件时 MP 不生成COUNT(*)语句,total 自然取不到。

解决:在配置类里注册MybatisPlusInterceptor并addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL))。注册后重启看 SQL 日志,会发现 MP 自动生成一条SELECT COUNT(*)语句。如果加了插件还没生效,检查是不是把Page对象 new 出来后又手动setTotal了,MP 会认为你已经填过分页数据,直接跳过 count 查询。

5.3 现象:逻辑删除配置了,删除后数据还能查出来

@TableLogic注解加了,deleteById执行后数据库里deleted字段确实变成了 1,但再查列表时已删除的数据还在。

原因:逻辑删除的全局配置缺失。实体注解只能让 MP 知道「这个字段参与逻辑删除」,但删除了什么值算删、什么值算没删,需要logic-delete-value和logic-not-delete-value配合。如果这两个配置没写,MP 默认用1和0,一般来说没问题;真正翻车的情况是数据库里已存在历史数据,有的行deleted是1,有的行是 2、3 等状态值,全局只认 1 就漏了。

解决:在global-config.db-config里补齐逻辑删除配置,把历史状态值统一收敛到 1 和 0。另外注意,如果deleted字段在实体里没声明@TableField,MP 可能把它当成普通字段参与查询,检查注解是否完整。

5.4 现象:乐观锁不生效,version 字段不递增

@Version注解加了,但updateById执行后version还是原值,并发覆盖也没被拦截。

原因:乐观锁插件没注册到拦截器链。3.0 的拦截器全部收敛到MybatisPlusInterceptor,如果只注册了分页插件,没加OptimisticLockerInnerInterceptor,@Version注解就只是个摆设。

解决:在MybatisPlusConfig里补上一行interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor())。另外检查updateById的入参:如果你先selectById查出实体,修改字段后再updateById,这个链路是正常的;但如果你直接new User()只设了主键和业务字段就 update,version为 null,MP 无法生成版本条件,等于没锁。

5.5 现象:自动填充不生效,insert 后 create_time 为 null

实体上fill = FieldFill.INSERT配置了,也写了MetaObjectHandler实现类,但插入数据后两个时间字段还是 null。

原因:MetaObjectHandler没有注册到 Spring 容器,或者实体字段名和 handler 里写的字符串不一致。strictInsertFill(metaObject, "createTime", ...)里的createTime是实体属性名,不是数据库列名create_time,写错任何一个大小写都会静默跳过。

解决:确认@Component注解存在,且实现类被 Spring 扫描。再看 handler 方法里的字段名和实体属性严格一致。还有一种隐蔽情况:业务代码在插入前手动执行了user.setCreateTime(someValue),strictInsertFill发现字段非空就不会覆盖,如果你希望始终使用数据库时间,手动赋值的地方要去掉。

5.6 现象:运行生成器报 ClassNotFound 或模板引擎错误

执行CodeGenerator.main()时抛出模板引擎相关异常,比如ClassNotFound: freemarker...。

原因:mybatis-plus-generator从 3.5.x 开始默认不再内置模板引擎,案例虽然基于 3.0,但你把版本升级到最新后,AutoGenerator的默认模板实现可能找不到类。

解决:引入mybatis-plus-generator时把模板引擎依赖一起加进去。用 Freemarker 就在 pom 里加org.freemarker:freemarker,然后在生成器里指定TemplateConfig设置模板路径。另一个排查点是确认mybatis-plus-generator版本和你引入的mybatis-plus-boot-starter版本同系列,差异过大会出现方法签名不匹配的问题。

6. 二次开发方向:把代码生成器改造成团队自己的基建工具

6.1 用自定义模板替换默认生成样式

案例的生成器用的是 MP 内置模板,输出的是标准三层代码。实际团队里通常会有自己的规范:实体要继承某个基类、Controller 要统一返回结构、字段注释要带上字典说明。这些改动不必每次都手动改生成后的代码,直接在模板层解决。

常见做法是在resources/templates下放一份自定义实体模板,替换默认的entity.java.vm:

package ${package.Entity}; #foreach($annotation in ${table.fieldAnnotations}) ${annotation} #end @ApiModel(description = "${table.comment}") @Data public class ${entity} extends BaseEntity { #foreach($field in ${table.fields}) @ApiModelProperty(value = "${field.comment}") private ${field.propertyType} ${field.propertyName}; #end }

再用TemplateConfig把模板指过去。改造点集中在全局配置的setTemplate上:templateConfig.setEntity("/templates/entity.java.vm"),Service、Controller 同理。这样执行一次生成器,所有实体自动带上extends BaseEntity,所有 Controller 自动实现统一接口,反而比手写更快。

6.2 参数化生成脚本

把CodeGenerator的main方法改造成可传参的入口,是团队内部推广生成器最值得做的事。我一般会让生成器支持两个参数:tables指定要生成的表名,module指定分包路径。执行方式改成 Maven 插件或命令行:

mvn -f pom-generator.xml compile exec:java \ -Dexec.mainClass=com.example.demo.generator.CodeGenerator \ -Dexec.args="t_user,t_order user"

生成器里按逗号拆表名,动态拼接strategy.setInclude(tables),再把packageConfig.setModuleName(module)关联到表前缀上。这样团队新人拿到项目,一条命令就能把新表对应的全套代码生成到com.example.${module}下,不用打开 IDE 修改任何配置。

6.3 验证与回归:生成后别急着写业务

我踩过最贵的坑,是生成完代码不跑测试直接写业务,最后发现某张表的逻辑删除和乐观锁配置有冲突,改了一晚上。从那以后我每次用生成器,不管改了什么配置,都强制走一遍「先跑通一个最小 CRUD 单测再动手改业务」的流程,让生成器和环境配置先互相证明是对的,再往里填业务逻辑。技巧是把案例里的用户表换成你们真实业务表,跑一遍分页查询和带条件的列表查询,SQL 日志确认每个条件都正确拼上,再开始写接口。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询