☰
MyBatis-Plus代码生成器实战:告别手动CRUD,提升SpringBoot开发效率
2026/10/5 12:57:50 网站建设 项目流程

干了多年Java后端,日常开发里最磨人的其实不是业务逻辑,而是那些一层不变的样板代码。SpringBoot项目里用MyBatis-Plus做持久层已经很普遍,但每次新建一张表,手动去写实体类、Mapper接口、Service和Controller,真的又枯燥又容易出错。尤其是字段多、表多的时候,光是复制粘贴改属性名就能让人怀疑人生。

MyBatis-Plus自带的AutoGenerator代码生成器,就是专门解决这个痛点的。它能连上数据库,读取表结构,直接帮你把一整套单表CRUD代码生成出来。这篇文章我会从实际项目使用的角度,把生成器的原理、配置、自定义模板、常见坑一次讲清楚,适合正在用SpringBoot做项目、想提升开发效率的团队和个人参考。

1. 代码生成器到底解决了什么

1.1 手动写CRUD的时间成本

我做过一个后台管理系统,光是基础数据表就接近四十张。每张表如果走全手动流程,实体类一个文件、Mapper接口一个文件、XML映射文件一个文件、Service一个接口加一个实现类、Controller再一个文件,这就是六七个文件。每个字段要对应Java属性,写注释、做类型转换、配MyBatis的映射关系,一张表折腾下来至少半小时。

四十张表就是二十个小时的纯体力劳动。这还只是初版,后续表结构一调整,实体类、XML里的映射全要跟着改。手动维护这种重复代码,效率低就算了,最容易出问题的反而是那种不起眼的小错误,比如某个字段类型映射错了,或者XML里结果集少写了一个字段,运行时才报错。

1.2 生成器能输出什么

MyBatis-Plus代码生成器做的事情,本质上叫“逆向来料加工”。你给它一个数据库连接地址,它读取表结构信息,结合配置好的策略,生成对应的一组代码文件。默认情况下,可以输出:

  • 实体类(带注解、注释、Swagger注解可选)
  • Mapper接口(继承BaseMapper)
  • Mapper XML文件(基础CRUD映射)
  • Service接口(继承IService)
  • Service实现类(继承ServiceImpl)
  • Controller(RESTful控制器,带分页查询、新增、修改、删除接口)

这意味着什么?意味着你建好表之后,跑一次生成器,一个基础模块就立起来了。增删改查、分页查询这些天天写的东西,直接变成“产品”,不用再从零开始加工。

1.3 为什么不选MyBatis Generator

很多老项目用MyBatis Generator(MBG),它同样能生成实体和Mapper,但要配合mybatis-generator-maven-plugin使用,XML配置非常啰嗦,而且生成的实体类带有大量原生MyBatis注解风格,和MyBatis-Plus的LambdaQueryWrapper、分页插件配合并不自然。

MyBatis-Plus AutoGenerator是专门跟着MyBatis-Plus生态走的。它的配置方式更简洁,代码里直接链式调用,生成对象的命名、注解风格都能和MyBatis-Plus的日常使用习惯完美对齐。我们项目里从MBG切到AutoGenerator之后,生成的代码一眼就能看懂,不需要二次删改,整体体验提升很明显。

2. AutoGenerator的工作机制

2.1 从数据库到代码的完整链路

理解生成器原理,不能只看它“能用”,还得知道它内部做了什么。AutoGenerator的处理流程大致如下:

  1. 建立数据库连接,读取表的元数据信息(表名、字段名、字段类型、注释、主键、索引等)。
  2. 根据配置的数据库类型到Java类型的映射规则,把每个字段映射成对应的Java类型(比如int → Integer,varchar → String,datetime → LocalDateTime)。
  3. 配置包名信息、模块名、前缀过滤策略,把数据库表名转换成对应的实体类名(比如user_info → UserInfo)。
  4. 综合以上信息,渲染模板。模板引擎把表结构数据填充到预设的代码模板里,最终输出文件。

数据流向是单向的,数据库是源头,代码文件是产物。所以生成器要求表结构尽量规范,注释完整,这样生成的代码注释才完整,可读性才高。

