news 2026/9/10 4:27:57

MyBatis-Plus代码生成器实战:从数据库表到CRUD代码一键生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis-Plus代码生成器实战:从数据库表到CRUD代码一键生成

前阵子帮朋友维护一个老系统,数据库里二十多张表,业务不算复杂,但每张表都得配实体类、Mapper接口、XML、Service、ServiceImpl、Controller。一开始我还挺有耐心,手写了四五张表以后实在顶不住,全是机械重复的增删改查,真正要动脑子的业务逻辑反而没时间看。后来把MyBatis-Plus 代码生成器(也就是常说的数据库逆向工程)接进来,从表结构到可运行的代码,一分钟不到就能产出一套,我把省下来的时间全花在业务优化上。今天这篇就把我实际配置、跑通、二次改造的完整过程写出来,包括版本选择的坑、连接串里的雷区、模板改造的细节,以及生成之后必须处理的几件收尾事。

本文适合谁看?如果你正在用 MyBatis-Plus 写后端接口,或者项目里有一堆表等待建 CRUD,又或者你之前试过动软、软著代码生成器这类老工具但觉得生成结果太死板、不好改,这篇应该能帮到你。我尽量把每个配置项背后的理由讲清楚,不光是给一段能跑的代码,而是让你知道哪一项改了对结果有什么影响,这样你拿到自己的项目里也能灵活调整。

1. 还在手写CRUD的人,值得重新认识一下"数据库逆向工程"

1.1 从动软到MyBatis-Plus:老派生成器为什么让人又爱又恨

如果你做过一段时间的后端开发,多少听过动软代码生成器,或者某些软著申请配套用的代码导出工具。这些工具在当年确实解决了大批量建代码的问题:连上数据库,选择表,点击生成,Controller、Model、DAL 一堆文件就出来了。但用过的同学大概率有同感:生成的东西太"重"且太"老",模板结构是固定的,想加个 Swagger 注解、想把主键策略换一下、想让实体类继承公共父类,都要去翻生成器的配置界面试半天,或者生成完手动批量替换。更麻烦的是,这类工具生成的代码往往和项目里实际使用的框架版本脱节,拿回来还要改依赖、改命名空间,规模一大,反而比手写还累。

MyBatis-Plus 代码生成器是另一种思路:它以依赖库的形式直接寄生在你的项目里,你写一段 Java 配置去驱动它,生成结果天然贴合 MyBatis-Plus 的体系——实体类带@TableName@TableId,Mapper 继承BaseMapper,Service 继承IService,Controller 里直接注入IService调用现成方法。它不追求"生成一套完整的三层架构",而是生成一套"能被 MyBatis-Plus 直接驱动的骨架"。所以相比之下,它的生成结果更轻、更贴合主流 Spring Boot 项目的习惯,改起来也容易。

1.2 逆向工程到底能替你做什么、不能替你做什么

刚接触代码生成器的人,容易把它想象成一个"输入数据库、输出整个项目"的神器,实际不是这样。MyBatis-Plus 代码生成器做的事本质上只有一件:读取数据库表结构(字段、类型、注释、索引、主键),翻译成 Java 代码文件。它能稳定解决的是单表 CRUD 那 80% 的重复劳动。

它能替你做的,我总结下来主要有这些:

  • 根据表名和字段名生成实体类,自动把user_name转成userName,下划线命名转驼峰。
  • 根据主键类型生成对应的@TableId注解,配置好自增或输入型主键策略。
  • 生成 Mapper 接口和 XML 文件,XML 里预留好ResultMap和基础字段列表。
  • 生成 Service 接口与实现类,自动继承IService/ServiceImpl,自带saveremoveByIdpage等通用方法。
  • 生成 Controller,提供一套最基础的增删改查 REST 接口。
  • 把数据库字段注释同步成实体类字段的 Javadoc,代码可读性直接从零分拉到及格线。

它不能替你做的也很明显:跨表复杂查询、业务状态流转、权限校验、数据权限过滤,这些还是要自己写。所以我的建议是:把生成器当"脚手架",别把它当"业务引擎"。它负责把地基和承重墙搭好,里面的装修和功能分区得自己来。

2. 版本与依赖:先把环境里的坑填平

