做 Java 后端这几年,MyBatis 逆向工程是我用得最多、又最容易被忽略的东西。说它常用,是因为每开一个新项目,只要数据库表设计好了,第一步往往就是把它生成成实体类、Mapper 接口和 XML 映射文件;说它容易被忽略,是因为大多数人跑完 mybatis-generator 之后,就把配置文件扔到一边,直到下次加表才想起来还有这么个东西。
我见过太多团队的生成结果,默认配置跑一遍,一个 user 表就能给你吐出十来个文件,Example 类、BaseResultMap、带一堆 where 条件的查询方法……目录倒是整齐,代码却越来越厚,真正有用的其实就那五六个方法。后来我慢慢摸出一套“清晰简洁版”的玩法:生成的代码刚好够用,不堆废料,还方便二次改造。这篇文章就把这套方案完整拆给你看,从配置思路到踩坑实录都摆出来,适合刚接触 MyBatis 的初学者,也适合被默认生成折腾到头大的老手。
1. 先聊清楚:为什么绝大多数项目的逆向工程都搞复杂了
1.1 逆向工程到底解决了什么问题
说句实话,MyBatis 逆向工程不是一个高深的技术,它解决的就是一个纯体力活问题。你建了一张表,里面有二十个字段,按照 MyBatis 的用法,你要写一个实体类、一个 Mapper 接口、一个 XML 映射文件,XML 里还要把 insert、update、delete、select 四个基础操作写全,每个操作都要把二十个字段映射一遍。这还只是一张表,项目里随便三四十张表,就是上千行重复代码,写的时候注意力稍不集中,漏了个字段,运行起来才发现,非常难受。
逆向工程做的就是把“数据库表”转换成“Java 代码”的自动化工序。它先把表结构读出来,字段名、类型、主键、注释全拿到,然后根据一套模板规则,生成实体类、Mapper 接口和对应的 XML 文件。人力写的代码和机器生成的代码最大的区别在于:机器不会累,不会漏字段,生成的 SQL 语句 99% 都符合规范写法。这也是为什么即便有人觉得逆向工程“不够灵活”,我也依然建议团队用,因为单表的基础 CRUD 本来就不应该浪费人工。
1.2 主流的三种做法和它们的适用边界
现在做 Java 的人,提到逆向工程基本绕不开三个方向,我画个对比你就能看清楚差异:
| 方案 | 生成物 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| MyBatis Generator(MBG) | 实体类、Mapper 接口、XML、Example | 官方维护、支持定制、逐年稳定 | 默认产物偏重,需精简配置 | 传统 MyBatis 项目、需要手写 XML 扩展 |
| MyBatis-Plus Generator | 实体类、Mapper、Service、ServiceImpl、Controller | 生成面广,自带 BaseMapper 通用 CRUD | 绑定 MyBatis-Plus 体系,自定义倾向受限 | 使用 MyBatis-Plus 的新项目 |
| 自己写模板引擎 | 任意代码 | 最自由,完全贴合公司规范 | 需要维护模板代码,成本最高 | 大厂规范化程度很高的团队 |
我这里聊的“清晰简洁版”走的是第一条路,用官方 MyBatis Generator,但对配置做减法。为什么不直接选 MyBatis-Plus Generator?因为很多项目还用的是原生 MyBatis 加 XML 映射,没引入 MyBatis-Plus;即便引入了,一旦用了它那套 ServiceImpl 模板,后续想在 XML 里做复杂 SQL 定制,绕来绕去反而别扭。官方 MBG 最稳,配置最透明,要对它做精简也最容易。
1.3 简洁与完整的边界在哪里
很多人把“生成得多”和“生成得全”划等号,这是一个误区。MBG 默认的 MyBatis3 运行模式会生成 Example 类,这个类的作用是帮你构建动态 where 条件,比如 where username like ? and status = ?。听上去很强大,但对大多数业务来说,查询条件都是写在 Service 或者 XML 里面的,Example 那一套复杂条件很少能直接用上。反而因为它多出来,一个表对应一套 Example,几十张表就是几十个没用的类,编译时间变长,代码 review 成本变大。
“清晰简洁版”的思路就一句话:只生成一定会用到的单表 CRUD,关掉所有用不上的 EnABle 开关,不让生成器往项目里塞废料。我见过精简过后,一张表就从原来十几个文件变成四个:实体类、Mapper 接口、XML 文件,以及可选的 Example(前提是你确实需要动态条件)。后面你会看到,这个精简过程其实就是几个开关的事情,难度并不高。
2. 动手前的准备:环境、依赖与两种运行姿势
2.1 基础环境清单
我假设你用的是常见的 Spring Boot 技术栈,下面这套环境是我在实际项目里用得最多的组合:
- JDK 8 或更高版本(Spring Boot 3 项目反正也是 JDK 17 起跳)
- Maven 3.6 以上,如果是 Gradle 项目,思路一样,只是插件的写法不同
- MySQL 8.0 或 5.7,示例默认用 MySQL,其他数据库比如 PostgreSQL、Oracle 需要换驱动和 URL 参数
- 一个能看表结构的数据库客户端,Navicat、DataGrip、命令行都行
这里有个容易忽略的点:MBG 本身只是一个代码生成工具,它和你项目里跑的 MyBatis 版本不强绑定。但如果你用了比较新的 JDK,建议用 1.4.x 版本的 mybatis-generator-maven-plugin,老版本 1.3.x 在某些环境下会碰到反射相关的兼容问题。
2.2 Maven 插件还是 Main 方法,两种方式各有什么价值
运行 MBG 有两种主流姿势。第一种是在 pom.xml 里配置 mybatis-generator-maven-plugin,执行命令 mvn mybatis-generator:generate,这也是我推荐大多数团队使用的方式。好处是配置文件放在项目里,任何一个同事拉下来代码都能一键生成,且生成器的版本跟着项目走,不会出现你本机插件版本和大家不一样的问题。
第二种是写一个 main 方法,内部调用 MBG 的 ShellRunner 或者直接使用 XMLConfigBuilder 去解析 generatorConfig.xml。这种方式适合你要把“生成代码”集成到自己的构建流程里,比如先解析数据库表,再做特殊处理,最后生成代码。虽然少了命令行这一步,但实际上只多了一层 Java 封装,核心动作还是那回事。
顺便提一个很多人会在面试里被问到的东西:XMLConfigBuilder。MyBatis 在初始化的时候,就是通过 XMLConfigBuilder 去解析 mybatis-config.xml,把 environment、mapper、typeAlias 这些配置吃进 Configuration 对象里。MBG 解析 generatorConfig.xml 用的也是类似思路,都是把 XML 节点映射成配置对象。所以你理解了 XMLConfigBuilder 的工作流程,反过来看 generatorConfig.xml 的结构会非常亲切,它无非就是把“配置解析”这件事做到另一个领域去了。
2.3 项目里的依赖与插件配置怎么写
我用的是 Maven 项目,pom.xml 里需要加入生成插件,版本选择 1.4.2 是我验证过比较稳的组合。配置片段如下:
<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.4.2</version> <configuration> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> <dependencies> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> </dependencies> </plugin>注意 true 的含义是:如果生成的文件已经存在,直接覆盖。这是一个双刃剑,后面我会在讲“保护手写代码”的时候专门展开。这里先记住,现阶段我们追求的是“干净生成”,先不纠结覆盖问题。插件里还必须把数据库驱动放进去,否则 MBG 连不上数据库,这个依赖不能省,也不要指望项目本身的依赖能传递进来,因为插件是在独立环境中运行的。
3. 核心配置逐项拆解(清晰简洁版的关键)
3.1 generatorConfig.xml 的整体结构认知
MBG 的配置文件名字默认叫 generatorConfig.xml,它遵循一个固定的 DTD 结构。你可以把它理解成一个四层结构:最外层是 generatorConfiguration,里面放一个或多个 context,每个 context 代表一套生成环境;context 下面有 jdbcConnection(数据库连接)、javaModelGenerator(实体类生成规则)、sqlMapGenerator(XML 生成规则)、javaClientGenerator(Mapper 接口生成规则),最后是一个或多个 table 节点。
这套结构看起来条条框框很多,其实拆开看就几个作用:告诉生成器“连哪个库、生成什么、生成到哪、怎么生成”。我最开始用 MBG 的时候也被这么多标签吓到过,后来发现你真正需要精调的标签不超过十个。下面我把关键参数逐一说明。
3.2 targetRuntime 这个参数决定了代码的“胖瘦”
context 节点的 targetRuntime 是精简配置的第一个核心开关。它有两个常用值:MyBatis3 和 MyBatis3Simple。
默认的 MyBatis3 模式生成的东西非常全,包括前面说的 Example 类,还包括 BaseResultMap、Base_Column_List 这些基础片段。MyBatis3Simple 模式就清爽得多:只生成实体类、Mapper 接口和 XML,而且接口里只带 selectByPrimaryKey、selectAll、insert、updateByPrimaryKey、deleteByPrimaryKey 这几个方法,不生成 Example。
“清晰简洁版”我建议优先考虑 MyBatis3Simple。如果你确实需要 Example,那也建议用 MyBatis3 模式并在 table 节点把 enableSelectByExample、enableUpdateByExample、enableDeleteByExample、enableCountByExample 全部配成 false,只保留单表的基础方法。这样做的好处是保留 Example 的生成能力,但不让它出现在最终代码里,灵活性更高。
| 对比项 | MyBatis3 | MyBatis3Simple |
|---|---|---|
| Example 类 | 生成 | 不生成 |
| 生成方法数 | 十几个,含各种 ByExample | 5 个核心方法 |
| 代码体积 | 较臃肿 | 干净 |
| SQL 定制空间 | 有 | 有 |
| 适用场景 | 复杂查询多、需要动态条件 | 常规单表 CRUD |
3.3 三个生成器节点的关键属性
javaModelGenerator 控制实体类的生成规则,重点是 targetPackage 和 targetProject。targetPackage 是包名,比如 com.example.domain;targetProject 是代码根目录,Maven 项目通常是 src/main/java。我一般还会设置 trimStrings 为 true,它的作用是生成 setter 方法时自动去除字符串两端的空格,这个看似不起眼的配置,在数据清洗上经常能帮你兜底。注意 MBG 文档里写得很清楚,这个属性要放在 context 下而不是 javaModelGenerator 的 property 里,放错位置不生效。
sqlMapGenerator 控制 XML 文件的生成规则,targetPackage 和 targetProject 跟上面同理。如果你不想把 XML 和 Java 代码混在一起,Maven 项目可以单独放到 src/main/resources/mapper 下,targetProject 写成 src/main/resources,targetPackage 写成 mapper,这样资源目录结构更清晰。
javaClientGenerator 控制 Mapper 接口的生成规则,type 属性有三个值:XMLMAPPER、ANNOTATEDMAPPER、MIXEDMAPPER。XMLMAPPER 是标准做法,接口方法对应到 XML 文件里的 SQL;ANNOTATEDMAPPER 把 SQL 直接写在注解里,不生成 XML;MIXEDMAPPER 则是基础操作走注解,复杂操作走 XML。我在实际项目里基本只用 XMLMAPPER,原因很简单:SQL 集中管理,且后续要加复杂 SQL 时,XML 的扩展能力比注解强太多。
3.4 table 节点和列映射的细节
table 节点是每个数据库表一个,最基础的两个属性是 tableName(表名)和 domainObjectName(实体类名)。MBG 默认能把 account_info 这样的表名转换成 AccountInfo 类名,但对于一些命名不规范的表,domainObjectName 就是救命的,比如某张表叫 t_order_detail,自动生成的实体类名可能不符合你的规范,我一般都会显式指定。
table 节点下还有两个常用的子节点。columnOverride 用于对某一列做覆盖映射,比如数据库列 type 是 TINYINT,但你的 Java 实体希望用 Integer 接收,而不是默认生成的 Byte,就可以在 columnOverride 里指定 javaType。ignoreColumn 用来忽略某些列,典型的场景是逻辑删除标志 deleted、乐观锁版本号 version 这类列,不需要参与普通的 CRUD 生成,直接忽略掉,避免业务上误更新。
另外容易忽略的是注释生成。MBG 默认不生成字段注释,生成的实体类里看不到数据库表注释和列注释。要解决这个,可以自己实现 CommentGenerator 类,在 addFieldComment 方法里把数据库列注释 set 到 Swagger 注解或 Javadoc 上。我在下面“实战”部分给你一个可直接抄的简易实现思路。
3.5 一套“清晰简洁版”的标准配置模板
下面这个配置是我现在最常用的模板,你可以直接抄走改改连接信息就能用:
<?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"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <property name="javaFileEncoding" value="UTF-8"/> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" userId="root" password="root"> </jdbcConnection> <javaModelGenerator targetPackage="com.example.domain" targetProject="src/main/java"> <property name="trimStrings" value="true"/> </javaModelGenerator> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper" targetProject="src/main/java"/> <table tableName="user_info" domainObjectName="UserInfo"> <generatedKey column="id" sqlStatement="MySql" identity="true"/> </table> </context> </generatorConfiguration>这里的 defaultModelType 设置为 flat,意思是实体类只生成一个,不带 xxxExample 的复杂模型。beginningDelimiter 和 endingDelimiter 设置成反引号,是为了防止表名或者字段名撞上 MySQL 关键字,这个坑我后面会细说。generatedKey 子节点表示主键是数据库自增,生成的 insert 语句会自动带上 useGeneratedKeys="true",插入后实体类的 id 会被回填。
4. 完整实操:从 user_info 表到一套能跑的 CRUD
4.1 先准备一张演示表
为了演示,我建一张最常见的用户表,字段尽量覆盖常用的类型:
CREATE TABLE `user_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(64) NOT NULL COMMENT '用户名', `nick_name` varchar(64) DEFAULT NULL COMMENT '昵称', `email` varchar(128) DEFAULT NULL COMMENT '邮箱', `status` tinyint DEFAULT '1' COMMENT '状态:1-正常 0-禁用', `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';表结构里有几个值得注意的点:id 是自增主键,status 是 tinyint,created_at 和 updated_at 是 datetime。生成之前我就要想好,实体类里对应时间字段用什么 Java 类型。默认生成器很可能会给 datetime 映射成 java.util.Date,但我的项目用的是 Java 8 时间 API,所以我希望生成的是 LocalDateTime。这个其实不是 generatorConfig.xml 配置能解决的,而是依赖 MyBatis 的 JSR310 支持,后面我会在“二次改造”部分说明。
4.2 运行生成命令
配置文件放在 src/main/resources/generatorConfig.xml 后,在项目根目录执行:
mvn mybatis-generator:generate正常情况下你会看到如下日志:
[INFO] Connecting to the database [INFO] Introspecting table user_info [INFO] Generating UserInfo.java [INFO] Generating UserInfoMapper.java [INFO] Generating UserInfoMapper.xml [INFO] SUCCESS!如果看到 Failed 或者 Exception,百分之八九十是数据库连接串、驱动依赖或者表名写错,用正常报错排查即可。
4.3 生成结果逐文件解读
生成完毕后,项目里会多出这几个文件:
src/main/java/com/example/domain/UserInfo.java src/main/java/com/example/mapper/UserInfoMapper.java src/main/resources/mapper/UserInfoMapper.xmlUserInfo.java 是一个典型的 POJO,每个字段对应一个私有属性,标配 getter/setter。因为开着 trimStrings,username 和 nick_name 这两个 String 字段的 setter 里会多出 trim 操作。这个实体类接下来可以直接放进 JPA 风格的库表模型思维里使用,也可以继续加 Swagger 注解和 Validation 注解,不影响运行。
UserInfoMapper.java 是接口,MyBatis3Simple 模式只会给这么几个方法:
public interface UserInfoMapper { int deleteByPrimaryKey(Long id); int insert(UserInfo row); UserInfo selectByPrimaryKey(Long id); List<UserInfo> selectAll(); int updateByPrimaryKey(UserInfo row); }没有多余的 Example 重载,一眼就能看出来这个 Mapper 能做什么,这种“可读性”就是我坚持精简的最大理由。
UserInfoMapper.xml 是重点。打开后你会发现它包含一个 resultMap、一个 Base_Column_List 片段,以及五个 SQL 语句。resultMap 明确把数据库字段和实体属性做了映射,比如 nick_name 映射到 nickName。insert 语句被配置成 useGeneratedKeys="true",主键自动回填;updateByPrimaryKey 语句会更新所有非主键字段。这里有个隐含细节:MyBatis3Simple 模式如果表里有 updated_at 字段,更新的时候会连同 updated_at 一起更新成 Java 实体里的值,而不是让数据库自己刷新 CURRENT_TIMESTAMP,这容易踩坑,我放在问题章节细聊。
4.4 接入 Spring Boot 并跑通基础调用
在 Spring Boot 里使用这个 Mapper,只需在启动类上做 @MapperScan 扫描,然后在 Service 里注入:
@Service public class UserInfoService { private final UserInfoMapper userInfoMapper; public UserInfoService(UserInfoMapper userInfoMapper) { this.userInfoMapper = userInfoMapper; } public UserInfo getUserById(Long id) { return userInfoMapper.selectByPrimaryKey(id); } public UserInfo createUser(UserInfo userInfo) { userInfoMapper.insert(userInfo); return userInfo; } }跑一个单元测试,插入一条记录,再查询出来,整个链路就通了。到这里,“从表到接口”的流程已经完整闭环,但实际项目往往不会这么简单收工,生成出来的代码还要面对业务上的各种“自定义需求”,下一章就是讲这个。
5. 生成代码的二次改造与保护手写逻辑
5.1 实体类的增补策略
虽然实体类是自动生成的,但自动生成并不代表你就不能改。我的习惯是把生成器产出的实体类当作“半个成品”,然后手动补上业务注解。比如加 Swagger 的 @ApiModelProperty、加 Validation 的 @NotBlank、加 Jackson 的 @JsonFormat 等。这些注解不影响数据库映射,起着接口文档和参数校验的作用。
这里要小心的是:一旦你下次重新运行生成器,overwrite 为 true 时会把这个手工改动过的文件覆盖掉,注解全部消失。解决思路有两个。第一,把 overwrite 改成 false,生成器发现文件已存在就跳过。但这样后续表结构变更的字段增删也不会反映到实体类中。第二,把实体类的手工扩展部分拆出去,比如用 Lombok 的 @Data 别乱加业务字段,真正需要的计算属性写到 VO 里,实体类只保留数据库字段。我比较推荐第二种,它让实体类的定位很纯粹,生成器的覆盖不会误伤业务代码。
5.2 Mapper 接口和 XML 的扩容方案
单表的基础 CRUD 生成完,业务里必然会出现一些比较复杂的查询,比如联表查询、分组统计、自定义 where 条件。这个需求我之前踩过一个坑:直接往生成的 UserInfoMapper 接口和 UserInfoMapper.xml 里加方法,然后重新运行生成器,手写的方法被覆盖了。后来我觉得最稳妥的方案是保持生成文件不动,新建两个扩展文件。
具体做法:新建 UserInfoMapperExt 接口,继承 UserInfoMapper,把自定义方法写在这里;XML 则新建 UserInfoMapperExt.xml,namespace 指向 UserInfoMapperExt。这样生成器重跑的时候,只覆盖原来的 UserInfoMapper 系列,扩展文件毫发无损。如果只是加少量方法,也可以不加 Ext 类,直接在生成 XML 里加,但前提是你把 overwrite 配成 false。我推荐扩展类方案,因为“生成器覆盖范围”和“手写代码范围”边界清晰,团队协作时不容易冲突。
Ext 接口的写法类似:
public interface UserInfoMapperExt extends UserInfoMapper { List<UserInfo> searchUser(@Param("keyword") String keyword, @Param("status") Integer status); }对应 XML:
<mapper namespace="com.example.mapper.UserInfoMapperExt"> <select id="searchUser" resultMap="com.example.mapper.UserInfoMapper.BaseResultMap"> select <include refid="com.example.mapper.UserInfoMapper.Base_Column_List"/> from user_info <where> <if test="keyword != null and keyword != ''"> and (username like concat('%', #{keyword}, '%') or nick_name like concat('%', #{keyword}, '%')) </if> <if test="status != null"> and status = #{status} </if> </where> </select> </mapper>注意 XML 里可以直接通过全限定名去引用原生成文件里的 resultMap 和 Base_Column_List 片段,这样手写 SQL 也保持简洁,代码不会重复维护一长串字段列表。
5.3 特殊字段的处理:类型处理器 TypeHandler
数据库字段和 Java 属性之间不是总是一一对应的,最典型的是 JSON 字符串字段存储一个对象、枚举字段存储一个 number、敏感字段加密后存储。这时候就需要 TypeHandler 起作用。
我先把 TypeHandler 的工作流程用最通俗的方式讲一遍,因为面试热词里也频繁出现它。MyBatis 执行一条 insert 时,对每个参数,会先找到对应的 TypeHandler,调用它的 setParameter 方法,再通过 setNonNullParameter 把 Java 对象转成 JDBC 能接受的类型,比如把 LocalDateTime 转成 Timestamp,把枚举转成 String 或 Integer。查询的时候流程反过来,MyBatis 从 ResultSet 拿到列值后,调用 TypeHandler 的 getNullableResult 方法,把 JDBC 类型转回 Java 对象。
放在逆向工程场景里,我们通常不需要让生成器自动去处理这些特殊字段,而是生成之后,在 XML 或 mybatis-config 里手动给字段指定 typeHandler。比如有一个 json_config 字段,我定义了一个 JacksonTypeHandler,继承 BaseTypeHandler,在查询结果映射时这样写:
<result column="json_config" property="jsonConfig" typeHandler="com.example.handler.JacksonTypeHandler"/>这样,存储的时候对象自动序列化成 JSON 字符串,查询的时候字符串自动反序列化成目标对象,业务代码完全无感。TypeHandler 这种“写入时转换、查询时还原”的模型,配合 MBG 生成的 resultMap,是二次开发中最常用的扩展手法。
5.4 批量操作的补充
逆向工程生成的 insert 是单条插入,项目里一旦有批量导入、批量同步的需求,就会出现 N 条 SQL 循环插入的尴尬。走循环本身没问题,但数据量一大,性能就差,连接往返次数太多。这时我通常会在 Ext XML 里手写一个批量插入:
<insert id="batchInsert" parameterType="list"> insert into user_info (username, nick_name, email, status) values <foreach collection="list" item="item" separator=","> (#{item.username}, #{item.nickName}, #{item.email}, #{item.status}) </foreach> </insert>这里要注意的是 foreach 的 collection 写法。如果你的 Mapper 方法参数只有一个 List,可以直接写 list;如果是多个参数或者用了 @Param 注解,就必须写注解里指定的参数名。这就是为什么 MyBatis 老有人在“参数绑定”上翻车。生成代码里全是 #{} 占位符,天然防 SQL 注入;手写扩展时同样坚持用 #{},只有极少数动态排序字段名、表名的场景才考虑用 ${},而且必须白名单校验。理解了这一点,面试里问到的“#{} 和 ${} 的区别”就不仅是背概念,而是有实际场景支撑了。
6. 实战踩坑与面试热点对照
6.1 我实际踩过的坑:问题速查表
这几年来,我在逆向工程上踩过的坑攒了不少,大部分都是配置或类型映射层面的,列成表格方便你对照排查:
| 问题现象 | 原因 | 解决办法 |
|---|---|---|
| 生成类的时间字段是 Date 而不是 LocalDateTime | 生成器默认按 JDBC 类型映射 | 项目配置 MyBatis 的 JSR310 模块,并自定义类型映射或用 columnOverride |
| tinyint(1) 生成的是 Boolean | MySQL 中 tinyint(1) 被识别为布尔类型 | 在 columnOverride 指定 javaType="Integer" |
表名包含关键字如order,SQL 报错 | 生成 SQL 没加反引号 | context 配置 beginningDelimiter 和 endingDelimiter 为 ` |
| 带 updated_at 的表,更新时时间不自动刷新 | 生成器把该字段当作普通字段更新 | 在 updated_at 的 columnOverride 里把 update 属性设为 false,或生成后手动删除该字段的 update 列 |
| 重新生成覆盖了手写 SQL | overwrite=true | 改用扩展 Mapper 方案,或全局关掉 overwrite |
| 生成文件报中文乱码 | 编码不一致 | context 配置 javaFileEncoding=UTF-8 |
| 驱动类找不到 | 插件缺少驱动依赖 | 在 mybatis-generator-maven-plugin 的 dependencies 里加 mysql-connector-j |
| 生成的实体类没有字段注释 | MBG 默认不生成注释 | 自定义 CommentGenerator 或用 columnOverride 补说明 |
上表里有个坑我想展开说,就是 updated_at 的问题。MyBatis3Simple 的 updateByPrimaryKey 会把所有非主键字段全部 set 进 SQL,包括 updated_at。这时如果业务里查出来一个 UserInfo 对象,改了用户名,然后调用 updateByPrimaryKey,updated_at 就会变成实体对象里查出来时的值,而不是数据库自己刷新成当前时间。我见过不止一次因为这个产生了“时间没变”的 bug。解决方式很简单,在 table 节点下面对 updated_at 单独做 columnOverride,把 update 属性关掉,或者干脆生成后手动把 XML 里这列从 update 语句中移除。如果你控制不了数据库设计表的习惯,这一步建议必做。
6.2 网上那串热词背后的常见技术点
你的项目标题关联到一堆 MyBatis 的热词,我猜你可能也在准备面试或带新人。有几个确实跟逆向工程强相关,我串起来聊一下。
XMLConfigBuilder 的工作流程。MyBatis 启动时会用 XMLConfigBuilder 解析主配置文件,它把 mybatis-config.xml 里的 settings、typeAliases、mappers 等节点逐个解析进 Configuration 对象。你去看 MBG 的代码,generatorConfig.xml 的解析同样走 XML 节点映射的路数。所以我一直建议新人先看 MBG 配置文件,再去看 MyBatis 初始化源码,会觉得 XMLConfigBuilder 没那么抽象。它本质上就是“把离散的 XML 节点转化为内部配置对象”的过程,理解了主流程,面试官再往下问二级缓存、插件责任链,你都有一个“配置从哪来”的根基。
TypeHandler 的工作流程。前面第五章已经串过一遍,这里只强调:面试时最好能说清楚 TypeHandler 的注册方式(局部注册到 resultMap / parameterMap,全局注册到 typeHandlers 节点)和执行时机。MyBatis 在 resultMap 里发现某个字段配了 typeHandler,查询时会优先用这个类型处理器,而不是根据 Java 类型或 JDBC 类型自动推断。所以你要处理 JSON 字段、枚举字段,类型处理器是绕不开的扩展点。
MyBatis 缓存。生成代码本身不会自动给 Mapper 开二级缓存。MBG 有个 table 节点的 enableCache 开关,打开后会在 XML 里插入 标签,但我觉得这个开关默认关掉为好。缓存生效的前提是数据变更足够少、查询足够频繁,而且要搞明白一级缓存是 SqlSession 级别、二级缓存是 namespace 级别。逆向工程生成的那些基础方法,比如 selectByPrimaryKey,确实适合缓存,但一旦你手写了 update 方法而忘了刷新缓存,就很容易出现脏数据。我的习惯是不依赖二级缓存,把缓存设计放到更上层(比如 Redis),数据库映射层尽量保持“无状态”。
6.3 为什么面试爱问 MyBatis 初始化,而逆向工程恰好是入口
面试里大量出现 XMLConfigBuilder、初始化工作流程、MyBatis 缓存、TypeHandler 工作流程图这类问题,本质上是因为 MyBatis 的核心机制就是“配置驱动 + JDBC 封装”。而逆向工程生成的代码恰好是理解这些机制的入口——你把一个 XML 文件里的 SQL 语句拿到手,看它如何与 Mapper 方法绑定;你把 resultMap 里的映射关系理清楚,就理解为什么数据库列名和 Java 驼峰属性需要转换;你碰一次 LocalDateTime 字段的问题,就会去查 MyBatis 怎么注册类型处理器。
所以如果你是刚学 MyBatis 不久的人,我不建议直接扎进源码,而是先把自己的项目逆向工程跑通,把生成的每个文件逐行看懂,再用 debugger 跟一次查询流程。这个过程比背十篇面试题都有用,因为它把抽象概念全部变成了看得见、能改动的代码。
7. 最后分享一点我的落地习惯
说了这么多,最后聊点新鲜的。我现在在新项目里落地的“清晰简洁版”流程大概是这样的:数据库表结构评审通过后,执行一次生成命令,生成产物纳入版本控制;实体类直接作为领域模型放 domain 包,业务字段需要加注解的话加在 VO 上;Mapper 接口和 XML 只保留基础 CRUD,一切复杂 SQL 写进 Ext 系列文件。这样团队其他人接手时,看一眼就知道:原生成的代码可以随便重跑覆盖,Ext 文件是业务手写部分,不能乱动。
还有一个小习惯,我一直觉得挺管用。每次数据库表结构变更之后,不要整个项目全部重新生成,只需要针对变更的表单独跑一次,因为 MBG 是按 table 节点逐个生成的。为了这个,我习惯在 generatorConfig.xml 里维护一份全量表清单,但每次执行前会临时把无关的表注释掉。你如果也这么干,一定记得确认文件的编码格式,数据库连接串的时区参数也别漏,否则生成时间和注释都可能出问题。
另外,再啰嗦一句关于逆向工程边界的理解。它能解决重复的、规范的单表 CRUD,但它不解决复杂的业务查询。有人期待把整张订单表、明细表、商品表各种关联查询都生成出来,这是不现实的,也不应该这样做。逆向工程的目标是让你把时间省下来,投入到那些真正需要业务判断的 SQL 上。用好了,它就是团队的效率工具;用不好,它就是代码仓库里的一堆废料。这个分寸,才是“清晰简洁版”最核心的价值。