1. 为什么还要聊 MyBatis 逆向工具
如果你写过 Java 后端,尤其是维护过那种表结构几十张、字段上百个的老系统,一定经历过这种场景:产品经理丢过来一个新需求,你打开数据库客户端,对着建表语句一行行抄字段名,然后在 IDE 里手写实体类、Mapper 接口、XML 映射文件。抄到第十个字段的时候,眼睛已经开始花了,create_time写成creat_time,is_deleted的映射类型写成String,编译能过,跑起来一查数据全是 null。
这种重复劳动,MyBatis-Generator 就是专门来终结它的。它的核心逻辑非常朴素:读取数据库的元数据(表名、字段名、字段类型、注释),按照你配置的规则,自动生成 Java 实体类、Mapper 接口和 XML 映射文件。你只需要改配置文件里的几个参数,剩下的交给它批量产出。
我见过不少团队对这类工具的态度两极分化。一派觉得"生成的代码太丑,不如手写可控";另一派则把它当成项目脚手架,新模块一上来先跑一遍生成,再在生成结果上做二次加工。我的经验是,关键在于你怎么配置它,而不是它本身好不好用。配置得当,生成出来的代码可以直接进版本库;配置随意,那确实是一堆需要大改的垃圾。
这篇文章面向的是刚接触 MyBatis-Generator、或者用过但总觉得生成结果不顺手的朋友。我会从配置文件的每一个关键参数讲起,把生成策略、命名规则、类型映射这些容易踩坑的地方拆开说清楚,最后给出一套我自己在用的、可以直接抄的配置模板。全程不涉及任何特定公司的内部框架,就是最通用的 MyBatis-Generator 用法。
2. 先把 generatorConfig.xml 的骨架搭明白
MyBatis-Generator 的运行方式有好几种:Maven 插件、Java 代码调用、命令行。不管哪种方式,核心都是同一个配置文件,通常叫generatorConfig.xml。这个文件的结构其实就三块:数据库连接配置、生成策略配置、表清单配置。很多人一上来就复制网上的模板,改个数据库密码就跑,结果生成出来的包名不对、路径不对、注释乱码,然后又回头一点点试。与其这样,不如先把每一块的作用搞清楚。
2.1 数据库连接与驱动加载的细节
配置文件最上面是<jdbcConnection>节点,这里有几个参数值得单独说:
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" userId="root" password="your_password"> </jdbcConnection>driverClass这块,MySQL 8.x 之后驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,如果你还用老的名字,控制台会给你一个警告,虽然不一定报错,但建议直接换成新的。connectionURL里的serverTimezone参数是很多人忽略的坑,不配的话,生成的实体类里Date类型字段在运行时会因为时区问题差几个小时,排查起来很费劲。
还有一个隐藏问题:驱动 jar 包不在 classpath 里。用 Maven 插件方式运行时,插件依赖和项目依赖是分开的,你项目里引了 MySQL 驱动,不代表插件能加载到。稳妥的做法是在插件的<dependencies>里显式声明驱动依赖,版本和你项目保持一致。
提示:如果数据库密码里有特殊字符(比如
@、#),在 XML 里需要做转义,或者用 CDATA 包起来,否则解析会出错。
2.2 生成策略配置决定了代码长什么样
<javaModelGenerator>、<sqlMapGenerator>、<javaClientGenerator>这三个节点,分别控制实体类、XML 文件、Mapper 接口的生成位置和包名。它们的targetPackage和targetProject两个属性必须配合好:targetProject是相对于工作目录的路径,targetPackage是包名,工具会自动把包名转成目录结构。
<javaModelGenerator targetPackage="com.example.demo.entity" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> <property name="trimStrings" value="true"/> </javaModelGenerator>enableSubPackages这个属性,当你的表名带前缀(比如t_user、t_order)时特别有用。开启后,工具会根据表名的前缀自动生成子包,t_user会进到entity.t包下。但说实话,这个行为大多数人不喜欢,因为包名会变得很奇怪。我的建议是关掉它,用后面的domainObjectRenamingRule来统一去掉表前缀,这样包结构更干净。
trimStrings属性控制是否对字符串类型的字段做 trim 处理,默认是 false。如果你的表里有char类型字段,建议开启,避免查询出来的值带一堆空格。
2.3 表清单配置的两种写法
表清单有两种配置方式:<table>逐个指定,或者用<table>的tableName属性配合通配符。逐个指定适合只生成部分表的场景,通配符适合全库生成。
<table tableName="t_user" domainObjectName="User" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> </table>这里有一组enableXxxByExample属性,默认全是 true。这意味着每张表会额外生成一堆XxxExample类和相关方法。对于简单的 CRUD 场景,这些 Example 类基本用不上,反而让代码库变得臃肿。我的习惯是全部关掉,只保留基础的增删改查方法。如果你的项目确实需要动态条件查询,再单独打开。
3. 让生成的代码能直接用的几个关键配置
配置文件跑通只是第一步,生成出来的代码能不能直接用,取决于你对命名规则、类型映射、注释生成这几个方面的调教。这一块是区分"能用"和"好用"的分水岭。
3.1 表名前缀与字段前缀的批量处理
实际项目里,表名带前缀是常态:t_user、tb_order、sys_role。如果直接生成,实体类名就是TUser、TbOrder、SysRole,看着别扭。这时候需要用到domainObjectRenamingRule:
<table tableName="t_%" domainObjectName="" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> <domainObjectRenamingRule searchString="^T" replaceString=""/> </table>searchString是正则表达式,replaceString是替换后的内容。上面这个配置会把t_user生成的类名从TUser变成User。注意,这个规则作用于生成的类名,不是表名本身。字段前缀的处理类似,用columnRenamingRule:
<columnRenamingRule searchString="^is_" replaceString=""/>这样is_deleted字段生成的属性名就是deleted,而不是isDeleted。不过这里要小心,如果去掉前缀后和 Java 关键字冲突(比如class、new),生成会失败,需要手动调整。
3.2 字段类型映射的覆盖与自定义
MyBatis-Generator 内置了一套 JDBC 类型到 Java 类型的映射表,大部分情况下够用。但有两种情况需要你手动干预:
第一种是tinyint(1)。MySQL 里很多人用它当布尔值,但生成器默认会映射成Byte或Integer,而不是Boolean。解决办法是在<javaTypeResolver>里配置:
<javaTypeResolver> <property name="forceBigDecimals" value="false"/> <property name="useJSR310Types" value="true"/> </javaTypeResolver>useJSR310Types设为 true 后,datetime会映射成LocalDateTime,date映射成LocalDate,比老的java.util.Date好用得多。至于tinyint(1)转 Boolean,需要自定义 TypeResolver,继承JavaTypeResolverDefaultImpl并重写calculateJavaType方法,这个稍微有点门槛,但网上有现成的实现可以借鉴。
第二种是decimal类型。默认会映射成BigDecimal,这是对的。但如果你把forceBigDecimals设成 true,所有numeric类型都会强制转成BigDecimal,包括int,这显然不是你想要的。所以这个属性保持 false 就好。
3.3 注释生成:别让数据库注释白写
数据库建表时写的COMMENT,如果生成器不读,那就白写了。开启注释生成很简单,在<commentGenerator>节点里配置:
<commentGenerator> <property name="addRemarkComments" value="true"/> <property name="suppressDate" value="true"/> <property name="suppressAllComments" value="false"/> </commentGenerator>addRemarkComments设为 true 后,数据库里的字段注释会变成实体类字段上的 Javadoc。suppressDate建议设为 true,否则每次生成都会在注释里加时间戳,导致 Git diff 里全是无意义的变更。suppressAllComments如果设成 true,那前面两个配置就都失效了,所以保持 false。
这里有个细节:注释里的换行符和特殊字符。如果数据库注释里写了多行文本,生成到 Javadoc 里可能会破坏格式。稳妥的做法是建表时注释保持单行,或者用自定义的 CommentGenerator 做转义处理。
4. 跑通生成之后的二次加工与维护策略
生成代码只是起点,真正的工作量在于生成之后怎么管理这些代码。我见过两种极端:一种是每次表结构变更就重新生成,覆盖掉所有手工修改;另一种是把生成结果复制出来改个名字,从此和生成器说再见。这两种都有问题。
4.1 生成代码与手写代码的边界划分
我的做法是分层管理:生成的实体类和 Mapper 接口放在generated包下,手写的业务逻辑放在manual包下。Mapper 接口继承生成的接口,业务方法写在子接口里。这样表结构变更时,重新生成generated包不会影响手写代码。
// generated 包下,自动生成 public interface UserMapper { int deleteByPrimaryKey(Long id); int insert(User record); User selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(User record); } // manual 包下,手写扩展 public interface UserMapperExt extends UserMapper { List<User> selectByCondition(UserQuery query); int batchInsert(List<User> users); }XML 文件同理,生成的 XML 只包含基础 SQL,手写的 XML 单独一个文件,通过<mapper namespace="...UserMapperExt">关联。这样职责清晰,重新生成时不会误伤。
4.2 表结构变更后的增量生成
表结构变更后,不要全库重新生成,那样会产生大量无意义的 diff。正确的做法是只生成变更的表。把<table>节点改成只包含目标表,跑一次生成,然后检查 diff,确认无误后提交。
如果变更只是加了一个字段,生成器会在实体类里加一个属性,在 XML 里加对应的resultMap映射和insert/update语句片段。这些变更通常是安全的。但如果字段类型变了,比如从varchar改成int,生成器会改属性类型,这时候要检查所有引用该属性的手写代码,避免编译错误。
注意:重新生成前一定要先提交当前工作区的代码,或者用
git stash保存。生成器覆盖文件是不可逆的,没有版本控制兜底很容易出事。
4.3 把生成器集成到构建流程里
如果团队里多个人协作,建议把生成器配置和运行方式固化下来。Maven 插件方式最省事:
<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> </plugin>配置好后,运行mvn mybatis-generator:generate就能生成。overwrite设为 true 表示覆盖已有文件,verbose设为 true 会在控制台输出详细日志,方便排查问题。
但这里有个团队协作的坑:不同人本地的数据库表结构可能不一致。如果 A 同学的表里多了一个字段,B 同学跑生成器时就会把这个字段的映射删掉。解决办法是让生成器连接一个统一的开发库,或者把生成器运行作为 CI 流程的一部分,由专人负责。
5. 那些年我踩过的生成器坑
说了这么多配置,最后聊几个实际踩过的坑,都是文档里不会写、但遇到了很耽误时间的。
5.1 生成的文件编码不对导致中文注释乱码
这个问题在 Windows 环境下特别常见。生成器默认用系统编码读取和写入文件,如果数据库是 UTF-8,系统是 GBK,生成的 Java 文件里中文注释就是乱码。解决办法是在 Maven 插件配置里加 JVM 参数:
<configuration> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> </configuration>然后在MAVEN_OPTS里设置-Dfile.encoding=UTF-8。或者更彻底一点,在<commentGenerator>里配置javaFileEncoding属性为UTF-8。这个属性不是所有版本都支持,1.4.x 之后才比较稳定。
5.2 主键类型识别错误
如果表的主键是bigint unsigned,生成器有时会识别成BigInteger而不是Long。这会导致 Mapper 接口里的selectByPrimaryKey参数类型不对,调用时编译报错。解决办法是在<table>节点里显式指定主键列:
<table tableName="t_user" domainObjectName="User"> <generatedKey column="id" sqlStatement="MySql" identity="true"/> </table>generatedKey节点告诉生成器主键列是自增的,生成的insert语句会带上useGeneratedKeys配置,实体类的主键属性也会被正确标注。
5.3 生成器把视图也当成表生成了
如果数据库里有视图,用通配符tableName="%"时,生成器会把视图也扫出来,生成一堆无意义的实体类。解决办法是用tableName精确指定表,或者在配置里加schema过滤。有些版本的生成器支持<property name="ignoreQualifiersAtRuntime" value="true"/>,但最稳妥的还是手动列出需要生成的表。
5.4 重复生成导致 XML 里的 resultMap 冲突
如果手写的 XML 里已经定义了同名的resultMap,重新生成时会产生冲突,MyBatis 启动时报 "Result Maps collection already contains value" 错误。解决办法是生成的 XML 和手写的 XML 用不同的 namespace,或者手写 XML 里不要定义和生成文件同名的 resultMap。
6. 一套可以直接抄的配置模板
说了这么多理论,最后给一份我目前在用的配置模板。这份配置的特点是:关闭了所有 Example 类生成、开启了 JSR310 时间类型、去掉了表前缀、开启了数据库注释、生成的代码可以直接进版本库。
<?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="mysql" targetRuntime="MyBatis3" defaultModelType="flat"> <property name="javaFileEncoding" value="UTF-8"/> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <commentGenerator> <property name="addRemarkComments" value="true"/> <property name="suppressDate" value="true"/> <property name="suppressAllComments" value="false"/> </commentGenerator> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" userId="root" password="your_password"> </jdbcConnection> <javaTypeResolver> <property name="forceBigDecimals" value="false"/> <property name="useJSR310Types" value="true"/> </javaTypeResolver> <javaModelGenerator targetPackage="com.example.demo.generated.entity" targetProject="src/main/java"> <property name="enableSubPackages" value="false"/> <property name="trimStrings" value="true"/> </javaModelGenerator> <sqlMapGenerator targetPackage="com.example.demo.generated.mapper" targetProject="src/main/resources"> <property name="enableSubPackages" value="false"/> </sqlMapGenerator> <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.demo.generated.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="false"/> </javaClientGenerator> <table tableName="t_user" domainObjectName="User" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> <generatedKey column="id" sqlStatement="MySql" identity="true"/> <domainObjectRenamingRule searchString="^T" replaceString=""/> </table> </context> </generatorConfiguration>这份配置里,defaultModelType设为flat表示每个表生成一个实体类,不生成带主键的独立类。beginningDelimiter和endingDelimiter设为反引号,是为了兼容 MySQL 里可能出现的保留字字段名。javaClientGenerator的type设为XMLMAPPER,表示 Mapper 接口和 XML 文件分离,这也是目前最主流的用法。
用这份配置跑一遍,生成的实体类字段名干净、注释完整、时间类型是LocalDateTime,Mapper 接口只有基础的增删改查方法。后续要加自定义查询,在manual包里扩展就行。
7. 关于生成器的一些个人体会
用了这么多年 MyBatis-Generator,我最大的感受是:它解决的是"从零到一"的问题,不是"从一到一百"的问题。表结构刚建好、项目刚起步的时候,它能帮你省下大量机械劳动;但项目进入维护期后,表结构变更频繁,每次重新生成都要小心翼翼检查 diff,这时候它的价值就下降了。
我的建议是:新项目、新模块,大胆用生成器打底;老项目、核心模块,手写为主,生成器只用来做参考。另外,生成器的配置最好纳入版本控制,团队统一使用同一份配置,避免每个人生成出来的代码风格不一致。
还有一点,不要过度依赖生成器的 Example 查询。那套 API 虽然灵活,但可读性差,复杂查询写出来像天书。我见过一个项目里,一个多表关联查询用 Example 拼了上百行代码,后来维护的人直接重写成了手写 SQL。生成器给的是基础能力,复杂逻辑还是得靠人。
最后分享一个小技巧:如果生成出来的实体类字段太多,看着眼花,可以在 IDE 里装一个折叠插件的,把 getter/setter 折叠起来,只看字段定义。或者用 Lombok 的@Data注解替代 getter/setter,生成器虽然不直接支持 Lombok,但可以在生成后手动加注解,或者用自定义的插件模板。这个就看个人取舍了,Lombok 能减少代码量,但也会带来一些编译期依赖的问题,团队里要统一意见。