2.1 版本矩阵:主框架与生成器版本号并不总是一一对应

我第一次用 MyBatis-Plus 代码生成器时踩的坑,就是版本号配错。项目里mybatis-plus-boot-starter用的是3.5.3.1,我随手找了一篇老文章,把mybatis-plus-generator配成了3.5.1,结果AutoGenerator类的 API 对不上,编译直接报错。后来才知道,MyBatis-Plus 的主框架版本和代码生成器版本是从某个版本开始各自独立演进的,生成器并不是跟着主框架的版本号走的

这里简单梳理一下版本演变的脉络。在3.5.1及之前,大家常见到的写法是AutoGenerator+GlobalConfig+DataSourceConfig+PackageConfig+StrategyConfig,用链式 setter 来配置。到了3.5.2之后,官方主推FastAutoGenerator,配置方式从"先 new 对象再逐步 set"变成了"create + lambda 回调",代码更简洁。到我现在用的3.5.4/3.5.3这些版本里,FastAutoGenerator已经很稳定了。

我建议你自己项目里如果主框架是3.5.x,直接上FastAutoGenerator,别再用老的AutoGenerator。如果主框架还是3.4.x,那生成器也得跟着用对应老版本,否则启动时可能出现方法找不到之类的兼容问题。这里提供一个我测试过的版本组合:

<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</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency>

注意最后那个freemarker,这是模板引擎依赖。生成器本身不内置模板引擎,官方默认使用的是 Velocity,但如果你的项目里没有引入 Velocity,运行时会报找不到模板引擎相关的类。我习惯用 Freemarker,因为它的模板语法我相对熟,而且 Spring Boot 项目里很多时候已经依赖了它,不会有冲突。如果你要用 Velocity,就引入velocity-engine-core;用 Beetl 就引入beetl这步最容易漏,漏了之后生成器代码本身能编译,一运行就报错。

2.2 数据库连接串里的隐藏雷区:驱动、时区与 nullCatalogMeansCurrent

搞定 Maven 依赖之后,第二个大坑出现在数据库连接串上。生成器要连数据库读表结构,这一步跑不通后面全是空谈。

首先要确认 MySQL 驱动版本。MySQL 5.x 用的是com.mysql.jdbc.Driver,但新版 MySQL 驱动已经把老驱动类标记过时,换个 MySQL 8.x 版本,就要改成com.mysql.cj.jdbc.Driver。我的习惯是直接用com.mysql.cj.jdbc.Driver,配合mysql-connector-j8.x 驱动,不管连 MySQL 5.7 还是 8.0 都能跑。如果你用的是连接池包,驱动类名照抄就行,不用额外处理。

然后是连接串的 URL 参数。一个比较常规的示例是:

jdbc:mysql://127.0.0.1:3306/my_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true

这里几个参数缺一不可:

  • useSSL=false:不关掉 SSL 握手,某些环境下连接会非常慢,甚至超时。生成器本来是一次性工具,能少一事是一事。
  • serverTimezone=Asia/Shanghai:MySQL 8.x 驱动强制要求设置时区,不设就会报The server time zone value ... is unrecognized,代码根本跑不起来。
  • nullCatalogMeansCurrent=true:这是个冷门参数,但建议从一开始就加上。它解决的是连接串里指定了数据库名之后,驱动在读取表清单时把catalog当成 null,导致去读系统库(比如information_schema或其它你有权限的库)的表,生成出一堆莫名其妙的表。加上这个参数,驱动会更严格地按照连接串里的库名去读取,避免"明明只想生成用户表,结果把系统表也扫进来"的情况。

还有一个隐藏问题:生成器对 MySQL 8 和 MySQL 5 的元数据读取方式有差异,如果你用的是 MySQL 5.6,连接串里serverTimezone参数可能反而不被识别,那就要看驱动版本,必要时降级驱动。总之先把驱动和连接参数对齐,再往下配生成器。

3. FastAutoGenerator核心配置拆解:每一项设置都带着理由

3.1 全局配置:从代码风格到注释归属

配置代码写起来不复杂,但每一项的含义和影响范围值得逐一看清楚。我先把一个完整的FastAutoGenerator示例放出来,后面再拆开讲:

