首页 / 资讯中心 / 文章详情

t3code代码生成器实战:从元数据到模板的工程化落地

t3code代码生成器实战:从元数据到模板的工程化落地 ★ FEATURED ARTICLE
1. 从t3code这个标题说起它到底是什么第一次看到t3code这个词我脑子里蹦出来的第一反应是——这大概率是个跟代码生成、模板引擎或者某种编码规范相关的东西。为什么这么说因为t3这个前缀在技术圈里通常有两种指向一种是Template Tier 3或者Type 3的缩写另一种是某个项目内部对第三代架构的代号而code就更直白了要么是源代码要么是编码动作要么是某种代码规范体系。我后来花了不少时间去翻各种技术社区、开源仓库和开发者讨论帖发现t3code这个词并没有一个官方统一的定义。它更像是一个在特定团队或特定项目内部流传的代号被不同的人赋予了不同的含义。有人拿它指代一套轻量级的代码生成脚手架有人用它命名自己写的编码转换工具还有人把它当作某种第三层编码规范的简称。这种模糊性其实挺有意思的——它说明t3code不是一个被大厂背书的标准术语而是一个从实际开发场景里长出来的、带着泥土味的工程实践产物。那这篇文章要聊什么我打算把t3code当作一个代码生成与编码规范落地的实践项目来拆解。具体来说我会围绕以下几个核心问题展开一个代号为t3code的项目通常要解决什么问题它的技术选型逻辑是什么核心模块怎么设计实操过程中有哪些坑以及如果你现在想在自己的团队里落地一套类似的方案应该从哪下手。适合谁看如果你是一个中小团队的技术负责人正在被重复的CRUD代码折磨或者你是一个独立开发者想给自己项目搞一套趁手的代码生成工具又或者你只是对代码自动化这个话题感兴趣想看看别人是怎么把这件事做扎实的——那这篇内容应该能给你一些可以直接抄作业的思路。注意下面所有的技术细节和实操步骤都是基于一个合格的开发者在面对这类需求时最可能采用的合理方案来展开的。因为t3code本身没有公开的官方文档我会把重点放在可复现的方法论上而不是纠结于某个具体实现的细枝末节。2. 为什么需要t3code这类东西重复劳动是最大的敌人2.1 一个真实到让人心疼的场景我先讲一个我亲身经历过的场景。几年前我参与过一个后台管理系统的开发项目不大大概二十来个实体表。听起来不多对吧但每个实体表都要配一套完整的CRUD接口Controller层要写增删改查四个方法Service层要写对应的业务逻辑Mapper层要写SQL映射再加上DTO、VO、Entity三层数据对象的转换。一个实体表从头到尾撸下来熟练工也得小半天。二十个表就是十天。十天里我写的代码除了表名和字段名不一样结构几乎一模一样。那种感觉就像是在流水线上拧螺丝拧到后面手指都麻了脑子完全不转。更可怕的是这种重复劳动带来的不是熟练度的提升而是注意力的涣散——因为太机械了反而容易在某个不起眼的地方写错字段名或者漏掉一个非空校验。这就是t3code这类项目要解决的核心痛点把开发者从重复的、模式化的编码劳动中解放出来让人只关注真正有业务价值的逻辑。2.2 代码生成的三种常见思路在聊t3code的具体设计之前有必要先厘清代码生成这件事的几种主流做法。我把它归纳为三类每一类都有各自的适用场景和取舍。第一种是模板驱动生成。这是最传统也最直观的方式。你准备好一套模板文件里面用占位符标记出可变的部分然后通过一个生成引擎把数据填充进去输出最终的代码文件。Java生态里的MyBatis Generator、Python里的Cookiecutter、前端领域的Plop.js都属于这一类。它的优点是简单直接模板即文档改起来方便缺点是灵活性有限遇到复杂的分支逻辑时模板会变得很难维护。第二种是基于AST的代码合成。这种方式不直接操作文本而是先解析现有的代码结构构建抽象语法树然后在树上做增删改查最后再序列化回代码文本。它的优势是生成的代码在语法层面一定是正确的而且可以做到只改需要改的地方不会覆盖掉开发者手写的逻辑。缺点是实现复杂度高需要依赖特定语言的解析器跨语言支持比较麻烦。第三种是运行时动态生成。这种方式不生成静态的代码文件而是在程序运行时通过反射、动态代理或者元编程的手段动态地创建类和对象。Java里的Spring AOP、Python里的元类、Ruby里的method_missing都是这个思路。它的优点是零编译开销改配置就能生效缺点是调试困难出问题时堆栈信息往往让人摸不着头脑。t3code这个项目从我了解到的零散信息来看大概率是以模板驱动为主、辅以AST解析做增量更新的混合方案。为什么这么判断因为纯模板方案在首次生成时很爽但在二次修改时很痛苦——你改了模板重新生成之前手写的业务逻辑就被覆盖了。而引入AST解析之后生成器可以识别出哪些代码块是自动生成的、哪些是人工编写的从而做到只更新自动生成的部分。这个思路在业界已经被验证过很多次了比如JHipster就是类似的策略。2.3 不搞代码生成行不行有人可能会说现在低代码平台这么成熟直接拖拽生成不就完了何必自己折腾代码生成这个问题我认真想过。低代码平台确实能解决一部分问题但它有两个硬伤一是锁定效应你的业务逻辑都沉淀在平台的私有格式里想迁移成本极高二是表达能力天花板遇到平台不支持的复杂逻辑时你只能绕路或者等平台更新。而代码生成不一样它生成的是标准代码你可以随意修改、随意扩展没有任何锁定。所以我的判断是如果你的团队有持续的前端/后端开发需求且业务模式相对固定那搞一套自己的代码生成工具是划算的。前期投入可能是一到两周但后续每个新模块都能省下大量时间而且生成的代码风格统一代码审查的负担也会小很多。3. t3code的核心模块拆解一个能用的生成器长什么样3.1 元数据层一切从数据源开始任何代码生成器的起点都是元数据。你得先告诉生成器我要生成什么它才能干活。元数据的来源通常有三种数据库表结构、API接口定义文件比如OpenAPI/Swagger、以及手工编写的配置文件。对于t3code这类项目我建议以数据库表结构为主、配置文件为辅。为什么因为数据库表结构是最接近事实的元数据来源字段名、类型、约束都是现成的不需要额外维护。而配置文件可以用来补充那些数据库里表达不了的信息比如字段的中文名称、是否在列表页展示、是否可搜索等等。具体来说元数据层需要抽取的信息包括表级信息表名、表注释、主键字段、索引信息字段级信息字段名、字段类型、是否可空、默认值、注释关系信息外键关联、一对多/多对多关系这些信息通过JDBC的DatabaseMetaData接口就能拿到不需要额外的依赖。下面是一段获取表字段信息的示例代码public ListColumnMeta getColumns(String tableName) { ListColumnMeta columns new ArrayList(); try (Connection conn dataSource.getConnection()) { DatabaseMetaData metaData conn.getMetaData(); ResultSet rs metaData.getColumns(null, null, tableName, null); while (rs.next()) { ColumnMeta col new ColumnMeta(); col.setName(rs.getString(COLUMN_NAME)); col.setType(rs.getString(TYPE_NAME)); col.setNullable(rs.getInt(NULLABLE) DatabaseMetaData.columnNullable); col.setComment(rs.getString(REMARKS)); col.setSize(rs.getInt(COLUMN_SIZE)); columns.add(col); } } catch (SQLException e) { throw new RuntimeException(读取表结构失败: tableName, e); } return columns; }这段代码看起来简单但实际用的时候有几个坑要注意。第一不同数据库驱动对REMARKS字段的支持程度不一样MySQL需要连接串上加useInformationSchematrue才能拿到注释第二TYPE_NAME返回的是数据库方言的类型名比如MySQL返回VARCHAROracle返回VARCHAR2需要做一层映射才能转成Java类型第三如果表里有ENUM或者JSON这类特殊类型处理起来会更麻烦建议在元数据层就做好归一化。3.2 模板层用最笨的办法做最灵活的事模板引擎的选择上我强烈建议用StringTemplate或者Freemarker这类成熟的文本模板引擎而不是自己写字符串拼接。原因很简单字符串拼接在处理缩进、换行、条件分支时极其容易出错而且生成的代码格式惨不忍睹。用模板引擎的话模板文件本身就是一份可读的代码骨架改起来直观维护成本低。t3code的模板层我建议按分层来组织每一层对应一种输出文件templates/ ├── entity.java.ftl # 实体类模板 ├── mapper.java.ftl # Mapper接口模板 ├── mapper.xml.ftl # MyBatis映射文件模板 ├── service.java.ftl # Service接口模板 ├── serviceImpl.java.ftl # Service实现模板 ├── controller.java.ftl # Controller模板 └── dto.java.ftl # DTO模板每个模板文件里通过Freemarker的指令来处理条件逻辑。比如在生成实体类时需要根据字段类型决定是否导入java.math.BigDecimal#list columns as col #if col.javaType BigDecimal import java.math.BigDecimal; /#if /#list这里有个经验之谈模板里不要写太复杂的逻辑。我见过有人在模板里写循环嵌套加条件判断最后模板文件比生成的代码还难读。正确的做法是把复杂逻辑前置到元数据处理阶段模板里只做简单的变量替换和条件渲染。比如字段是否需要在列表页展示这个判断应该在元数据层就算好模板里直接用col.listVisible就行而不是在模板里写一堆if。3.3 生成引擎把元数据和模板撮合到一起生成引擎的核心职责就三件事加载元数据、渲染模板、写入文件。听起来简单但要做好需要处理不少细节。首先是文件路径的规划。生成的代码要放到哪个包下、哪个目录里这个规则要提前定好。我一般会按照Maven的标准目录结构来组织src/main/java/com/example/module/ ├── entity/ ├── mapper/ ├── service/ │ └── impl/ ├── controller/ └── dto/路径的生成逻辑可以写成一个简单的策略类根据表名推导出模块名再拼上固定的后缀。比如表名是t_order去掉前缀t_得到order然后生成Order、OrderMapper、OrderService等类名。其次是文件覆盖策略。这是最容易被忽视但最影响体验的部分。我的建议是首次生成时直接写入文件二次生成时先检查文件是否已存在如果存在对比文件内容只有内容有变化时才覆盖对于用户可能手工修改过的文件比如Service实现类生成时保留用户代码块只更新自动生成的部分最后这个保留用户代码块的功能实现方式是在模板里用特殊的注释标记出自动生成区域// t3code:auto-generated-start public ListOrder listOrders(OrderQuery query) { return orderMapper.selectByQuery(query); } // t3code:auto-generated-end // 以下是用户自定义代码生成器不会覆盖生成引擎在写入文件前先读取原文件把auto-generated-start和auto-generated-end之间的内容替换掉其余部分原样保留。这个机制虽然简单但实用性极强能避免重新生成一次代码手写逻辑全没了的惨剧。3.4 配置层让生成器适应不同项目一个生成器如果只能用于一个项目那价值有限。要让它具备通用性必须把项目相关的配置抽出来。我一般会用一个YAML文件来管理这些配置t3code: basePackage: com.example moduleName: order author: zhangsan tablePrefix: t_ outputDir: ./src/main/java templateDir: ./templates typeMapping: VARCHAR: String INT: Integer BIGINT: Long DATETIME: LocalDateTime DECIMAL: BigDecimal ignoreColumns: - create_time - update_time - deleted这个配置文件里typeMapping是最关键的它定义了数据库类型到Java类型的映射关系。不同项目的映射规则可能不一样比如有的项目用LocalDateTime有的用Date通过配置就能灵活切换。ignoreColumns则用来排除那些公共字段避免在每个实体类里都生成一遍。4. 实操全流程从零搭一套t3code4.1 环境准备与依赖引入假设你用的是Java技术栈Maven项目那第一步就是在pom.xml里引入必要的依赖。核心依赖就三个模板引擎、数据库驱动、以及一个用来简化文件操作的工具库。dependencies !-- Freemarker模板引擎 -- dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version2.3.32/version /dependency !-- MySQL驱动按实际数据库替换 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Hutool工具库简化文件读写 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency /dependencies版本号这块不用太纠结选一个稳定版就行。Freemarker 2.3.x系列已经非常成熟了API基本没变过。Hutool的话如果你不想引入额外依赖用JDK自带的java.nio.file也能完成文件操作只是代码会啰嗦一些。4.2 元数据读取模块的实现元数据读取我前面已经给了核心代码这里补充一下类型映射的处理。数据库类型到Java类型的转换不是一对一的需要根据字段长度和精度做二次判断。比如VARCHAR(10)和VARCHAR(4000)都映射成String没问题但DECIMAL(10,2)和DECIMAL(20,6)都映射成BigDecimal也说得过去。真正需要小心的是TINYINT(1)——在MySQL里它通常表示布尔值但驱动返回的类型名还是TINYINT需要特殊处理。private String mapToJavaType(String dbType, int size, int decimalDigits) { switch (dbType.toUpperCase()) { case VARCHAR: case CHAR: case TEXT: return String; case INT: case INTEGER: return Integer; case BIGINT: return Long; case TINYINT: // TINYINT(1) 通常表示布尔值 return size 1 ? Boolean : Integer; case DECIMAL: case NUMERIC: return BigDecimal; case DATETIME: case TIMESTAMP: return LocalDateTime; case DATE: return LocalDate; case TIME: return LocalTime; default: return String; } }这个映射表不是绝对的你可以根据自己的项目规范调整。比如有的团队强制要求所有金额字段用Long存分那DECIMAL就可以映射成Long。关键是保持一致性整个项目里同一类型的映射规则要统一。4.3 模板编写实战以Controller为例我拿Controller模板来举例因为它的结构最典型包含了注解、依赖注入、方法定义、参数校验等多个要素。package ${basePackage}.${moduleName}.controller; import ${basePackage}.${moduleName}.service.${className}Service; import ${basePackage}.${moduleName}.dto.${className}DTO; import ${basePackage}.common.Result; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; import java.util.List; /** * ${tableComment} 控制器 * * author ${author} * date ${.now?string(yyyy-MM-dd)} */ RestController RequestMapping(/api/${moduleName?uncap_first}) public class ${className}Controller { Resource private ${className}Service ${className?uncap_first}Service; /** * 分页查询${tableComment} */ GetMapping(/list) public ResultList${className}DTO list(${className}Query query) { return Result.success(${className?uncap_first}Service.list(query)); } /** * 根据ID查询${tableComment} */ GetMapping(/{id}) public Result${className}DTO getById(PathVariable Long id) { return Result.success(${className?uncap_first}Service.getById(id)); } /** * 新增${tableComment} */ PostMapping public ResultLong create(Valid RequestBody ${className}DTO dto) { return Result.success(${className?uncap_first}Service.create(dto)); } /** * 更新${tableComment} */ PutMapping(/{id}) public ResultVoid update(PathVariable Long id, Valid RequestBody ${className}DTO dto) { dto.setId(id); ${className?uncap_first}Service.update(dto); return Result.success(); } /** * 删除${tableComment} */ DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { ${className?uncap_first}Service.delete(id); return Result.success(); } }这个模板里有几个细节值得说。第一${className?uncap_first}是Freemarker的内置函数把类名首字母小写用来生成变量名。第二${.now?string(yyyy-MM-dd)}会自动填充当前日期省得每次手动改。第三Valid注解和Result包装类是团队规范的一部分如果你的项目里没有这些模板里去掉就行。提示模板里的注释和空行不要省。生成的代码是要给人看的格式整洁的代码能显著降低后续维护的心理负担。我见过有人为了省事模板里所有换行都去掉生成的代码挤成一坨看着就头疼。4.4 生成引擎的组装与运行把元数据读取、模板渲染、文件写入这三块串起来就是一个完整的生成流程。我一般会写一个CodeGenerator类作为入口对外暴露一个generate(String tableName)方法。public class CodeGenerator { private final DataSource dataSource; private final Configuration freemarkerConfig; private final GeneratorConfig config; public void generate(String tableName) { // 1. 读取元数据 TableMeta tableMeta metadataReader.read(tableName); // 2. 构建模板数据模型 MapString, Object dataModel new HashMap(); dataModel.put(tableName, tableMeta.getTableName()); dataModel.put(tableComment, tableMeta.getComment()); dataModel.put(className, toClassName(tableMeta.getTableName())); dataModel.put(columns, tableMeta.getColumns()); dataModel.put(basePackage, config.getBasePackage()); dataModel.put(moduleName, config.getModuleName()); dataModel.put(author, config.getAuthor()); // 3. 遍历模板逐个渲染 for (TemplateConfig template : config.getTemplates()) { String outputPath buildOutputPath(template, dataModel); String content renderTemplate(template.getPath(), dataModel); writeFile(outputPath, content); } } private String renderTemplate(String templatePath, MapString, Object dataModel) { try { Template template freemarkerConfig.getTemplate(templatePath); StringWriter writer new StringWriter(); template.process(dataModel, writer); return writer.toString(); } catch (Exception e) { throw new RuntimeException(模板渲染失败: templatePath, e); } } }运行的时候你可以写一个main方法把要生成的表名传进去public static void main(String[] args) { CodeGenerator generator new CodeGenerator(dataSource, config); generator.generate(t_order); generator.generate(t_user); generator.generate(t_product); }如果表比较多可以从数据库里查出所有表名批量生成。但我不建议一上来就全量生成最好先拿一张表试水确认生成的代码符合预期之后再批量操作。5. 踩坑记录与常见问题排查5.1 生成代码编译不过的几种典型情况这是最常见的问题生成的代码看着没问题一编译就报错。我整理了几种高频情况问题现象根本原因解决方法找不到符号BigDecimal模板里用了BigDecimal但没生成import在模板开头根据字段类型动态生成import方法重复定义二次生成时覆盖策略有问题检查auto-generated标记是否正确泛型类型不匹配元数据里字段类型映射错误核对typeMapping配置缺少Override注解模板里漏写了在Service实现类模板里补上包名拼写错误basePackage配置有误检查YAML配置文件其中方法重复定义是最隐蔽的。有一次我生成完代码编译时报了一堆方法已存在的错误查了半天才发现是生成引擎在写入文件时把新内容追加到了旧内容后面而不是替换。后来加了auto-generated标记才解决。5.2 字段类型映射的边界情况数据库里的类型五花八门标准类型好处理但遇到一些特殊类型就容易翻车。我列几个我踩过的坑TINYINT(1)的布尔陷阱。MySQL里TINYINT(1)通常用来存布尔值但JDBC驱动返回的TYPE_NAME还是TINYINTCOLUMN_SIZE是1。如果不做特殊判断生成的Java字段就是Integer跟实际语义不符。解决办法就是在类型映射时加一个判断TINYINT且size 1时映射成Boolean。JSON类型的处理。MySQL 5.7之后支持JSON类型但JDBC驱动返回的TYPE_NAME是JSON标准Java类型里没有对应的。我的做法是映射成String然后在实体类里加一个TableField(typeHandler JacksonTypeHandler.class)注解让MyBatis-Plus自动做JSON序列化。无符号整数的坑。MySQL支持INT UNSIGNED但Java里没有无符号类型。如果字段值可能超过Integer.MAX_VALUE就得映射成Long。判断方法是看COLUMN_SIZEINT UNSIGNED的size是10而普通INT是11因为要留符号位。5.3 生成器与现有代码的冲突处理在一个已有项目里引入代码生成器最大的挑战不是技术而是人的习惯。你生成了一套代码但同事之前手写了一套类似的两套代码并存维护起来就是灾难。我的建议是先小范围试点再逐步推广。选一个全新的模块用生成器生成全套代码跑通之后让团队review。大家觉得没问题了再约定以后新模块统一用生成器。对于已有的老代码不要强行替换保持原样就行等哪天需要大改的时候再顺手重构。另外生成器的配置文件一定要纳入版本管理。typeMapping、ignoreColumns这些配置不同人可能有不同意见放在Git里大家都能看到变更历史减少扯皮。5.4 性能与批量生成的注意事项单表生成通常几百毫秒就完成了但如果要批量生成几十张表就得注意一下性能。主要瓶颈在数据库元数据读取和文件IO上。元数据读取这块建议一次性把所有表的元数据读出来缓存到内存而不是每生成一张表就查一次数据库。DatabaseMetaData.getColumns()这个方法在有些数据库上性能很差反复调用会明显拖慢速度。文件IO这块如果生成的目录不存在需要先创建。Files.createDirectories()是幂等的重复调用不会报错可以放心用。另外写入文件时建议用StandardOpenOption.CREATE和StandardOpenOption.TRUNCATE_EXISTING确保文件不存在时创建、存在时覆盖。注意批量生成前一定要先备份代码或者确保项目在Git的干净状态下。我吃过一次亏批量生成时配置写错了把几十个文件全覆盖了幸好有Git能回滚不然就得加班重写了。6. 进阶玩法让t3code更贴合你的团队6.1 自定义模板扩展点基础的CRUD生成只是起点真正让生成器有价值的是贴合业务的自定义模板。比如你们团队有一套统一的日志切面、权限注解、或者缓存策略这些都可以做成模板的一部分。我举一个实际的例子。我们团队要求所有Service方法都要加操作日志格式是OpLog(module 订单, action 创建)。这个注解如果每次手写很容易漏掉或者写错。把它做到模板里生成的时候自动填充模块名和操作名就省心多了。OpLog(module ${tableComment}, action 创建) public Long create(${className}DTO dto) { // ... }自定义模板的关键是抽象出团队内部的通用模式。你观察一下团队里重复度最高的代码片段是什么把它提炼成模板就能显著提升生成器的实用价值。6.2 与CI/CD流程的集成生成器不一定非要手动运行也可以集成到CI/CD流程里。比如在代码提交时通过Git Hook触发一次生成确保生成的代码和最新的表结构保持同步。具体做法是写一个pre-commit钩子检查schema.sql或者数据库迁移文件是否有变更如果有就自动运行生成器然后把生成的文件加入本次提交。这样能避免表结构改了但代码没更新的问题。不过这个做法有争议。有人觉得自动生成的文件不应该进版本库应该在构建时动态生成。我的看法是如果生成的代码需要人工修改那就应该进版本库如果完全是自动生成、不需要人工干预的那可以放到.gitignore里构建时再生成。大多数情况下Service实现类和Controller都需要人工补充业务逻辑所以还是进版本库比较稳妥。6.3 从代码生成到代码规范检查t3code这个名字里的code除了指代代码生成也可以延伸到代码规范。生成器保证了代码的初始结构是规范的但后续的人工修改可能会破坏这种规范性。所以配套的代码规范检查工具就很有必要。我一般会在项目里集成Checkstyle或者Spotless定义一套团队统一的代码格式规则。生成器生成的代码天然符合这些规则人工修改时如果不符合提交时就会被拦下来。这样生成检查形成闭环代码质量就有了双重保障。Spotless的配置很简单在pom.xml里加一个插件plugin groupIdcom.diffplug.spotless/groupId artifactIdspotless-maven-plugin/artifactId version2.43.0/version configuration java googleJavaFormat version1.19.2/version /googleJavaFormat removeUnusedImports/ /java /configuration /plugin配置好之后运行mvn spotless:apply就能自动格式化代码。生成器生成的代码和Spotless格式化的结果一致就不会出现生成完还要手动格式化的尴尬。6.4 多数据库方言的支持如果你的项目需要支持多种数据库比如开发用MySQL生产用Oracle那生成器就得处理方言差异。核心差异在两个方面类型映射和SQL语法。类型映射前面已经聊过了通过配置文件就能解决。SQL语法的差异主要体现在分页查询上MySQL用LIMITOracle用ROWNUMPostgreSQL用LIMIT ... OFFSET。如果生成的Mapper XML里写死了LIMIT换数据库就跑不了。解决办法是在模板里根据dbType变量做条件渲染#if dbType mysql LIMIT #{offset}, #{limit} #elseif dbType oracle WHERE ROWNUM #{offset limit} #elseif dbType postgresql LIMIT #{limit} OFFSET #{offset} /#if这样一套模板就能适配多种数据库切换数据库时只需要改配置不用动模板。7. 我个人的一些实操心得7.1 生成器不是银弹别指望它解决所有问题我见过一些团队引入代码生成器之后恨不得所有代码都自动生成连复杂的业务逻辑都想用模板拼出来。结果就是模板越来越臃肿维护模板的成本比手写代码还高。我的经验是生成器只负责生成结构不负责生成逻辑。CRUD的骨架、数据对象的定义、接口的签名这些适合生成而具体的业务规则、条件分支、异常处理这些应该留给人来写。生成器把开发者从重复劳动中解放出来但解放出来的时间应该用来思考业务而不是继续折腾生成器。7.2 模板的维护成本要提前考虑模板文件本质上也是代码也需要维护。当团队的技术栈升级时比如从Spring Boot 2.x升到3.x模板也得跟着改。如果模板数量多、逻辑复杂升级成本会很高。所以我在设计模板时会刻意保持模板的简洁。能用配置解决的不写进模板能通过元数据预处理的不写进模板。模板里只保留最核心的结构定义这样升级时改动量最小。7.3 文档和示例比代码本身更重要生成器是给团队用的不是给你一个人用的。如果别人不知道怎么用、不知道怎么改模板那这个生成器就只是个玩具。我一般会配套写三样东西一份README说明怎么配置和运行一份模板编写指南说明各个变量的含义以及一个完整的示例项目展示生成前后的效果。这三样东西写好了团队里其他人就能自助使用不用每次都来问你。7.4 版本管理要跟上生成器本身也要有版本号。每次修改模板或者生成逻辑都应该打一个tag记录变更内容。这样当生成的代码出现问题时可以快速定位到是哪个版本的生成器导致的。我习惯在生成的代码文件头部加一行注释标明生成器版本// Generated by t3code v1.2.0这样一看就知道是哪个版本生成的排查问题方便很多。7.5 不要过度设计最后一条也是最重要的一条不要过度设计。我见过有人把代码生成器做成了一个平台有Web界面、有权限管理、有生成历史记录结果维护成本极高最后没人用。对于大多数团队来说一个命令行工具加一套模板文件就够了。简单、直接、够用这才是工程实践该有的样子。等真的有了更复杂的需求再逐步迭代也不迟。代码生成这件事说到底就是用一次性的投入换取长期的效率提升。投入多少、怎么投入取决于团队的规模和业务的复杂度。但不管怎样先跑起来再优化永远比停留在设计阶段强。
阅读完成 · 觉得有帮助?
咨询建站