多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从建表SQL到Java代码:代码生成器的全流程解析与避坑指南

从建表SQL到Java代码:代码生成器的全流程解析与避坑指南 简介一款面向Java后端开发者的本地代码生成工具可根据数据库表信息一键生成控制层、服务层、仓储层、实体类、映射器接口及映射文件的基础增删改查代码帮助开发者摆脱重复手写数据访问层的繁琐工作。资源以压缩包形式提供共25个文件包括18个Java源码/模板文件、2个映射配置文件、1个数据库连接配置、1个示例建表脚本、1个可执行工具、1个启动脚本以及1份详细使用说明整体约24.51MB。目前已有1934人学习/下载。使用时只需按说明修改配置中的绝对路径、数据库连接信息及目标表名再运行启动脚本即可在指定目录中得到带接口注释、实体映射注释和基础增删改查逻辑的代码文件这些代码可直接复制到项目里调整大部分增删改查场景都能快速落地。配套的建表脚本和使用说明让整个流程易于验证适合有一定基础的Java开发者用于项目初始阶段或表结构变更后的代码同步。1. 根据数据库SQL生成Java代码的代码生成器一次解析几十张表一起出码数据库设计评审刚过建表 SQL 发到群里后端下半夜就得开始一项纯体力劳动照着一个 bigint、一个 varchar(64) 把 Entity 敲出来再复制进 Mapper XML 写 resultMap字段一多眼睛就花少写一个字段要等联调才发现。根据数据库 SQL 生成 Java 代码的代码生成器就是为这个场景准备的把 CREATE TABLE 解析成结构化元数据再用模板批量产出 Entity、Mapper、XML、Service几十张表的重复代码几秒生成完。它适合手里有建表脚本、或者能连上测试库的 Java 后端团队尤其适合 Spring Boot MyBatis 这类模板化很强的组合。这篇文章把解析、类型映射、模板渲染、落盘完整走一遍最后是五条血泪踩坑。2. 解析建表SQL这一步JSqlParser 和 JDBC 两条路线怎么选生成器的第一步永远是拿表结构。这一步有两条路线直接连数据库用 JDBC 的 DatabaseMetaData 接口捞字段信息或者拿一个 schema.sql用 SQL 解析库把 CREATE TABLE 拆开。两条路线的差异不只是代码写法不同而是整个生成器的运行环境和依赖都不一样。2.1 能连数据库就走 JDBC只有SQL文件才去硬解析我一般会优先连库因为数据库里存的信息最全列注释、主键、是否自增、默认值一个接口全都有。代价是生成环境必须能连上数据库账号得有 information_schema 的读权限。很多公司数据库权限管得严生产库肯定不能连那就退回读 SQL 文件的路线。DatabaseMetaData metaData connection.getMetaData(); ResultSet tables metaData.getTables(null, null, %, new String[]{TABLE}); while (tables.next()) { String tableName tables.getString(TABLE_NAME); ResultSet columns metaData.getColumns(null, null, tableName, null); ListColumnMeta columnMetas new ArrayList(); while (columns.next()) { ColumnMeta meta new ColumnMeta(); meta.setColumnName(columns.getString(COLUMN_NAME)); meta.setSqlType(columns.getString(TYPE_NAME)); meta.setComment(columns.getString(REMARKS)); meta.setNullable(columns.getInt(NULLABLE) ! DatabaseMetaData.columnNoNulls); columnMetas.add(meta); } ResultSet pk metaData.getPrimaryKeys(null, null, tableName); while (pk.next()) { String pkColumn pk.getString(COLUMN_NAME); columnMetas.stream() .filter(c - c.getColumnName().equalsIgnoreCase(pkColumn)) .forEach(c - c.setPrimaryKey(true)); } // 到这里一张表的字段列表、注释、主键就都拿到了 }这段代码里最容易被忽略的是连接串参数MySQL 要带上 useInformationSchematrue 和 characterEncodingutf8否则 REMARKS 字段返回 null中文注释在生成结果里全变成 null。getPrimaryKeys 返回的可能是复合主键循环里做 equalsIgnoreCase 匹配就行如果一张表没有主键生成器后续做按主键更新的方法时就要特殊处理。JDBC 方式还有个隐藏好处数据库对类型名和大小写的处理已经帮你做过了比如 MySQL 的 int 在 TYPE_NAME 里就是 INT不会出现 varchar(64) 带括号的情况。缺点也很明显它要求表真的存在于库里。如果你们是拿着 SQL 做评审、库还没建或者生成器要跑在 CI 环境里不想依赖外部服务离线解析 SQL 文件就更合适。2.2 用 JSqlParser 把CREATE TABLE拆成表结构离线解析在 Java 生态里最省事的库是 JSqlParserMaven 坐标是 net.sf.jsqlparser:jsqlparser专门把 SQL 字符串变成 Java 对象。先跑一个最小解析看它长什么样import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; String createSql CREATE TABLE sys_user (\n id BIGINT AUTO_INCREMENT COMMENT 主键ID,\n user_name VARCHAR(64) NOT NULL COMMENT 用户名,\n PRIMARY KEY (id)\n ) COMMENT用户表; CreateTable ct (CreateTable) CCJSqlParserUtil.parse(createSql); System.out.println(ct.getTable().getName()); // sys_user System.out.println(ct.getColumnDefinitions().size()); // 2列 for (ColumnDefinition cd : ct.getColumnDefinitions()) { System.out.println(cd.getColumnName()); // id / user_name System.out.println(cd.getColDataType().getDataType()); // BIGINT / VARCHAR System.out.println(cd.getColumnSpecs()); // [AUTO_INCREMENT, COMMENT, 主键ID] }两个关键说明。第一CCJSqlParserUtil.parse 一次只解析一条语句schema.sql 里如果塞了几十条 CREATE TABLE要自己按分号拆分或者逐条读取单独 parse别直接丢整份文件进去否则会在第一条语句结束后的位置报错。第二COMMENT 在 columnSpecs 里的形态随版本变化有的版本里 COMMENT 和 主键ID 是数组里相邻的两个元素有的版本已经把它收拢成字符串。拿到数组先打印出来看一眼再写抽取逻辑别照着网上的旧帖子写死下标。2.3 注释、主键、自增解析完怎么组装成元数据模型解析完得到的是零散字符串离能用还差一步把字段、注释、主键、自增这些信息统一装进元数据模型。这是整个生成器的数据地基后面模板渲染只认这个模型不认 SQL。public class TableMeta { private String rawTableName; // 保留原始大小写生成 XML 时要用 private String comment; // 表注释 private ListColumnMeta columns new ArrayList(); } public class ColumnMeta { private String columnName; // 原始列名 private String sqlType; // 原始 SQL 类型 private String comment; // 列注释 private boolean primaryKey; private boolean autoIncrement; private boolean nullable; }装配代码里最麻烦的是表级注释和主键。MySQL 的 CREATE TABLE 末尾那段 COMMENT用户表JSqlParser 有的版本解析到 TableOptions 里有的版本要自己用正则兜底。主键也有两种写法列定义里直接带 PRIMARY KEY或者表级 PRIMARY KEY (id) 单独列一行。// 表级注释正则兜底处理 COMMENT用户表 Matcher m Pattern.compile(COMMENT\\s*\\s*([^]*), Pattern.CASE_INSENSITIVE) .matcher(createSql); if (m.find()) { tableMeta.setComment(m.group(1)); } // 列级注释和自增、主键 for (ColumnDefinition cd : ct.getColumnDefinitions()) { ColumnMeta meta new ColumnMeta(); meta.setColumnName(cd.getColumnName()); meta.setSqlType(cd.getColDataType().getDataType()); meta.setComment(extractComment(cd.getColumnSpecs())); meta.setAutoIncrement(cd.getColumnSpecs().stream() .anyMatch(s - s.toUpperCase().contains(AUTO_INCREMENT))); boolean pk cd.getColumnSpecs().stream() .anyMatch(s - s.toUpperCase().contains(PRIMARY KEY)); if (!pk primaryKeyColumns.contains(cd.getColumnName())) { pk true; // 表级 PRIMARY KEY (id) 命中的列 } meta.setPrimaryKey(pk); }表级主键的列名集合可以从 JSqlParser 的主键 API 拿也可以直接用正则 PRIMARY KEY\s*(([^)])) 抠。我的经验是正则更稳因为不依赖具体版本的 API。还有一点rawTableName 必须从头保留到渲染结束生成 Mapper XML 时用原始大小写中途转驼峰会丢掉大小写信息这个问题在第 5.4 节还会踩一次。3. SQL类型到Java类型映射表和命名策略是生成器的灵魂元数据模型建好后下一道工序是翻译把数据库类型翻译成 Java 类型把下划线列名翻译成驼峰属性名。这一步做得好不好直接决定生成出来的代码能不能过编译、能不能直接进业务开发。3.1 SQL类型到Java类型的映射表一张要不断扩展的表映射表是整个生成器里最需要维护的部分因为每个公司用的数据库方言不一样。先给出 MySQL 最常见的一套映射这是我实际项目里的默认值SQL 类型MySQLJava 类型是否需要 importvarchar / char / text / longtextString否int / integer / tinyint / smallintInteger否bigintLong否decimal / numericBigDecimaljava.math.BigDecimalfloat / doubleFloat / Double否bit / booleanBoolean否dateLocalDatejava.time.LocalDatedatetime / timestampLocalDateTimejava.time.LocalDateTimetimeLocalTimejava.time.LocalTimeblob / longblobbyte[]否映射规则里有两个必须处理的变种。第一个是 tinyint(1)很多团队把它当布尔用生成 Boolean 更符合业务直觉但有的老表里 tinyint(1) 存的其实是 0/1/2强行生成 Boolean 会让业务判断出现真空。我会在配置里放一个开关 tinyintAsBoolean默认 false 生成 Integer确认规范后需要时再打开。第二个是 int unsignedMySQL 无符号整型的取值范围已经超过 Java 的 Integer如果库里有这种列建议映射到 Long否则数据超过 21 亿时直接溢出。public final class TypeMapping { private static final MapString, String MAP new HashMap(); static { MAP.put(varchar, String); MAP.put(char, String); MAP.put(text, String); MAP.put(longtext, String); MAP.put(int, Integer); MAP.put(integer, Integer); MAP.put(tinyint, Integer); MAP.put(smallint, Integer); MAP.put(bigint, Long); MAP.put(decimal, BigDecimal); MAP.put(numeric, BigDecimal); MAP.put(float, Float); MAP.put(double, Double); MAP.put(bit, Boolean); MAP.put(date, LocalDate); MAP.put(datetime, LocalDateTime); MAP.put(timestamp, LocalDateTime); MAP.put(time, LocalTime); MAP.put(blob, byte[]); } public static String toJavaType(String sqlType, boolean tinyintAsBoolean) { String key sqlType.toLowerCase(); if (tinyintAsBoolean tinyint.equals(key)) { return Boolean; } return MAP.getOrDefault(key, String); } }映射方法入口收到的一定是干净的类型名也就是解析阶段已经去掉括号的类型。这一步别偷懒别在映射方法里做字符串替换因为 varchar(64) 直接匹配不上 varchar。无法识别的类型回退到 String 并打 WARN 日志这样比生成时抛异常好——但警告一定要打出来避免一个 timestamp 被悄悄当成 String 用。类型映射完还要同步算 import 列表。String、Integer、Long 在 java.lang 里不用引BigDecimal、LocalDate、LocalDateTime、LocalTime 必须 import。我把这个逻辑放在 TableMeta 里public ListString getImportList() { SetString imports new TreeSet(); for (ColumnMeta c : columns) { switch (c.getJavaType()) { case BigDecimal: imports.add(java.math.BigDecimal); break; case LocalDate: imports.add(java.time.LocalDate); break; case LocalDateTime: imports.add(java.time.LocalDateTime); break; case LocalTime: imports.add(java.time.LocalTime); break; } } return new ArrayList(imports); }这样模板里只需要 list importList 循环输出不用在 .ftl 里摆弄条件判断也避免出现同一个 import 被写两遍的问题。3.2 下划线转驼峰命名规则的三个边界下划线转驼峰是生成 Entity 字段名时的重头戏大多数人的第一版实现就是循环处理下划线但实际会撞上三个边界。第一个边界是列名本身已经是驼峰。有些历史表是用 orderId 这种命名建的你的转换方法如果无条件把首字母大写orderId 会变成 Orderid。所以要先判断字符串里有没有下划线没有就只做首字母大小写调整public static String toCamel(String name, boolean firstUpper) { if (name null || name.isEmpty()) { return name; } if (!name.contains(_)) { return firstUpper ? Character.toUpperCase(name.charAt(0)) name.substring(1) : Character.toLowerCase(name.charAt(0)) name.substring(1); } StringBuilder sb new StringBuilder(); boolean upper firstUpper; for (int i 0; i name.length(); i) { char ch name.charAt(i); if (ch _) { upper true; } else { sb.append(upper ? Character.toUpperCase(ch) : Character.toLowerCase(ch)); upper false; } } return sb.toString(); }第二个边界是连续下划线。user__name 这种列名在数据质量差的表里不算罕见按上面的算法会得到 userName但中间那个双下划线直接消失语义其实不对。我的建议是建表规范里直接禁止连续下划线生成器侧遇到就打 WARN别默默吞掉。第三个边界是列名里的数字。order_2019 转出来是 order2019这是合法 Java 标识符不用处理。但 2019_order 这种数字开头的列名在 MySQL 里可以存在生成 Java 字段名时必须以字母或下划线开头遇到这种情况我选择直接报错而不是自动加前缀——自动改名字会让业务代码和数据库列对不上出了问题很难排查。3.3 TableMeta与ColumnMeta的完整形态前面散着定义了两个类真正落地时还要加几个生成期辅助方法让模板层保持零逻辑。模板里只需要取值不需要做任何计算这样出了问题能快速分清是数据错了还是模板错了。public class TableMeta { private String rawTableName; private String comment; private ListColumnMeta columns new ArrayList(); public String getEntityName() { String camel toCamel(rawTableName, true); return camel.endsWith(Entity) ? camel : camel Entity; } public String getMapperName() { return toCamel(rawTableName, true) Mapper; } public ColumnMeta getPrimaryKey() { return columns.stream() .filter(ColumnMeta::isPrimaryKey) .findFirst().orElse(null); } }getPrimaryKey() 返回 null 的情况要提前处理没有主键的表selectByPrimaryKey、updateByPrimaryKey 都没法生成。我的做法是在渲染前统一校验一遍缺主键的表只生成 Entity不生成 Mapper同时把警告打到控制台。别让模板渲染到一半才报错那时候你已经改不动数据了。Entity 后缀加不加也要做成配置项有的团队喜欢直接叫 SysUser有的要求加 Entity 后缀避免和其他业务类混淆。4. 用Freemarker把元数据渲染成Entity和Mapper最小可运行实现元数据准备好后生成器就进入了真正出代码的阶段。这个阶段核心是模板引擎模板文件决定了产出代码的风格也是整个生成器里最值得花时间打磨的部分。4.1 为什么用模板引擎而不是StringBuilder拼代码第一版生成器最容易写成 StringBuilder.append(public class ) className append( {)……刚开始觉得挺爽等你要给字段加 Javadoc、按条件生成 import、或者往 Mapper XML 里输出where标签时字符串拼接的缩进、换行、转义会让代码变得完全不可读。模板引擎把这段逻辑反过来代码骨架是模板文件生成器只负责填数据。Freemarker 和 Velocity 都能干这事。我选 Freemarker理由很实际Spring Boot 的 starter 就是它团队里大多数人熟。模板里能用 #list 遍历字段能写 #if 做条件已经覆盖生成器 99% 的场景。模板放 resources/templates 目录后改模板不用重新编译生成器这个体验比改 Java 代码里的字符串强太多。4.2 Entity模板和Mapper模板一份能直接进项目的骨架entity.ftl 负责生成实体类模板里只做取值和循环package ${pkg}; #list importList as imp import ${imp}; /#list /** * ${table.comment} * 表名${table.rawTableName} */ Data public class ${table.entityName} { #list table.columns as col /** * ${col.comment} */ private ${col.javaType} ${col.javaName}; /#list }mapper.ftl 负责生成 Mapper 接口里面是数据库增删改查的最小集package ${pkg}; import ${entityPkg}.${table.entityName}; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; Mapper public interface ${table.mapperName} { int insert(${table.entityName} record); int updateByPrimaryKey(${table.entityName} record); ${table.entityName} selectByPrimaryKey(${table.primaryKey.javaType} ${table.primaryKey.javaName}); int deleteByPrimaryKey(${table.primaryKey.javaType} ${table.primaryKey.javaName}); }mapper.ftl 里 selectByPrimaryKey 的参数类型直接取主键列的 javaType所以要求 TableMeta 在渲染前算好 primaryKey 字段如果主键为 nullFreemarker 会在取值时报错。Mapper 接口里将来写参数默认值、分页这些全走 #{} 占位符天然避开 sql 注入的问题这个习惯要从模板里固化下来而不是等业务代码里再规范。Mapper XML 的模板同样重要resultMap 和列清单是重头戏?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespace${pkg}.${table.mapperName} resultMap idBaseResultMap type${entityPkg}.${table.entityName} #list table.columns as col result column${col.columnName} property${col.javaName}/ /#list /resultMap sql idBase_Column_List #list table.columns as col ${col.columnName}#if col_has_next,/#if /#list /sql /mapper这里最能看出 2.3 节强调保留原始 columnName 的价值result 标签的 column 用的是数据库原始列名property 用的是驼峰属性名两套名字在同一个文件里共存一旦中途丢了一个生成的就是一个看起来正常但运行时完全对不上的 XML。4.3 生成器主流程从SQL文件到落盘模板就位后主流程就是一个循环加一次渲染但配置项的设计决定了生成器能不能适应不同项目。我习惯用一个 generator.yml 把路径和开关集中起来sqlFile: ./schema.sql templateDir: ./src/main/resources/templates tablePrefix: sys_ entityPackage: com.example.demo.entity mapperPackage: com.example.demo.mapper mapperXmlDir: ./src/main/resources/mapper outputDir: ./target/generated-sources/java tinyintAsBoolean: false主流程代码public class CodeGenerator { public static void main(String[] args) throws Exception { GeneratorConfig config GeneratorConfig.load(generator.yml); ListTableMeta tables SqlMetaLoader.load(new File(config.getSqlFile())); for (TableMeta table : tables) { if (config.getTablePrefix() ! null !config.getTablePrefix().isEmpty()) { table.setRawTableName(table.getRawTableName() .replaceFirst(^ config.getTablePrefix(), )); } } FreemarkerEngine engine new FreemarkerEngine(config.getTemplateDir()); for (TableMeta table : tables) { if (table.getPrimaryKey() null) { System.err.println(WARN: table.getRawTableName() 没有主键跳过Mapper生成); continue; } engine.render(entity.ftl, table, config.getOutputDir() / config.getEntityPackage().replace(., /) / table.getEntityName() .java); engine.render(mapper.ftl, table, config.getOutputDir() / config.getMapperPackage().replace(., /) / table.getMapperName() .java); } } }几个参数的坑要说清楚。tablePrefix 用来剥业务表前缀sys_user 剥掉 sys_ 后生成 UserEntity否则每个类都叫 TSysUser 很难受。剥前缀用的是正则 replaceFirst注意前缀里的特殊字符要转义比如 t_ 没问题但 cms_ 里的下划线在正则里不是特殊字符而任何带 . 或 * 的前缀都会出问题。渲染引擎封装时一定要设置 UTF-8 编码Configuration cfg new Configuration(Configuration.VERSION_2_3_32); cfg.setDirectoryForTemplateLoading(new File(templateDir)); cfg.setDefaultEncoding(UTF-8); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);这里 setDefaultEncoding(UTF-8) 是很多人第一次跑生成器就翻车的地方Windows 下不设这一行注释里的中文全变乱码。outputDir 指向 target/generated-sources/java 的好处是构建时不会混进 src 目录配合 build-helper-maven-plugin 可以把生成代码直接编译进 classpath但如果团队习惯直接改生成代码就输出到业务源码目录这个选择本质上决定了生成器是工具还是一次性脚手架。5. 代码生成器的避坑记录5个让生成结果作废的高频问题生成器跑通容易但要保证生成结果能在真实项目里活下来下面这五个问题几乎每个团队都会撞上至少一个。每一条我都按现象、原因、解决三个步骤记录。5.1 关键字列名让Mapper XML直接编译失败现象生成完一跑 MyBatis报 BuilderExceptionSQL 解析阶段就挂掉定位到一条 order 字段上的查询。原因建表的人用了 order、desc、group、index 这类 MySQL 保留字做列名生成器在 resultMap、insert 列清单、where 条件里全裸写列名数据库解析到保留字直接语法错误。解决维护一份保留字清单命中就用反引号包起来order、desc这样输出。注意 insert 的列名、update 的 set 片段、where 条件每一处都要处理不能只处理 resultMap。保留字清单不用追求全覆盖 order、desc、group、index、key、status 这些高频就够日常顶一阵。根本解法还是回到建表规范禁止保留字做列名生成器只是兜底。5.2 自增主键和逻辑删除字段被当成普通字段现象insert 语句把自增主键 id 也插进去了数据库报主键不可插入逻辑删除字段 deleted 出现在查询条件里把已删除的数据捞回来。原因解析阶段拿到了 autoIncrement 标记但生成 insert SQL 时没过滤逻辑删除字段完全没有识别机制。解决在 ColumnMeta 加 autoIncrement 和 logicalDelete 两个布尔标记。生成 insert 时跳过 autoIncrement 列生成查询和删除语句时如果表里有逻辑删除字段自动拼上 deleted 0 条件。逻辑删除字段名做成配置项默认值为 deleted表里有这个字段就自动启用没有就不处理。5.3 JDBC方式取不到注释Javadoc全是null现象用 JDBC 方式生成Entity 里所有字段注释清一色 null生成出来的代码跟白板一样可读性大打折扣。原因MySQL 的 getColumns() 返回的 REMARKS 字段需要驱动去 information_schema 里查默认连接串下很多驱动版本直接不查REMARKS 就是 null。解决JDBC URL 拼成 jdbc:mysql://host:3306/db?useInformationSchematruecharacterEncodingutf8两个参数缺一个都会出问题。如果加了参数还是 null检查建表时到底有没有写 COMMENT很多历史表只有列名没有注释这是数据库规范问题生成器解决不了。5.4 表名大小写Windows能跑Linux连表都找不到现象本地 Windows 连 MySQL 生成并启动一切正常提交到 Linux 服务器后MyBatis 启动报 Table sys_user doesnt exist。原因Windows 上 MySQL 默认 lower_case_table_names1表名大小写不敏感Linux 默认是 0大小写敏感。生成器如果不小心把表名转成了驼峰 SysUser真实表名却是 sys_user在 Linux 上就找不到。解决Mapper XML 里 table 名永远用原始大小写 sys_userMyBatis-Plus 场景则在实体上加 TableName(sys_user)。生成器内部从头到尾保留 rawTableName中途任何位置都不要转驼峰替换掉原始值。这条跟进 2.3 节里强调的原始表名留到最后是同一个问题Double Check 一下生成物里的表名到底是不是原样。5.5 覆盖生成把手写代码全部冲掉现象改完代码重新生成一次手写的加密逻辑、校验分支全没了git diff 一片红同事差点跟你拼命。原因生成器直接 FileWriter 覆盖目标文件没有考虑已有文件里的手工改动也没做备份。解决生成前读旧文件生成后逐行 diff默认只打印差异不落盘加 --force 参数才真正覆盖同时给模板引入代码保护带这是让生成器从一次性脚本变成可反复使用工具的关键做法放在下一章。6. 给生成器加道后悔药dry-run与代码保护带的落地技巧6.1 先写临时目录再对比生成前留一次反悔机会我后来的做法是让生成器默认不直接写目标文件而是先生成到临时目录再和已存在的文件做逐行对比Path preview Files.createTempDirectory(gen-preview); String previewPath preview.resolve(fileName).toString(); engine.render(entity.ftl, table, previewPath); ListString oldLines Files.exists(target) ? Files.readAllLines(target, StandardCharsets.UTF_8) : List.of(); ListString newLines Files.readAllLines(Path.of(previewPath), StandardCharsets.UTF_8); if (!oldLines.equals(newLines)) { System.out.println(diff: target); // 打印差异内容只有加了 --force 才覆盖 }这个习惯是被覆盖生成坑过两次之后养成的。生成器一旦能反复跑数据库修改结构就是日常操作每次 DDL 变更加上一句生成命令先看差异再决定要不要合入比直接覆盖安全一个量级。6.2 代码保护带让手工代码在重生成中存活模板里给业务代码留两个标记重生成时把旧文件标记之间的内容抠出来插回新文件private static final String USER_CODE_START // USER_CODE_START; private static final String USER_CODE_END // USER_CODE_END; public static String preserveUserCode(String oldContent, String newContent) { String block extract(oldContent, USER_CODE_START, USER_CODE_END); if (block null || block.isEmpty()) { return newContent; } return newContent.replace( USER_CODE_START \n USER_CODE_END, block); }这个技法看起来很原始但比任何智能合并都可靠MyBatis Generator 时代大家就靠它保护定制代码。我自己的教训是生成器跑通只是开始真正的工程化在生成之后的合并策略上。把生成器挂在 Maven 编译阶段之前建表脚本一变跑一次生成命令diff 之后合入这套流程我用了很久没出过大事。代码生成器解决的是重复劳动但解决不了建表规范的债——注释写没写、保留字惹不惹事、主键有没有全在源头。把源头管住生成器就是个顺手的利器。希望帮到你。本文还有配套的精品资源点击获取
返回列表