2.2 类型映射的核心规则

数据库字段到Java类型的映射,是生成器最关键的环节,也是很多人在第一次使用时最容易疑惑的地方。不同的数据库有不同数据类型,AutoGenerator内置了一系列默认映射规则。

以MySQL为例,常见的映射关系是:

MySQL类型Java类型(JDK8+)说明
varchar / char / textString
intIntegertinyint会被映射成Integer
bigintLong
decimal / numericBigDecimal金额、精确浮点用这个
date / datetime / timestampLocalDate / LocalDateTime新版默认映射到java.time包
float / doubleFloat / Double
bit / booleanBoolean / Integerbit(1)一般映射为Boolean

这里有一个容易踩的坑:如果项目JDK版本是1.8以下,或者数据库驱动版本太老,时间字段可能被映射成java.util.Date。而MyBatis-Plus 3.x配合typeHandler处理LocalDateTime很成熟,所以我个人强烈建议JDK8+项目直接使用新版生成器,让时间类型默认映射到java.time包,后续开发更清爽。

2.3 模板引擎选型

AutoGenerator支持Velocity、Freemarker、Beetl三种模板引擎,默认是Velocity。如果没有特殊偏好,直接用默认就好,它生态最稳,生成的代码没有模板痕迹。

如果你需要定制生成代码的风格,可以选择Freemarker或Beetl,它们的模板语法更直观,很多开发者更熟悉。切换方法很简单,在生成器配置里指定TemplateEngine实现类即可。我生产环境里用的都是Velocity,因为它的模板文件可以做精细控制,官方默认模板也是基于Velocity写的,几乎没有迁移成本。

3. 实操准备:版本选择与工程搭建

3.1 依赖版本怎么搭配

这里直接给出我在生产环境验证过的稳定组合,也是目前使用范围很广的方案,适用SpringBoot 2.x系列:

  • SpringBoot:2.7.x
  • mybatis-plus-boot-starter:3.5.3
  • mybatis-plus-generator:3.5.3
  • velocity-engine-core:2.3
  • mysql-connector-java:8.0.33

如果你的项目已经升级到SpringBoot 3.x,需要换成mybatis-plus-spring-boot3-starter,JDK要求17以上,依赖坐标和配置方式略有区别。但生成器的核心配置几乎一致,不影响本文内容参考。

这里强调一个通用原则:生成器版本尽量和使用中的MyBatis-Plus主版本保持一致。如果生成器版本太新,生成的代码格式可能和当前运行库有细微出入,虽然大多数情况下不影响编译,但为了省心,还是对齐版本好。

3.2 工程目录结构

我用一个独立Maven模块(或独立测试类)启动生成器,而不是放在业务主类里。这样做的原因是生成器依赖和业务依赖可以隔离,避免把模板引擎等构建期依赖带到线上运行包。推荐结构:

springboot-demo ├── pom.xml └── src ├── main │ ├── java │ │ └── com/example/demo │ └── resources └── test └── java └── com/example/demo

生成器代码放在test目录下,用@Test方法执行。这样既可以复用主工程的依赖,又不污染线上代码路径。

3.3 数据库准备

生成器的核心数据来源是数据库表,表结构质量直接影响生成代码质量。我在演示中创建一张用户表,包含常见字段类型,方便观察生成效果。