FastAutoGenerator.create( "jdbc:mysql://127.0.0.1:3306/my_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true", "root", "123456") .globalConfig(builder -> { builder.author("shipan") // 作者名,会写到类注释里 .enableSwagger() // 生成 Swagger 注解 .dateType(DateType.TIME_PACK) // 时间类型用 java.time 包 .commentDate("yyyy-MM-dd") // 生成注释里的日期格式 .outputDir(System.getProperty("user.dir") + "/src/main/java"); }) .packageConfig(builder -> { builder.parent("com.example.demo") .moduleName("system") .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") .pathInfo(Collections.singletonMap( OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper")); }) .strategyConfig(builder -> { builder.addInclude("sys_user", "sys_role", "sys_user_role") .addTablePrefix("sys_") .entityBuilder() .enableLombok() .logicDeleteColumnName("deleted") .versionColumnName("version") .enableTableFieldAnnotation() .controllerBuilder() .enableRestStyle() .formatFileName("%sController"); }) .execute();

globalConfig里的author会直接进到每个类的 Javadoc 注释里,团队协作时建议写真实姓名或工号,方便追溯。enableSwagger()开不开取决于项目里是否已经集成了 Swagger/OpenAPI,如果集成了就开,生成的实体类字段上会自动加@ApiModelProperty,Controller 方法上会加@ApiOperation;如果项目里没集成 Swagger,开了反而编译不过。dateType(DateType.TIME_PACK)是我个人的偏好,把Date替换成LocalDateTime,现代项目基本都是这个约定,避免一堆Date类型在时间格式化上反复踩坑。commentDate控制的是类注释里"@since 2025-01-01"这种日期的格式,默认带时间分秒,我改成纯日期,干净一点。

3.2 包名与模块划分:决定代码长在哪棵树上

packageConfig最大的作用是决定代码生成到哪个包下面。很多人只配置了parent,没配置moduleName,结果所有类都直接堆在com.example.demo下面,项目结构立刻变成一锅粥。我的做法是:parent写项目的根包,moduleName写当前模块或业务域的名字,比如systemorderuser,这样生成的类会落在com.example.demo.system.entitycom.example.demo.system.mapper这类路径下,一个业务域一个包,找代码和做权限控制都方便。

还有个细节容易忽略:entitymapperserviceserviceImplcontroller这些子包名默认是英文,如果你们团队有自己约定,比如用modeldaomanager代替默认名称,也可以在这里改。真正决定输出路径的是包名 + Java 源码目录,所以不要只改子包名而忘了确认最后的输出目录是否在src/main/java下。

特别要提的是pathInfo这一项。默认情况下,生成的 XML 文件会放在src/main/java对应的包目录里,这不符合 Maven 工程的约定。正常的 XML 应该放在src/main/resources/mapper下。所以必须用pathInfoOutputFile.xml指向资源目录,否则后续 MyBatis 扫描 XML 时会找不到文件。这是我每次配置必写的一项,而且它只影响 XML 的输出位置,不改变 Mapper 接口的包名,两者互不影响。

3.3 策略配置:收窄生成范围,保留扩展余地

strategyConfig是整个生成器里最需要花心思的部分。第一件事是明确要生成哪些表

addInclude就是白名单,只生成指定的表。为什么不直接全库生成?因为很多业务库里会有各种历史表、临时表、统计表,这些表根本不需要生成 CRUD 代码,全库生成会制造一堆垃圾类。用addInclude收窄范围,每次跑生成器之前先想清楚这次要动哪些表,代码产出可控。特殊情况下可以用addExclude排除表,但我印象里用白名单比黑名单理性,因为你在说"我只要这些",而不是"除了这些我都要"。

addTablePrefix("sys_")表示生成实体类时去掉sys_前缀。比如表名sys_user,生成的实体类是User,而不是SysUser。这个前缀设计在大型项目里比较常见,表名用统一前缀做业务域隔离,但 Java 类名里不需要这个前缀。如果你的表已经叫user_info,没有多余前缀,那addTablePrefix就不加,让user_info直接转成UserInfo即可。

entityBuilder下面的几项也值得说说。enableLombok()开启后生成的实体类上会加@Data注解,不生成一堆 getter/setter 方法,代码立刻简洁一个量级;前提是项目里已经引入了 Lombok 依赖,否则编译报错。logicDeleteColumnName("deleted")指定表里的逻辑删除字段名,生成器会在实体类对应字段上自动加@TableLogic注解,这样走 MyBatis-Plus 的通用删除方法时,就会自动改成update语句而不是delete语句,这是线上系统不能省的安全底线。versionColumnName("version")指定乐观锁版本字段,生成的字段上会带@Version,配合后续配置的乐观锁插件,就能实现并发更新的安全控制。

enableTableFieldAnnotation()比较容易被忽略。开启后,实体类每个字段上都会加@TableField("列名"),虽然 MyBatis-Plus 默认也能根据驼峰转下划线自动映射,但显式标注可以避免字段名里出现特殊词、多词缩写时映射错乱。代价是代码稍微啰嗦一点,但稳妥性更好。我倾向于开启,尤其是在接手老表、字段命名不规范的情况下,这个注解等于一个保护罩。

最后是controllerBuilder部分。enableRestStyle()让生成的 Controller 自动加@RestController而不是@Controller,省得每生成完一批代码还手动改一个注解;formatFileName("%sController")控制类名格式,默认就是UserController这种,一般不用改,但如果你不喜欢 Controller 后缀,也可以改成%sApi之类。

3.4 数据源配置:选择目标库的正确姿势

可能有人会问,FastAutoGenerator.create(url, username, password)不是已经写了连接信息吗,为什么还要单独配置数据源?

原因在于create方法接受的是最简单的基础连接参数,而dataSourceConfig允许你指定更细的数据库类型、驱动类名,以及自定义数据库类型转换规则。对于大部分单数据源项目,直接create(url, username, password)就够了。但如果你连接的是 PostgreSQL、Oracle 或者 SQLServer,推荐用dataSourceConfig显式声明:

FastAutoGenerator.create( new DataSourceConfig.Builder(url, username, password) .databaseQueryClass(SqlQuery.class) // 默认根据 url 判断,一般不用手动指定 .typeConvert(new MySqlTypeConvert()) .build() )

实际项目中,我用得最多的是默认行为。只有当数据库驱动无法被自动识别,或者字段类型映射不符合预期的时候,才需要显式去改。比如 MySQL 的tinyint(1)在某些版本里会被映射成Boolean,但业务上可能希望映射成Integer,这时就需要自定义typeConvert来处理。这个属于进阶玩法,新手可以先跳过,等遇到具体问题再回来调。

4. 从建表到出代码:一次完整逆向工程演示

4.1 准备一张覆盖常见场景的业务表

光讲配置不落地,看完还是不会用。我拿一张实际业务表跑一遍完整流程,你可以照着建表、照着生成,然后对比结果。

假设我们要生成一个"系统用户"相关的代码,表结构如下:

CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `status` tinyint(1) DEFAULT '1' COMMENT '状态:1启用 0禁用', `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除标记:0未删除 1已删除', `version` int(11) DEFAULT '0' COMMENT '乐观锁版本号', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

注意我在建表时把每个字段的COMMENT都写清楚了。这一步非常关键,因为生成器会把字段注释直接变成实体类字段的 Javadoc。如果建表的时候图省事不写注释,生成的实体类是干干净净的,后面看代码的人只能去翻数据库,体验极差。好的表结构是好代码的第一步,这句话在代码生成器场景下体现得淋漓尽致。

这张表里涵盖了常用的字段类型:主键bigint自增、字符串、整数、布尔、日期时间,还有逻辑删除字段deleted和乐观锁字段version,生成的代码能覆盖绝大多数后端 CRUD 场景。

4.2 运行生成器与产出文件

配置沿用上一节的完整示例,我这里把实际跑起来的main方法贴完整,方便你直接复制修改:

public class CodeGenerator { public static void main(String[] args) { String url = "jdbc:mysql://127.0.0.1:3306/my_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true"; String username = "root"; String password = "123456"; FastAutoGenerator.create(url, username, password) .globalConfig(builder -> { builder.author("shipan") .enableSwagger() .dateType(DateType.TIME_PACK) .commentDate("yyyy-MM-dd") .outputDir(System.getProperty("user.dir") + "/src/main/java"); }) .packageConfig(builder -> { builder.parent("com.example.demo") .moduleName("system") .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") .pathInfo(Collections.singletonMap( OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper")); }) .strategyConfig(builder -> { builder.addInclude("sys_user") .addTablePrefix("sys_") .entityBuilder() .enableLombok() .logicDeleteColumnName("deleted") .versionColumnName("version") .enableTableFieldAnnotation() .controllerBuilder() .enableRestStyle() .formatFileName("%sController") .mapperBuilder() .enableBaseColumnList() .enableBaseResultMap(); }) .execute(); } }

运行后,项目src/main/java下会多出这些文件:

src/main/java/com/example/demo/system/ ├── controller/ │ └── UserController.java ├── entity/ │ └── User.java ├── mapper/ │ └── UserMapper.java ├── service/ │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java src/main/resources/mapper/ └── UserMapper.xml

注意UserMapper.xml被我手动指定到了src/main/resources/mapper下,而不是默认的src/main/java。这是很多人在生成完代码后出现Invalid bound statement报错的主要原因,路径不对,MyBatis 找不到对应的 SQL 映射文件,所以从配置阶段就得把它掰到正确的位置。

4.3 查看生成的代码:哪些直接用,哪些要动手改

生成完以后,我们先看看实体类长什么样。下面是生成的User.java核心内容:

@Data @EqualsAndHashCode(callSuper = false) @TableName("sys_user") @ApiModel(value = "User对象", description = "系统用户表") public class User implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty("主键ID") @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty("用户名") @TableField("username") private String username; @ApiModelProperty("密码") @TableField("password") private String password; @ApiModelProperty("状态:1启用 0禁用") @TableField("status") private Integer status; @ApiModelProperty("逻辑删除标记:0未删除 1已删除") @TableField("deleted") @TableLogic private Integer deleted; @ApiModelProperty("乐观锁版本号") @TableField("version") @Version private Integer version; @ApiModelProperty("创建时间") @TableField(value = "create_time", fill = FieldFill.INSERT) private LocalDateTime createTime; @ApiModelProperty("更新时间") @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

这里有几个点需要你留意。

一是createTimeupdateTime自动加了fill = FieldFill.INSERTfill = FieldFill.INSERT_UPDATE,这是 MyBatis-Plus 的自动填充注解。如果要生效,你还得自己在项目里配置一个MetaObjectHandler实现类,在插入和更新时统一填写这两个字段。不配置的话,注解加了也白加,数据库里的时间字段如果本身有默认值CURRENT_TIMESTAMP倒也能跑,但如果你想让代码统一维护时间,就得补上这个 Handler。

二是UserMapper接口里只继承了BaseMapper<User>,但 XML 文件里已经生成了ResultMap和基础的selectByExample类似的结构。这里提示你,如果没有特殊 SQL,Mapper 接口和 XML 其实可以精简,但因为 XML 里保存了最完整的字段列信息,后续写联表查询时可以直接复制字段清单,所以建议保留。

三是UserController生成的是一套最原始的 CRUD 接口,比如:

@RestController @RequestMapping("/system/user") @Api(tags = "系统用户表") public class UserController { @Autowired private UserService userService; @PostMapping public Result save(@RequestBody User user) { ... } @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { ... } @GetMapping("/{id}") public Result getUser(@PathVariable Long id) { ... } @GetMapping public Result list(@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, User user) { ... } }

这段代码"能跑",但离"能上线"还差得远:事务、数据权限、参数校验、日志、实际的分页查询条件都要自己补。我的定位是:Controller 生成出来当接口骨架参考,或给内部管理后台用,真正对外的业务接口还是要手工精写。而实体类、Mapper、Service 这三层生成完成度很高,基本可以原样使用。

5. 改模板比改代码更划算:自定义生成模板的实战

5.1 把官方模板抠出来改成自己的

代码生成器默认的模板功能很全,但不可能满足所有团队的定制需求。比如有的团队要求实体类必须继承一个BaseEntity,有的要求在 Controller 的每个方法上加@PreAuthorize权限注解,有的要求在 Mapper 接口里追加自定义查询方法。这些需求如果生成完再手动改,每张表都要改一遍,太痛苦。正确做法是:改模板,让生成的结果从一开始就符合团队规范。

模板文件在哪?如果你用的是 Freemarker,模板文件就在mybatis-plus-generator的 jar 包里的/templates目录下。文件命名大概是这样:entity.java.ftlmapper.java.ftlservice.java.ftlserviceImpl.java.ftlcontroller.java.ftlmapper.xml.ftl。你可以从本地的 Maven 仓库把依赖 jar 解压出来,把需要的模板文件复制到项目src/main/resources/templates目录下,再按需修改。

然后在生成器配置里加一段,让代码生成器使用你的自定义模板:

.templateConfig(builder -> { builder.entity("/templates/entity.java.ftl") .controller("/templates/controller.java.ftl") .mapper("/templates/mapper.java.ftl") .service("/templates/service.java.ftl") .serviceImpl("/templates/serviceImpl.java.ftl") .xml("/templates/mapper.xml.ftl"); })

5.2 在实体模板里埋入 Swagger 与专属注解

举个我自己改过的例子。项目里实体类要统一加一个@ApiModel注解,并且要在类注释里记录对应的表名和表注释。我改entity.java.ftl模板,在类定义位置加入:

<#if swagger> @ApiModel(value = "${entity}对象", description = "${table.comment!}") </#if> @TableName("${table.name}") @Data public class ${entity} implements Serializable { private static final long serialVersionUID = 1L; <#list table.fields as field> <#if field.comment!?length gt 0> /** * ${field.comment} */ </#if> <#if swagger> @ApiModelProperty(value = "${field.comment}") </#if> @TableField("${field.name}") private ${field.propertyType} ${field.propertyName}; </#list> }

模板里用到的${entity}${table.name}${field.propertyName}这些变量,是生成器内部渲染模板时上下文中提供的。如果你不熟悉模板语法,先大致理解成"占位符 + 条件判断"即可,改的时候主要关注 HTML 标签之外的那几行 Java 结构是否满足需求。这里最关键的是:模板文件的存放路径和配置里的builder.entity(...)路径必须一致,否则会静默使用默认模板,你改了半天的东西根本不生效。

另一个常见的模板改造是把@TableId的主键策略改成指定类型。默认生成器会根据数据库主键判断IdType.AUTO,但如果你用的是分布式 ID(比如雪花算法),可以在模板里强制写出@TableId(value = "id", type = IdType.ASSIGN_ID)。这样生成出来的实体类,新增数据时即使不手动设置id,MyBatis-Plus 也会帮你生成一个雪花 ID,很省心。

5.3 裁剪输出:不需要的东西不生成

模板改造解决的是"生成的不够好"的问题,还有一类需求是"生成得太多了"。比如很多内部接口根本不需要 XML 文件,或者单表操作完全不需要 Service 层,哪些代码不生成,可以直接在strategyConfig里关掉:

.strategyConfig(builder -> { builder.serviceBuilder().formatServiceFileName("%sService") .controllerBuilder().enableRestStyle() .mapperBuilder().enableBaseResultMap(); })

更彻底一点,可以通过templateConfig把某个模板置空:

.templateConfig(builder -> { builder.xml(null); })

这样生成之后 XML 文件就不会出现。我遇到的情况是:项目里允许 MyBatis-Plus 的BaseMapper直接提供单表 CRUD,确实没有必要为每张表都放一个 XML 文件。只有那些包含复杂 SQL 的表,才需要单独生成 XML 并手工加工。所以我在团队里的默认做法是:先不关 XML,等确认表里不需要复杂 SQL 了,再删掉 XML,避免后期排查问题少一个环节。你可以根据自己项目的口味来,习惯极简就关掉,习惯保守就留着。

6. 生成之后的必修课:扫描、XML、逻辑删除与分页

6.1 Mapper扫描与XML路径:最常见的两个启动报错

代码生成完不代表项目能直接起来,我见过太多人激动地跑main生成完代码,一启动 Spring Boot 就报错,然后一脸懵。最典型的两个错误都跟 Mapper 相关。

第一个是 Mapper 接口没被扫描到。生成出来的UserMapper只是普通接口,它要实现 MyBatis 的动态代理,必须被 Spring 容器扫描到。两种常见处理方式:在启动类上加@MapperScan("com.example.demo.**.mapper"),或者在每个 Mapper 接口上标@Mapper。我个人推荐@MapperScan,因为一张表一个注解太啰嗦,还容易漏。如果你生成的包名里有moduleName,记得把扫描路径写对,比如com.example.demo.system.mapper

第二个是 XML 文件位置不对或没被加载。Spring Boot 项目里,MyBatis-Plus 默认会去classpath*:/mapper/**/*.xml找 XML 文件。如果你用上面的路径配置,生成在src/main/resources/mapper下,那默认就能被扫描到。如果你的 XML 放在别处,或者明明放在resources目录却没生效,多半是应用配置里缺少这一段:

mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.demo.system.entity

type-aliases-package配置好之后,XML 里的resultTypeparameterType可以用短类名,不用写全限定名,是个好习惯。

6.2 逻辑删除、乐观锁、分页插件:不配好等于半残

生成器把@TableLogic@Version写在实体类上了,但如果你没在 MyBatis-Plus 配置里注册对应的拦截器,这两个注解的作用其实是"部分生效"。

逻辑删除比较特殊,@TableLogic只要在字段上标了,MyBatis-Plus 的通用删除方法就会自动改成逻辑删除,不需要额外插件。但有个细节:逻辑删除的全局配置和字段默认值的对齐。你在实体类上标了@TableLogic,如果删除时没有在 SQL 里设置删除值(默认是 1,未删除是 0),数据库里也得跟模板保持一致的约定。更稳妥的方式是在application.yml里显式声明:

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

这样即使实体类忘了标@TableLogic,只要字段名匹配,MyBatis-Plus 也会自动识别。

乐观锁就需要显式注册插件了。在配置类里加一个MybatisPlusInterceptorBean:

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

这里我把OptimisticLockerInnerInterceptorPaginationInnerInterceptor放在一起注册了。分页插件基本是 MyBatis-Plus 项目的标配,生成器生成的page方法如果不配分页插件,selectPage返回的数据不会真的分页,而是查出全量数据再包装,这在数据量大时是性能隐患。所以建议一次性把这两个拦截器都注册上。

7. 把生成器变成团队基建:批量执行与日常维护心得

7.1 全表生成还是精确生成:我的取舍原则

用代码生成器时间久了,你会发现它不仅能提高单次开发效率,还能沉淀成团队的基础设施。但怎么用好它,还是有一些原则要守住。

我的取舍原则是:Controller 少生成或不生成,Service 和 Mapper 能生成就生成。

Controller 这层代码往往跟具体业务接口设计强相关,用户权限、接口命名、返回结构、参数校验全是定制化的,生成器给的东西只能算"雏形"。如果团队风格是把业务写在 Controller 里,那生成器生成的 CRUD 也许勉强够用;但如果你们有严格的分层规范,Controller 应该手工控制。我在日常开发中,生成器默认只输出entitymapperserviceserviceImpl,Controller 开着是为了看接口结构参考,但往往生成完我会直接删除。

Service 这层是值得生成的。因为 MyBatis-Plus 的IService已经提供了大量现成方法,Service接口和实现类生成后几乎零成本可用,后续加业务逻辑就在对应方法里扩展,不会影响整体结构。

7.2 表结构变更后,如何优雅地重新生成

数据库表结构不是一成不变的,加了字段、改了注释、换了索引,都是家常便饭。这时候重新跑一遍生成器,会遇到一个问题:覆盖还是保留?

我的建议是:实体类、Mapper 接口、XML 这三类文件可以直接覆盖。因为它们的核心内容来自数据库表结构,不包含业务逻辑,重新生成后只要没有手工动过,结果一定是对的。但 Service 接口和实现类、Controller 就不建议直接覆盖了,因为这里大概率已经写过业务方法,覆盖一次丢一大堆代码,心态直接就崩了。

实际操作上,我通常只让生成器重新生成实体类和 Mapper 接口,跑之前用addInclude指定变更过的表,跑完之后再手动把新增字段补到业务代码里。这样可以最大程度避免"生成器覆盖手写代码"的惨案。

如果你真的希望生成器完整重新生成一套,那就把项目里对应的手写类先备份,生成完再对比合并。这不是最优雅的方式,但胜在可控。

7.3 我遇到过的几个冷门坑

最后分享几个我踩过的、不太容易在文档里看到的坑。

第一个是表名大小写问题。MySQL 在 Windows 下表名大小写不敏感,但在 Linux 下敏感,addInclude里写的表名必须跟数据库里的大小写完全一致。比如库里的表叫Sys_User,你在addInclude里写sys_user,Windows 上能生成,Linux 上就会提示找不到表。

第二个是生成器连接数据库超时。如果数据库地址是内网 IP,且serverTimezone没配或者配错,连接耗时可能长达几十秒。我遇到过一次生成器卡住不动,排了半天才发现是时区问题,驱动一直在尝试解析本地时区。把serverTimezone=Asia/Shanghai加上之后立刻恢复正常。

第三个是关于模板文件后缀。如果你用 Freemarker,模板文件后缀必须是.ftl;如果用 Velocity,后缀是.vm。配置templateConfig时,路径对应关系要对得上,否则运行时会直接抛模板引擎不匹配的异常。这个错虽然好定位,但第一次遇到时确实会愣一下。

第四个是关于enableSwagger()的连锁反应。这个选项一旦开启,生成器不但会在实体类上加 Swagger 注解,还会在 Controller 的方法上加@ApiOperation、在类上加@Api。如果项目里没有 Swagger 依赖,编译直接失败。所以我在生成器跑之前都会先确认项目的pom.xml里有没有springfoxspringdoc相关依赖,没有就先不开,生成了再手动补注解反而更快。

用了几年代码生成器,我最深的体会是:工具解决的是"重复劳动",不解决"设计问题"。表结构设计得乱七八糟,生成器只能帮你把乱象原样搬到 Java 世界;表设计得规范清晰,生成器产出的代码也赏心悦目。所以每次跑生成器之前,我都会先花十分钟看一遍表的注释、字段命名、类型选择,确认没问题再动手。与其说生成器提高了我的写码速度,不如说它逼着我先想清楚数据库设计——这大概是它在工程之外给我带来的最大价值。如果你正打算把代码生成器引入项目,建议从一个边界清晰的业务模块开始,小范围试点,跑通一条最小路径后再铺开,会比一次性全量生成顺手很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 4:27:06

Keras图像分类调优七开关:让CNN稳定识别T恤与外套

1. 这不是在调参&#xff0c;是在给AI做“触觉训练”你有没有试过把一件T恤和一件牛仔外套同时塞进洗衣机&#xff1f;机器不会管你是棉质还是牛仔布&#xff0c;它只认“一堆能转的软东西”。但人眼一扫就知道&#xff1a;袖子长度、领口形状、下摆是否收束、肩线是否硬挺——…

作者头像 李华
网站建设 2026/9/10 4:26:17

STM32C5轮询读取LSM6DSV320X陀螺仪的原理与实战避坑指南

1. 这不是“跑个例程”那么简单&#xff1a;为什么轮询读LSM6DSV320X是STM32C5项目里最常踩坑的起点你手头刚拿到一块崭新的STM32C5开发板&#xff0c;芯片丝印清晰&#xff0c;配套的LSM6DSV320X传感器模块也焊得工整。你打开CubeMX&#xff0c;勾选IC1&#xff0c;生成初始化…

作者头像 李华
网站建设 2026/9/10 4:25:52

STM32F103RC全桥驱动死区PWM配置与补偿解析

简介&#xff1a;面向STM32全桥驱动与电机控制场景的PWM死区实验资源&#xff0c;使用Keil开发环境基于STM32F103RC寄存器方式实现四路带死区PWM输出&#xff0c;适合嵌入式初学者及需要理解死区配置、TIM定时器寄存器操作的开发者。压缩包共60个文件&#xff0c;以h头文件、c源…

作者头像 李华
网站建设 2026/9/10 4:25:10

考虑需求响应的电热综合能源系统两阶段优化调度及Matlab实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 4:24:57

AI Agent记忆系统实战:从架构到遗忘机制

你有没有过这种体验&#xff1a;昨天刚和某个AI助手聊完旅行计划&#xff0c;今天再打开它&#xff0c;对方一脸无辜地反问“你想去哪儿玩来着”。如果你只是个普通用户&#xff0c;顶多吐槽一句“人工智障”&#xff1b;但如果你正在做AI Agent开发&#xff0c;这种“金鱼记忆…

作者头像 李华