1. 为什么还要自己写Mapper?先聊聊生成器的价值边界
先抛一个结论:mybatis-generator这玩意儿,不是让你彻底不写SQL,而是把最没有技术含量的那部分代码从你的工作量里抹掉。
我在实际项目里见过太多这样的场景:一个订单表二三十个字段,手写insert、update、selectByPrimaryKey,每个方法都是机械重复。字段多了还容易漏,漏一个字段运行时才报错,查半天发现是某个字段没写进映射。更尴尬的是,同事的代码风格各不相同,张三的Mapper里缩进用四个空格,李四的用两个Tab,代码评审的时候一半时间花在争论格式上。
上面这些痛点,恰好都是mybatis-generator(下文简称MBG)能解决的。
MBG是MyBatis官方出品的老牌代码生成器,从数据库表结构反向生成三层东西:实体类(Model)、Mapper接口、XML映射文件。你用一条命令,它就能把一张表对应的全套CRUD代码给你吐出来。如果你用的是MySQL,把表结构设计好之后,生成代码几乎是零成本的事情。
这篇文章写给谁?三种人:
- 刚开始接触Spring Boot + MyBatis,还在手动写增删改查的新手
- 项目里已经有不少表,想统一代码风格、提高开发效率的团队
- 听别人提过代码生成器,但没系统搞明白配置和坑点的人
2021版的MBG在配置和依赖上跟老版本有一些差异,网上很多教程都是基于1.3.x的老写法,直接拷贝过来会报错。这篇文章我会基于实际踩过的坑,从零到一把整个流程梳理清楚。
先说清楚边界:MBG适合生成单表的基础CRUD,它不擅长处理复杂的多表关联查询、动态SQL里那些业务味很重的逻辑。这类场景仍然需要你自己手写。所以正确的预期是——用生成器把80%的重复代码搞定,剩下20%的复杂查询自己在XML里补。
2. 准备工作:版本选择、依赖引入与工程结构
2.1 2021年这个时间点,版本怎么选
MBG的版本演进其实有一个比较大的分水岭:1.4.0版本对包结构做了调整。
1.3.x时代的包名是org.mybatis.generator,1.4.0之后,核心类迁移到了org.mybatis.generator.api下面的新包结构,同时指定了Java版本要求。2021年写新项目,我建议直接用1.4.0以上的版本,Maven仓库里最新的稳定版是1.4.0,之后还有1.4.1、1.4.2的小版本修复。
版本这块有个很容易踩的坑:你拿网上旧教程的配置去跑1.4.x,会发现某些配置节点的属性变了、某些类已经废弃了,报错信息还很隐晦,根本看不出是版本兼容问题。
我个人的建议组合是这样:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8及以上 | MBG 1.4.0之后强制要求JDK 1.8+ |
| mybatis-generator-core | 1.4.0或1.4.2 | 核心生成引擎 |
| mybatis-generator-maven-plugin | 1.4.0或1.4.2 | Maven插件方式运行 |
| mybatis | 3.5.x | 运行时依赖,MBG本身不强依赖具体版本 |
如果你用的是Spring Boot 2.4.x或2.5.x,内置的MyBatis Starter版本通常已经够用,不需要额外操心。
2.2 两种项目接入方式,推荐Maven插件
MBG的运行方式主要有三种:命令行、Maven插件、Java代码直接调用。2021年最主流、最推荐的方式就是Maven插件方式,因为它是声明式的,配置文件放在项目里,团队共享,任何人clone下来执行一条mvn mybatis-generator:generate就能复现同样的生成结果。
命令行方式适合不依赖构建工具的临时场景,Java代码方式适合把生成能力封装成自己的可视化工具。这两个后面会展开讲。
先看工程目录结构。假设一个标准的Spring Boot多模块或单模块项目,建议这样组织:
project-root/ ├── pom.xml ├── src/main/java/com/example/demo/ │ ├── entity/ // 实体类,生成的Model放这里 │ ├── mapper/ // Mapper接口 │ └── resources/ │ ├── mapper/ // XML映射文件 │ └── generator/ // MBG配置文件单独放一个目录 └── sql/ // 建表SQL脚本配置文件generatorConfig.xml放在src/main/resources/generator下,不要扔在src根目录跟application.yml混在一起,后面维护的时候好找。
2.3 pom.xml里需要加的东西
用Maven插件方式,只需要在pom.xml里引入插件。核心片段如下:
<build> <plugins> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.4.0</version> <configuration> <!-- 指定配置文件路径 --> <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile> <!-- 允许覆盖已有文件,默认false,如果体验生成功能建议先false --> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> <dependencies> <!-- 数据库驱动,这里以MySQL为例 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.25</version> </dependency> </dependencies> </plugin> </plugins> </build>有两个细节要特别提醒:
第一个,数据库驱动一定要在插件的<dependencies>里声明。很多人只在自己的工程依赖里加了JDBC驱动,结果跑插件时提示ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是Maven插件运行在独立的ClassLoader里,跟主工程的依赖不共享,必须在插件内部单独声明。
第二个,<overwrite>这个参数。设置为true时,每次生成会覆盖同名文件;设置为false时,遇到已存在的文件会跳过。我建议开发初期设为false,确认生成结果满意之后再用true,防止手滑把手工改过的代码覆盖了。
3. generatorConfig.xml核心配置逐项拆解
3.1 配置文件的结构总览
MBG的配置文件核心是<generatorConfiguration>根节点,里面通常包含这么几块:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- 1. 可选的properties文件,用于外部化配置 --> <properties resource="generator/config.properties"/> <!-- 2. context节点,一个context对应一套生成策略 --> <context id="mysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <!-- 3. 注释生成策略 --> <commentGenerator> <property name="suppressAllComments" value="true"/> </commentGenerator> <!-- 4. 数据库连接 --> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC" userId="root" password="123456"/> <!-- 5. 实体类生成策略 --> <javaModelGenerator targetPackage="com.example.demo.entity" targetProject="src/main/java"> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- 6. SQL映射文件生成策略 --> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <!-- 7. Mapper接口生成策略 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.demo.mapper" targetProject="src/main/java"/> <!-- 8. 表配置 --> <table tableName="user" domainObjectName="User"/> </context> </generatorConfiguration>上面这个配置是我精简过的核心版本,实际项目还可能配置<plugins>、<table>的列覆盖、日期类型转换规则等。下面逐个节点说明作用和容易被忽略的点。
3.2 连接信息:连接URL里的时区问题
<jdbcConnection>节点没什么玄机,就是数据库连接信息。但这里有个高频报错我几乎每次在群里都能看到:
The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone.这是MySQL 8.x驱动引入的严格时区校验导致的。老教程里用的是jdbc:mysql://localhost:3306/demo_db这种不带参数的写法,放到MySQL 8 + JDBC 8驱动下直接报时区错误。
解决办法是连接URL后面拼接参数:
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai" userId="root" password="123456"/>两个参数说明:
useSSL=false:本地开发环境没必要走SSL握手,加上能减少连接耗时,同时避免MySQL 8默认开SSL导致的警告日志。serverTimezone=Asia/Shanghai:显式告诉驱动使用东八区,避免服务器时区和驱动默认时区不一致导致的乱码或时间偏移。
还有一个细节:XML里&符号必须转义成&,这是XML语法的硬性要求,不转义的话解析会直接失败。
3.3 生成路径:targetProject的三种写法
<javaModelGenerator>和<sqlMapGenerator>都涉及targetPackage和targetProject两个属性,很多人一开始搞不清这两个属性的配合规则。
targetPackage指定的是包名或者目录名,targetProject指定的是相对于当前执行模块的路径。实际使用中有三种典型写法:
写法一:Maven工程,生成到src/main/java
<javaModelGenerator targetPackage="com.example.demo.entity" targetProject="src/main/java">这种写法直接生成到源码目录,生成完就能编译,是我最推荐的方式。
写法二:Maven工程,生成到独立的source目录
<javaModelGenerator targetPackage="com.example.demo.entity" targetProject="src/main/gen">src/main/gen是独立的生成目录,需要在pom.xml里把src/main/gen配置成build-helper-maven-plugin的额外source目录。好处是生成代码和手写代码物理隔离,重新生成的时候不会污染你的手工代码。
写法三:绝对路径
<javaModelGenerator targetPackage="com.example.demo.entity" targetProject="D:/workspace/demo/src/main/java">不推荐,换个环境路径就失效了,配置文件失去可移植性。
XML映射文件的写法略微不同:
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>这里的targetPackage填的是mapper,生成后对应src/main/resources/mapper/目录。如果你的XML文件规约放在resources/mapper下,这样写即可。
3.4 table节点:指定表名、实体名和列覆盖
<table>节点是核心中的核心,它决定了对哪张表生成代码。最简单的写法:
<table tableName="user"/>此时MBG默认用表名作为实体名,user表生成User.java,同时Mapper接口叫UserMapper,XML叫UserMapper.xml。
如果有特殊命名,比如表名带前缀t_user,你不想让实体类叫TUser,可以显式指定:
<table tableName="t_user" domainObjectName="User"/>domainObjectName指定实体类名,MBG会自动推导出UserMapper、UserMapper.xml这些衍生文件名。
还有个enableInsert、enableSelectByPrimaryKey之类的开关,用来控制单表生成的CRUD方法类别。默认全开,但有些表没必要生成insert,比如配置表只读,可以这么写:
<table tableName="config" domainObjectName="Config" enableInsert="false" enableUpdateByPrimaryKey="false" enableDeleteByPrimaryKey="false"/>保留selectByPrimaryKey和selectAll就够了。
3.5 注释生成策略:把默认注释关掉的关键作用
MBG默认会在生成的每个类、每个方法上生成一段注释,格式大致是:
/** * 表名:user * 表注释:用户表 * @author MyBatis Generator * @date 2021-08-15 */这段注释最大的问题在于——它带生成时间。如果你启用了版本管理,每次重新生成代码,即使表结构没变,生成文件的注释时间变了,Git里就会多出一堆无意义的diff。代码评审的时候很容易被追问:这次改动改了什么?答:重新跑了一次生成器。
解决方案就是<commentGenerator>里关掉默认注释:
<commentGenerator> <property name="suppressAllComments" value="true"/> </commentGenerator>suppressAllComments设为true后,所有自动注释直接不生成,干净利落。如果你还是想保留一些业务含义,可以自定义注释生成器(继承DefaultCommentGenerator重写方法),设置addRemarkComments属性,这个后面进阶部分展开。
4. 跑通整个流程:从建表到生成代码的完整实操
4.1 准备一张测试表
为了演示一个完整流程,我先建一张订单表:
CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `order_no` varchar(64) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '下单用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付 1-已支付 2-已取消', `remark` varchar(512) DEFAULT NULL COMMENT '备注', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这张表包含了自增主键、普通索引、DEFAULT值、ON UPDATE CURRENT_TIMESTAMP,涵盖了比较常见的MySQL表特性。
4.2 完整配置示例
针对这张表,完整的generatorConfig.xml是这样的:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <context id="mysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <commentGenerator> <property name="suppressAllComments" value="true"/> </commentGenerator> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai" userId="root" password="123456"/> <javaModelGenerator targetPackage="com.example.demo.entity" targetProject="src/main/java"> <property name="trimStrings" value="true"/> </javaModelGenerator> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.demo.mapper" targetProject="src/main/java"/> <table tableName="t_order" domainObjectName="Order"/> </context> </generatorConfiguration>这里说明两个关键选型:
targetRuntime="MyBatis3Simple":这个选项很关键,它生成的方法数量远少于默认的MyBatis3,只生成countByExample、deleteByPrimaryKey、insert、selectByPrimaryKey、updateByPrimaryKey、selectAll这些基本方法,不生成基于Example的复杂查询方法。实际项目中大部分单表操作用Simple就够了,代码量少、可读性强。defaultModelType="flat":这个选项让每张表只生成一个实体类,不会再拆出Key类、Criteria类。默认的conditional模式遇到联合主键会额外生成复合主键类,徒增复杂度。没有特殊需求就用flat。
4.3 执行生成命令
在项目根目录执行:
mvn mybatis-generator:generate如果你在pom.xml里有多个插件配置、或者有多个profile,可以指定:
mvn -Pdev mybatis-generator:generate -Dmybatis.generator.configurationFile=src/main/resources/generator/generatorConfig.xml首次执行建议加上-X参数打印完整调试信息:
mvn mybatis-generator:generate -X这样如果配置有问题,你能看到完整的异常堆栈,而不是被Maven吞掉只有一行报错的提示。
生成成功后,控制台输出类似:
[INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.257 s [INFO] ------------------------------------------------------------------------4.4 生成结果长什么样
生成完成后,目录结构如下:
src/main/java/com/example/demo/ ├── entity/ │ └── Order.java └── mapper/ └── OrderMapper.java src/main/resources/mapper/ └── OrderMapper.xml看一眼实体类的核心代码:
package com.example.demo.entity; import java.math.BigDecimal; import java.util.Date; public class Order { private Long id; private String orderNo; private Long userId; private BigDecimal totalAmount; private Integer status; private String remark; private Date createdAt; private Date updatedAt; // 省略getter/setter }注意几点:decimal(10,2)映射成BigDecimal,datetime映射成Date,tinyint映射成Integer,bigint映射成Long。这是JDBC驱动的默认映射规则,合理且符合直觉。
再看Mapper接口:
package com.example.demo.mapper; import com.example.demo.entity.Order; public interface OrderMapper { int deleteByPrimaryKey(Long id); int insert(Order row); Order selectByPrimaryKey(Long id); java.util.List<Order> selectAll(); int updateByPrimaryKey(Order row); }干净利落。insert和updateByPrimaryKey默认带入所有字段(包括null字段),如果你希望只操作非空字段,后面还有insertSelective和updateByPrimaryKeySelective,可以通过<table>节点里的insertStatementSupportsSelectKey或切换到MyBatis3运行时获得。
XML文件的内容不必全贴,核心结构是有ResultMap、各个SQL语句的完整映射。重点说一个我在实际项目里几乎每次都要改的点:updateByPrimaryKey默认是更新所有字段,包括null。如果你用前端传来的对象直接做更新,表单里没填的字段会被置空。所以很多团队会改成手写updateByPrimaryKeySelective,这只更新非空字段。这个差异初看不明显,上线后就是数据丢失事故级别的坑。
5. 生成代码如何融入Spring Boot项目
5.1 Mapper扫描配置
Spring Boot项目里,需要让容器知道Mapper接口在哪。两种方式:
方式一:启动类上加@MapperScan
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }方式二:每个Mapper接口上加@Mapper
@Mapper public interface OrderMapper { // ... }推荐方式一,一个注解搞定所有Mapper,不用每个接口重复标注。
5.2 XML映射文件的位置约定
MBG生成的XML在src/main/resources/mapper,Spring Boot默认不扫描这个目录。需要配置MyBatis的mapper-locations属性。
如果用的是mybatis-spring-boot-starter,在application.yml里:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity这里有个坑:如果忘了配置mapper-locations,运行时调用Mapper方法会报Invalid bound statement (not found)。这个报错信息非常常见,半数新手都踩过。排查的第一件事就是确认XML扫描路径是否配置正确。
5.3 Service层怎么用生成好的Mapper
写一个演示用的Service:
@Service public class OrderService { @Autowired private OrderMapper orderMapper; public Order getById(Long id) { return orderMapper.selectByPrimaryKey(id); } public List<Order> getAllOrders() { return orderMapper.selectAll(); } public void createOrder(Order order) { orderMapper.insert(order); } }5.4 实体类的扩展注意点
生成的实体类跟数据库表字段一一对应,但业务上经常需要额外字段。比如订单列表页要展示用户名,你可能会在Order实体里加一个userName字段。
我强烈建议:不要直接改生成的实体类文件,而是新建一个VO类继承或聚合它。
原因很简单:生成器是随时可能重跑的。如果表结构加了字段,你重跑一次MBG,Order.java会被覆盖(overwrite=true时),你手加的字段和对应的getter/setter全没了。即使overwrite=false,生成器检测到文件存在会跳过,你依然得不到新字段。最稳妥的方案是:
- 实体类
Order保持和表结构严格对应 - 扩展字段放进
OrderVO、OrderQuery这类专门的类 - 复杂查询直接用
@Select注解写在Mapper接口里,或者手写XML
这样重跑生成器的代价几乎为零。
6. 高频报错与排查链路:我遇到的坑都在这了
6.1 坑一:表名带前缀,生成的实体名不符合预期
这个问题本质上是MBG的表名到类名的转换规则完全依赖domainObjectName的显示配置。没有配置时,t_order会被转换成一个很奇怪的TOrder。为什么会这样?因为MBG的表名转换算法只是简单地把下划线去掉、首字母大写,t_order去掉下划线就是Torder,再校正首字母大写得到TOrder。
我见过有人的解决方案是在表名上加反引号,或者写tableName="t_order"然后忍受TOrder的类名。这显然不够优雅。
正确的做法就一句:每个<table>都显式写domainObjectName。一张表一行配置,清晰可控。
6.2 坑二:再次生成时,业务代码被覆盖
MBG的overwrite参数只在Maven插件的<configuration>里配置,但很多人不知道它仅对XML映射文件生效,Java文件不会因为overwrite=true而强制覆盖——MBG对Java文件的保护策略是:如果同名文件已存在,默认跳过,而不是覆盖。
这个设计让很多人产生误解。实际行为是这样的:
- XML映射文件:overwrite=true时直接覆盖
- Java文件:即使overwrite=true,如果同路径文件已存在,生成器会跳过并写日志
所以重跑生成器,你的Java实体类如果手工改过,不会丢。但这也是隐患——表结构变了之后重跑,实体类如果没有被更新,你手动加的新字段、新接口里用的新属性就会编译报错。这时候要手动删掉旧文件再跑一次。
我个人的工作流是:表结构变化后,先手工对比哪些文件是纯生成物,把对应的实体类、Mapper接口、XML文件删掉,再重跑生成。虽然多了一步,但是心里有底,不会出现新旧代码残留的脏状态。
6.3 坑三:JDBC驱动版本和数据库版本不匹配
这个问题在升级MySQL 8后尤其典型。你的数据库是MySQL 8.0.x,但pom里驱动还是5.1.47,跑生成器会报连接失败或驱动类找不到。
驱动类名变了:MySQL 5.x驱动类是com.mysql.jdbc.Driver,MySQL 8.x是com.mysql.cj.jdbc.Driver。配置里如果写的是旧的类名,MBG会直接ClassNotFound。
另外一个相关问题是驱动只能在插件里用,如果插件的<dependencies>漏了驱动依赖,报错也是ClassNotFoundException: com.mysql.cj.jdbc.Driver。这两个地方都排查一下。
6.4 坑四:表注释里的特殊字符导致XML解析失败
MySQL表注释里如果写了emoji或者其他特殊字符,MBG读取后写入XML注释时可能产生编码问题。有些团队的表注释写着写着就带上了火星文,生成完的XML文件用IDE打开是乱码,然后编译报错。
解决办法:生成前确保数据库连接的URL里配置了characterEncoding=utf8,同时generatorConfig.xml文件本身保存为UTF-8编码。还有一点,如果是Windows环境,注意有些编辑器默认保存为GBK,也要统一转成UTF-8。
6.5 坑五:想用的字段被生成了,不想用的字段也生成了
表结构是DBA统一设计的,但不同业务方用到的字段不一样。比如user表有个internal_remark字段是内部运营备注,普通业务方根本不该碰。
MBG的<table>节点可以配置列的包含/排除:
<table tableName="user" domainObjectName="User"> <ignoreColumn column="internal_remark"/> </table>加了<ignoreColumn>后,生成的实体类、Mapper、XML里完全不会出现这个字段。适合做API层接口隔离的场景,直接从源头杜绝误用。
7. 进阶优化:让生成结果更贴合团队需求
7.1 targetRuntime怎么选:MyBatis3 vs MyBatis3Simple
前面提了一嘴,这里展开讲透。这两个运行时生成的代码风格差异很大:
| 特性 | MyBatis3 | MyBatis3Simple |
|---|---|---|
| 生成方法数 | 很多,包含Example系列 | 精简,基础CRUD |
| 是否有Example类 | 是 | 否 |
| 单表动态查询 | 通过Example实现 | 不支持 |
| 代码量 | 膨胀 | 精简 |
| 适用场景 | 复杂查询需求多的项目 | 以单表普通CRUD为主 |
我的判断标准很简单:如果你刚开始做项目,先上MyBatis3Simple。等确实出现了需要动态条件查询的表,再单独把那张表生成两次——可以针对同一张表加两个<table>节点,一个用Simple运行时,另一个用MyBatis3运行时,分别生成不同命名的实体和Mapper,灵活得很。
但大多数情况下,用Simple + 手写扩展XML就够了。Example这个东西,写起来绕,读起来也绕,团队协作成本高。
7.2 自定义注释生成器,让生成的代码带上自己的规范
如果你不想完全关掉注释,又不想每次生成都产出一堆时间戳diff,可以写一个自定义的CommentGenerator。
思路是继承DefaultCommentGenerator,重写addComment和addJavaFileComment方法:
package com.example.demo.generator; import org.mybatis.generator.api.CommentGenerator; import org.mybatis.generator.api.IntrospectedColumn; import org.mybatis.generator.api.IntrospectedTable; import org.mybatis.generator.internal.DefaultCommentGenerator; import org.mybatis.generator.internal.util.StringUtility; import java.util.Properties; public class CustomCommentGenerator extends DefaultCommentGenerator { @Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); } @Override public void addFieldComment(org.mybatis.generator.api.dom.java.Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) { // 直接使用数据库字段注释 if (introspectedColumn.getRemarks() != null && !introspectedColumn.getRemarks().isEmpty()) { field.addJavaDocLine("/**"); field.addJavaDocLine(" * " + introspectedColumn.getRemarks()); field.addJavaDocLine(" */"); } } }然后在generatorConfig.xml里指定:
<commentGenerator type="com.example.demo.generator.CustomCommentGenerator">这样一个字段注释会直接取数据库表的COMMENT,生成的实体类可读性大幅提升,而且不含时间戳,不会产生无意义的Git diff。
7.3 接入Lombok,让实体类瘦身
默认生成的实体类每个字段都带一对getter/setter,一个20个字段的表,光getter/setter就一百多行。如果你的团队在用Lombok,可以让生成的实体类只保留字段声明,类上打@Data注解。
实现方式有两种:
方式一:用MBG自带的lombok扩展点。在<context>里配置:
<plugin type="org.mybatis.generator.plugins.LombokPlugin">这个插件需要你自己实现,核心是重写modelBaseRecordClassGenerated方法,在生成的类上添加@Data注解,并抑制getter/setter的生成。
方式二:用第三方的mybatis-generator-lombok-plugin,网上有现成的开源项目。引入后只需要:
<plugin type="com.softwareloop.mybatis.generator.plugins.LombokPlugin">我的建议是:如果你愿意折腾,自己写一个也就二三十行代码;不想折腾,直接引入现成的插件。
7.4 集成到Maven生命周期:一键生成,省心省力
默认情况下生成器需要手动执行mvn mybatis-generator:generate,如果项目有CI/CD,可以把生成器绑定到generate-sources阶段:
<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.4.0</version> <executions> <execution> <id>generate-mybatis-code</id> <phase>generate-sources</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin>这样每次mvn compile或mvn package时自动执行生成,配合overwrite=true,代码始终与表结构同步。
但我不建议无脑这么做,原因还是前面说的:自动覆盖会让手工改动在编译时悄然消失。更稳妥的做法是:表结构变更后手动触发生成,生成结果提交到Git,code review后合并。这和我前面说的“删掉旧文件再重跑”的思路一致。
7.5 与MyBatis-Plus、tk.mybatis的取舍
到了2021年,市面上已经有MyBatis-Plus这类增强框架,自带BaseMapper,单表CRUD连生成XML都省了。那还有必要用MBG吗?
我自己的看法是:看团队情况。
MyBatis-Plus优势在于封装更彻底,单表CRUD完全零代码,甚至分页插件、逻辑删除都内置了。但它的问题也很明显:一旦你发现自己需要精细控制SQL,它的实体注解体系会带来额外的理解成本;而且它的XML能力依然要自己写Mapper.xml,只是多了一个BaseMapper的自动CRUD兜底。
MBG的优势在于生成的是标准MyBatis代码,透明可修改,没有任何框架魔法。对于要求SQL完全可控、团队风格统一、需要代码评审看了心里踏实的团队,MBG更合适。
还有一个更实际的角度:如果团队已经用MyBatis-Plus写了不少业务代码,这时候引入MBG反而会让代码风格割裂,一半是BaseMapper自动CRUD,一半是生成的手写CRUD,维护成本反而升高。技术选型这事,没有绝对的对错,只有适不适合当下的团队和项目阶段。
8. 命令行和Java API方式:补充两种运行姿势
8.1 命令行方式:解耦Maven,快速试用
有时候你只是想快速生成几个文件看看效果,不想动pom.xml,命令行方式最合适。需要先下载jar包:
wget https://repo1.maven.org/maven2/org/mybatis/generator/mybatis-generator-core/1.4.0/mybatis-generator-core-1.4.0.jar还需要MySQL驱动jar包,然后执行:
java -cp mybatis-generator-core-1.4.0.jar:mysql-connector-java-8.0.25.jar \ org.mybatis.generator.api.ShellRunner \ -configfile generatorConfig.xml -overwriteWindows用户把冒号换成;。需要同目录下有generatorConfig.xml文件。
命令行方式适合本机快速试验,但对团队协作不友好,配置文件、jar包版本容易各自为战。
8.2 Java API方式:可以封装成自己的生成工具
如果你的团队有统一开发平台,想提供一个网页端或IDE插件的生成入口,可以用Java API方式。核心代码:
package com.example.demo.generator; import org.mybatis.generator.api.MyBatisGenerator; import org.mybatis.generator.config.Configuration; import org.mybatis.generator.config.xml.ConfigurationParser; import org.mybatis.generator.internal.DefaultShellCallback; import java.io.File; import java.util.ArrayList; import java.util.List; public class GeneratorRunner { public static void main(String[] args) throws Exception { List<String> warnings = new ArrayList<>(); boolean overwrite = true; File configFile = new File("src/main/resources/generator/generatorConfig.xml"); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null); for (String warning : warnings) { System.out.println(warning); } } }这个入口可以做成一个main方法直接跑,也可以做成Spring Boot的CommandLineRunner。实际项目中我曾经把它封装成一个内部工具:开发者在网页上勾选表名、填好包名,后端调用这串代码生成ZIP包下载。上手难度不大,但省了大家挨个配Maven插件的功夫。
9. 从我实际使用MBG两年半的经验谈几点感受
回到开头那个问题:为什么还要自己写Mapper?
答案其实很朴素——重复的活需要自动化,但自动化的边界要清晰。
MBG这两年半用下来,我的感觉是它最适合的定位是项目初期的“脚手架加速器”和“代码风格统一器”。一个新项目几十张表,手写一遍CRUD要两三天,用MBG五分钟搞定,而且每张表的代码结构一模一样,新同事接手几乎没有学习成本。
但也要认清它的短板。遇到多表关联、复杂子查询、需要深度优化的分页SQL,MBG生成的代码帮不上忙,这些还是得自己动手。甚至它的Example机制在复杂场景下会变成负担——为了查一个简单条件,你得创建Example、创建Criteria,代码绕得不行。
我自己的实践习惯是这样的:
- 表结构交付后,立即用MBG生成基础代码,作为项目的地基
- 实体类严格保持与表结构一致,扩展字段一律放VO
- XML映射文件里的手工SQL用注释块区分开,标上“手工维护”四个字,避免误覆盖
- 每次重跑生成器之前,先看一眼Git工作区有没有未提交的手工改动,再决定要不要清理旧文件
- 生成结果必须走代码评审,因为表结构注释乱、字段命名不规范这些问题,会在生成代码里原样暴露
最后分享一个小技巧:如果数据库表字段注释写得规范,生成的代码可读性会大幅提升。所以与其吐槽MBG生成的东西丑,不如先花点时间把表注释维护好。一个注释清晰的user表,生成的User.java读起来跟手写的一样顺眼。工具只是放大了你的输入质量,这个道理在代码生成器上体现得尤其明显。