CREATE TABLE `sys_user` ( `id` bigint 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 '邮箱', `status` tinyint DEFAULT '1' COMMENT '状态:1启用,0禁用', `balance` decimal(10,2) DEFAULT '0.00' COMMENT '账户余额', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` tinyint DEFAULT '0' COMMENT '逻辑删除:0未删除,1已删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='用户表';

注意我给表加了sys_前缀,这是一种非常常见的命名方式。生成器可以通过配置去掉前缀,把表名sys_user映射成实体类SysUser,具体配置在下一章细说。

4. 核心代码:一次完整的自动生成配置

4.1 FastAutoGenerator配置解析

MyBatis-Plus 3.5.x版本起,官方推荐使用FastAutoGenerator这个新的链式配置API,比老版的AutoGenerator更简洁。我直接给出一个完整可运行的配置类。

package com.example.demo; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; import java.util.Collections; public class MyGenerator { public static void main(String[] args) { // 1. 配置数据源 String url = "jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; String username = "root"; String password = "123456"; // 2. 输出目录:项目根目录下的 generator-output String outputDir = System.getProperty("user.dir") + "/generator-output"; FastAutoGenerator.create(url, username, password) // 全局配置 .globalConfig(builder -> builder .author("YourName") .enableSwagger() // 开启Swagger注解 .dateType(DateType.TIME_PACK) // 时间类型使用java.time .outputDir(outputDir) .commentDate("yyyy-MM-dd") ) // 包配置 .packageConfig(builder -> builder .parent("com.example.demo") // 父包名 .moduleName("system") // 模块名 .entity("entity") .service("service") .serviceImpl("service.impl") .mapper("mapper") .xml("mapper.xml") .controller("controller") .pathInfo(Collections.singletonMap(OutputFile.xml, outputDir + "/xml")) ) // 策略配置 .strategyConfig(builder -> builder .addInclude("sys_user") // 需要生成的表,可多张 .addTablePrefix("sys_") // 去掉表前缀 .entityBuilder() .naming(NamingStrategy.underline_to_camel) // 下划线转驼峰 .enableLombok() // 启用Lombok .enableTableFieldAnnotation() // 字段添加@TableField .logicDeleteColumnName("deleted") // 逻辑删除字段 .versionColumnName("version") // 乐观锁字段,如果表里有 .controllerBuilder() .enableRestStyle() // 生成RestController .serviceBuilder() .formatServiceFileName("%sService") // Service文件名 .formatServiceImplFileName("%sServiceImpl") // 实现类文件名 .mapperBuilder() .enableMapperAnnotation() // Mapper加@Mapper注解 ) // 使用Velocity模板引擎 .templateEngine(new VelocityTemplateEngine()) .execute(); } }

这段代码跑起来,控制台会打印每一步的生成日志,最后在项目目录下看到generator-output文件夹,里面就是全套产物。注意路径信息中xml的outputDir单独配置,因为MyBatis-Plus默认把XML生成到java目录下,这在工程标准结构里不合适,所以手动指到resources目录。

4.2 运行结果长什么样

以sys_user表为例,生成的代码文件如下:

generator-output/ └── com/example/demo/system ├── controller/SysUserController.java ├── entity/SysUser.java ├── mapper/SysUserMapper.java ├── mapper.xml/SysUserMapper.xml ├── service/SysUserService.java └── service/impl/SysUserServiceImpl.java

实体类的核心部分大致长这样:

@Data @EqualsAndHashCode(callSuper = false) @TableName("sys_user") public class SysUser implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @TableField("username") private String username; // 其他字段省略 @TableField("create_time") private LocalDateTime createTime; @TableField("deleted") private Integer deleted; }

细节处理得不错,@TableId、@TableField、@TableName这些注解都自动加好了。Controller里更是直接把增删改查、分页查询的方法都写好了:

@RestController @RequestMapping("/sysUser") public class SysUserController { @Autowired private SysUserService sysUserService; @GetMapping("/page") public IPage<SysUser> page(Page<SysUser> page) { return sysUserService.page(page); } @GetMapping("/{id}") public SysUser get(@PathVariable Long id) { return sysUserService.getById(id); } @PostMapping public Boolean add(@RequestBody SysUser sysUser) { return sysUserService.save(sysUser); } @PutMapping public Boolean update(@RequestBody SysUser sysUser) { return sysUserService.updateById(sysUser); } @DeleteMapping("/{id}") public Boolean delete(@PathVariable Long id) { return sysUserService.removeById(id); } }

这块代码拿来就能用,日常管理后台的基本接口需求已经满足了。剩下的工作就是把默认的返回结构包装成自己项目的ResponseEntity风格,这个可以通过自定义模板解决,后面会讲。

4.3 集成到工程里的收尾工作

代码生成后,需要把生成的文件拷贝到正式工程目录里(或者直接把输出目录指向src/main/java)。还需要确保SpringBoot能扫描到Mapper接口,主要有两种方式:

  • 方式一:在启动类上加@MapperScan("com.example.demo.system.mapper")
  • 方式二:使用生成器里enableMapperAnnotation()生成的@Mapper注解

我项目里用的是启动类加@MapperScan,统一管理扫描路径,避免每个Mapper都加注解。如果你是面向团队的项目,推荐这种方式,规范更统一。

此外,Service实现类里的一个细节值得留意。如果项目里有公共字段填充需求(比如create_time、update_time自动填充),需要配置MyBatis-Plus的MetaObjectHandler。这个不算生成器问题,但属于配套工作:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); } }

配合实体类字段的@TableField(fill = FieldFill.INSERT)注解,插入和更新时时间字段就不用手动赋值了。

5. 自定义模板:把生成器调教成自己的风格

5.1 为什么需要自定义模板

官方默认模板属于通用型,能覆盖大多数场景,但在真实项目里总有几个“不顺眼”的地方。比如Controller返回类型不是项目的统一Result对象,实体类注释风格和团队规范不一致,或者Mapper XML里不需要生成那么多冗余的resultMap。

自定义模板的核心思路是:把官方模板文件拷贝出来,根据自己的需求修改,然后在生成器配置里指定自己的模板路径即可。整个过程不复杂,收益却很大,尤其适合团队推广。

5.2 模板文件放哪里、怎么写

Velocity模板文件一般放在项目resources目录下的templates文件夹,和生成器代码放一起。工程结构如下:

src/test/resources/templates/ ├── controller.java.vm ├── service.java.vm ├── serviceImpl.java.vm ├── mapper.java.vm ├── mapper.xml.vm └── entity.java.vm

我举一个Controller模板的例子,假设项目的统一返回结构是R类:

package ${package.Controller}; import org.springframework.web.bind.annotation.*; import com.example.demo.common.R; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import ${package.Service}.${table.serviceName}; import ${package.Entity}.${entity}; #if(${restControllerStyle}) import org.springframework.web.bind.annotation.RestController; #else import org.springframework.stereotype.Controller; #end @RestController @RequestMapping("/${table.entityPath}") public class ${table.controllerName} { @Autowired private ${table.serviceName} ${table.serviceName.substring(0,1).toLowerCase()}${table.serviceName.substring(1)}; @GetMapping("/page") public R<Page<${entity}>> page(Page<${entity}> page) { return R.ok(${table.entityPath}Service.page(page)); } @GetMapping("/{id}") public R<${entity}> get(@PathVariable Long id) { return R.ok(${table.entityPath}Service.getById(id)); } @PostMapping public R<Boolean> add(@RequestBody ${entity} ${table.entityPath}) { return R.ok(${table.entityPath}Service.save(${table.entityPath})); } @PutMapping public R<Boolean> update(@RequestBody ${entity} ${table.entityPath}) { return R.ok(${table.entityPath}Service.updateById(${table.entityPath})); } @DeleteMapping("/{id}") public R<Boolean> delete(@PathVariable Long id) { return R.ok(${table.entityPath}Service.removeById(id)); } }

然后在生成器配置里指定模板路径:

.templateConfig(builder -> builder .controller("/templates/controller.java.vm") .service("/templates/service.java.vm") .serviceImpl("/templates/serviceImpl.java.vm") .entity("/templates/entity.java.vm") .mapper("/templates/mapper.java.vm") .xml("/templates/mapper.xml.vm") )

模板文件路径默认从classpath根目录下解析,所以放到src/test/resources/templates下即可。官方模板文件可以从MyBatis-Plus Generator的源码包或GitHub仓库里获取原始文件,在此基础上修改比从零编写简单得多。

5.3 包名和路径生成的规律

在实际配置过程中,很多人在包名和路径这里犯迷糊。MyBatis-Plus生成文件的路径规则其实是固定的:

  • 默认父包:com.example.demo
  • 默认子包:entity、mapper、service、service.impl、controller
  • 实际输出:com/example/demo/entity(对应的磁盘路径是outputDir + com/example/demo/entity)

ModuleName参数的作用是引入中间层。比如parent是com.example.demo,moduleName是system,那么实体类包名就是com.example.demo.system.entity。这在多模块项目或后台分系统结构里很方便,不用手动去改一大坨包名。

如果你不想生成Service或Controller层,可以在策略配置中分别设置:

.serviceBuilder().disableService() .controllerBuilder().disable()

这种开关在只想要实体和Mapper的场景下特别实用,避免了生成一堆用不上的代码。

6. 常见问题与排查实录

6.1 表前缀处理不生效

这是被问得最多的问题之一。明明数据库表叫sys_user,配置了addTablePrefix("sys_"),生成的实体类还是叫SysUserPrefix或者直接报了SysUser找不到表,这类问题多半是配置顺序或者多个前缀导致。

要注意addTablePrefix是一个可变参数,可以传多个值,比如.addTablePrefix("sys_", "t_")。如果表名前缀不统一,配置就需要相应调整。另外还需要确认表名匹配是严格前缀匹配,不会做模糊替换,所以前缀字符串一定要写准确。

6.2 时间字段映射成了Date

如果你的MySQL驱动版本低于8.x,或者项目JDK版本低于Java 8,那么默认的时间映射可能是java.util.Date。解决思路两个:

第一,升级数据库驱动到mysql-connector-java 8.x。

第二,在globalConfig中显式指定日期类型:

.dateType(DateType.TIME_PACK)

TIME_PACK对应java.time包类型,这是新版生成器的默认值。如果还不行,检查是否在实体类里手动加了java.util.Date相关import,清理掉重新生成即可。

6.3 Mapper XML映射文件跑到java目录

这个问题比较隐蔽。默认配置下,XML文件生成的位置是包路径下的java目录,而不是resources目录。它在IDE里能正常显示,但打包阶段Maven不会把java目录下的XML复制到classes,导致运行时提示Invalid bound statement。

解决办法是在packageConfig里单独指定xml输出路径:

.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"))

这样XML会直接生成到resources/mapper目录下,并且记得在application.yml里配置Mapper XML路径:

mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml

6.4 代码覆盖风险与版本管理

生成器重复执行,默认会覆盖同名文件。这一点在团队协作时要格外注意,如果你改了生成后的文件再重新生成,改动会被直接覆盖掉。

我个人的习惯是:

  • 生成器输出目录独立,不要直接指向src/main/java。
  • 每次生成完,人工review差异后再拷贝到正式工程。
  • 生成器代码和模板文件提交到Git仓库,但把generator-output目录加入.gitignore。

这样既能享受生成器的效率,又能避免误覆盖问题。

6.5 逻辑删除字段与查询器冲突

如果你配置了逻辑删除字段,生成器的实体类会自动加上@TableLogic注解。这本身是好事,删除操作会自动变成update deleted=1。但有次我在代码里手动写了delete from sys_user,导致逻辑删除字段完全失效,数据被物理删除。这个不算生成器bug,但生成器生成的delete方法容易让人误以为底层还是物理删除。

建议在项目规范里明确:使用生成器生成的removeById、removeBatchByIds等系列方法操作数据,禁止手写物理删除SQL。这样逻辑删除的配置才真正落实。

7. 从效率到质量:生成器的长期价值

模板代码不是“傻代码”,它的真正价值在于把人的精力从重复劳动中解放出来,集中到业务规则和系统架构上。我在团队里推广这套方案后,新模块接入的时间从平均一天缩短到半天以内,剩下时间都花在业务字段和逻辑校验上,而不是和MyBatis映射较劲。

按照我个人的使用体会,最理想的工作流是这样的:先设计好数据库表结构,注释写清楚,然后跑一次生成器得到基础CRUD,再用自定义模板统一Controller返回结构,最后只写那些生成器覆盖不到的业务方法。

代码生成器不是银弹,但它能解决真实存在的大量重复工作。如果你正在做SpringBoot项目,并且Mapper和Service层还在手写,真心建议花半天时间把AutoGenerator配置好,长期来看这笔投入非常划算。

最后分享一个小技巧:生成器配置类本身也值得好好封装。把数据源信息抽到application.yml里,模板路径统一管理,这样不同环境切换时只改配置文件即可。生成器用好了,项目组的新人也能很快上手,减少很多重复性的指导成本。

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

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

立即咨询