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

t3code:基于三层架构的代码生成器设计与实践

t3code:基于三层架构的代码生成器设计与实践 ★ FEATURED ARTICLE
t3code这个词乍看像是某个漏洞编号或者是公司内部的项目代号但在我手里它是一套折腾了大半年的开发脚手架。简单说t3code是我自己定义的一套“三层架构”代码生成方案专治中小型项目中那些重复得让人头皮发麻的增删改查代码。这套东西解决的核心痛点其实就一个当你的业务表从二十张涨到八十张而每张表都需要后端Controller、Service、Mapper以及前端List页、Form页时再勤快的程序员也会崩溃复制粘贴都能把手粘秃噜皮。这篇文章就聊聊我是怎么设计t3code的它的核心逻辑、技术选型以及实际落地时踩过的坑希望能给正在被样板代码折磨的朋友一点参考。如果你是个刚接触项目开发不久的新手或者是在团队里负责后端但又不得不顺手写前端页面的“全栈苦力”再或者你已经在用MyBatis-Plus、Spring Boot这类框架但觉得代码生成器生成的东西还是不够贴合自己项目习惯——那么t3code这套思路应该对你有用。它不是什么惊天动地的创新但足够实用而且你完全可以在看完后复制这套逻辑构建属于自己的代码生成工具。1. 项目整体设计与思路拆解1.1 为什么需要t3code重复劳动的本质先算笔账。一张业务表落地成完整功能通常需要以下几种代码后端实体类Entity、数据传输对象DTO、数据访问对象DAO/Mapper、服务接口Service接口、服务实现ServiceImpl、控制器Controller。前端列表页用于查询、分页、删除、表单页用于新增、编辑、校验、接口请求封装API JS文件、路由配置。一张表算下来少说十来个文件其中至少百分之七十是纯模板代码。比如Controller里就是“接收参数、调用Service、返回统一结果”三板斧Entity里就是“数据库字段一一映射、getter/setter”的机械工作。这些代码没有技术含量却占了开发时间的大头。很多团队的实际开发效率瓶颈不是业务逻辑难写而是这些样板代码把精力耗光了。t3code的定位就是把这些固定结构的代码通过模板和配置自动生成。它的核心设计思路可以拆成三个要点第一生成代码必须符合项目规范包括命名规范、包结构规范、统一返回格式规范第二生成代码必须可独立运行也就是生成完不需要手改太多地方就能直接跑第三生成器本身必须够灵活因为不同项目的表结构、注释习惯、字段风格千差万别不能写死。1.2 技术选型的深坑与决策t3code最初版本我试图用纯Java定时任务加Freemarker模板来搞后来发现预见性太差——因为代码生成不只是“读表结构、填模板”它还要处理很多边角问题。比如数据库字段类型如何映射到Java类型tinyint是映射成Integer好还是Boolean好比如表名user_info如何转成实体类名UserInfo、驼峰命名转换的边界条件是什么再比如如果数据库字段有注释怎么把注释自动写到字段上方的Javadoc里这些逻辑如果全堆在一段脚本里会变得难以维护。所以后来t3code换了个思路核心引擎用Java写模板层用Freemarker配置层用YAML输出的目标语言支持Java和TypeScript。这样做的原因很简单Freemarker对Java后端开发者最友好调试起来直接看到渲染异常YAML配置天然支持注释和层级结构适合描述“表前缀、包名、输出路径”这类元信息。其实做这种工具最忌讳的就是贪多嚼不烂。有人上来就想做“一键生成前后端所有模块”结果模板套了一层又一层到最后生成的代码没人看得懂反而比手写还难维护。t3code的收敛原则是只生成“结构稳定”的部分业务特殊逻辑一律预留扩展接口不在模板里写死。1.3 t3code适合什么场景从实践来看t3code最适合的场景是管理后端这类CRUD密集型的项目。什么用户管理、订单管理、商品管理、配置管理这类功能的数据结构清晰、操作模式固定是代码生成器的天然适用区。但如果你要做的是类似实时推荐引擎、复杂的规则引擎类似于工作流审批这种状态流转极度复杂的模块强行用生成器反而会束手束脚这时候手写核心逻辑、用生成器只做外围接口层可能才是正解。所以在设计之初我就定了一个原则t3code生成的是“框架内代码”而不是“业务内代码”。它负责把Controller、Service接口、ServiceImpl骨架、Mapper接口、实体类、前端List页和Form页的壳子搭建好但Service实现类中的业务方法默认只生成空实现并抛出TODO提示实际逻辑需要开发者自己填。2. 核心细节解析与实操要点2.1 数据库元数据读取一切的起点生成代码的第一步是读取数据库表结构这一步的质量直接决定后面所有步骤的对错。t3code使用JDBC的DatabaseMetaData接口来读取表信息、字段信息、主键信息和注释信息。这里面有非常多的细节容易踩坑。举个例子MySQL的tinyint(1)在多数ORM框架里会被映射为Boolean但实际业务中tinyint(1)也可能用来表示状态值0/1/2这时候直接映射Boolean就炸了。t3code的解决方案是在配置文件中增加一个字段类型覆盖表用户可以手动指定某些表如果用了tinyint(1)就映射为Integer而不是Boolean。这个覆盖逻辑优先级最高其次才是默认映射规则。再比如注释问题。MySQL中通过SHOW FULL COLUMNS FROM table_name能拿到字段的注释但JDBC的getColumns()方法在不同驱动版本下表现不一致。我试过某些版本的MySQL Connector/J返回的REMARKS字段就是空的。这种情况下需要回退到执行SHOW FULL COLUMNS原生SQL来补全注释信息。这个坑在t3code的早期版本里出现过后来做了一个兜底策略优先用getColumns()获取基本信息如果发现注释为空且表不为空就自动执行原生SQL重试一次。2.2 模板设计的核心原则分离稳定与可变Freemarker模板的设计是整个t3code的灵魂环节。如果模板写得不好生成的代码风格就会五花八门甚至出现语法错误。t3code的模板目录结构是这样的t3code-templates/ ├── java/ │ ├── entity.ftl │ ├── mapper.ftl │ ├── service.ftl │ ├── serviceImpl.ftl │ └── controller.ftl ├── frontend/ │ ├── api.ftl │ ├── list.vue.ftl │ └── form.vue.ftl └── config/ ├── application.yml └── generator.yml每个模板文件内部又划分为几个固定的块文件头注释块、包名和导入块、类声明块、字段定义块、方法实现块。这样分块的好处是当项目规范发生变化时比如统一返回结果类从R改名为Result只需要将模板里对应位置的变量改掉全项目代码风格瞬间统一。在设计模板时有一个必须注意的原则尽可能减少模板中的条件判断。如果模板里充斥着#if field.type String这类分支说明模板的抽象层级不够。t3code的做法是在Java代码层面预先处理好字段类型、导入类型集合、注解字符串、字段名转换结果以“准备好的数据模型”传给模板。模板只管按既定格式输出不做复杂逻辑判断。这样既提升了模板的可读性也方便后期维护。2.3 命名策略一道容易忽视的送命题代码生成器最容易让人抓狂的就是命名。user_login_log这种表名到底应该生成UserLoginLog还是UserLoginloglogin_time这个字段应该是loginTime还是logintime别看这些都是小事生成的代码风格不统一后期review时能被喷死。t3code内部实现了一套完整的小驼峰转换逻辑处理了下划线转驼峰、连续大写缩写、数字前缀等等边界情况。此外还支持自定义表前缀剥离比如所有表名都以t_开头生成实体类时自动去掉这个前缀。t_user_info生成UserInfo而不是TUserInfo。这个功能在配置里加了一个table-prefix参数generator: datasource: url: jdbc:mysql://localhost:3306/t3code_demo username: root password: root table-prefix: t_ package-name: com.example.demo output-path: ./output命名转换还有一层需要仔细处理就是数据库字段名和Java字段名的映射。如果数据库里有的字段已经用了驼峰命名比如loginTime而有的用了下划线命名login_time这时候直接做转换就会出现双重转换的BugloginTime被转成logintime。t3code的解决方案是转换前先判断字段名中是否已经包含大写字母如果包含则保持原样仅做首字母小写处理否则才执行下划线转驼峰逻辑。2.4 配置设计宁可配置多不可魔法值一个工具好不好用配置设计能占一半的功劳。t3code的配置分为三块数据源配置、生成规则配置、模板路径配置。为了满足不同项目的需要生成规则配置支持三种作用域全局配置对所有表生效、表级配置针对单表覆盖全局、字段级配置对某个字段单独覆盖。我举一个字段级配置的实际例子。在某些业务表里逻辑删除字段叫deleted类型是char(1)值为0和1。在t3code的全局配置里这段字段会被映射为String类型。但如果你某张表里恰好有一个名为deleted的字段实际上存的是Integer类型那么就可以通过表级配置覆盖指定这张表的deleted字段映射为Integer。这种灵活度在初期可能用不上但项目大了之后你会发现它能避免很多极端但真实存在的坑。配置文件的解析使用SnakeYAML库因为它的生态成熟、兼容性好。同时t3code支持配置热加载——修改YAML文件后重新执行生成命令不需要重新启动整个生成器只需要监听文件变化然后重新解析即可这在调试模板时特别节省时间。3. 实操过程与核心环节实现3.1 环境准备与快速启动要实际跑一遍t3code前提条件如下JDK 8及以上版本推荐JDK 11因为部分依赖在JDK8下编译有兼容问题Maven 3.6或者直接用打包好的可执行JARMySQL 5.7t3code的元数据读取逻辑基于MySQL其他数据库需要自行适配把项目clone下来之后直接修改application.yml中数据源指向你自己的数据库然后执行java -jar t3code-cli.jar --config./config/generator.yml --tableuser_info,order_info命令行支持两个核心参数--config指定配置文件路径--table指定要生成代码的表名多个表用逗号分隔。如果不指定--table默认读取数据库中所有表这个功能通常只在初始化阶段使用平时不建议全量生成因为会覆盖你之前手写的部分文件。首次执行成功后输出目录下会出现如下结构output/ ├── java/com/example/demo/ │ ├── controller/UserInfoController.java │ ├── service/UserInfoService.java │ ├── service/impl/UserInfoServiceImpl.java │ ├── mapper/UserInfoMapper.java │ └── entity/UserInfo.java └── vue/src/api/userInfo.js └── vue/src/views/userInfo/List.vue └── vue/src/views/userInfo/Form.vue3.2 核心引擎实现源码解析t3code的执行流程可以简化为五步加载配置、读取表结构、构建数据模型、渲染模板、输出文件。其中构建数据模型这一步是最核心的。数据模型DataModel的构建过程本质上就是把数据库字段类型转换为编程语言类型并组装好生成代码所需的所有上下文信息。以Java实体类为例t3code会构造以下数据实体类名、包名、导入的类列表如LocalDateTime、BigDecimal等字段列表字段名、类型、注释、是否主键、是否自增表名、表注释是否启用MyBatis-Plus注解如TableName、TableId、TableField字段类型转换规则是从配置的映射表中读取的public class TypeMapper { private MapString, String javaTypeMap new HashMap(); public String map(String jdbcType, int columnSize) { // tinyint特殊处理逻辑 if (TINYINT.equalsIgnoreCase(jdbcType) columnSize 1) { return Boolean; } return javaTypeMap.getOrDefault(jdbcType.toUpperCase(), String); } }这段代码里有一个容易忽略的细节TINYINT长度为1的情况返回Boolean但如果在配置文件中人为指定了覆盖规则则覆盖规则的优先级更高不会走到这段逻辑里。3.3 前端代码生成的特殊处理前端代码生成是t3code从第二版开始加入的能力也是被团队同事评价“真香”的功能。生成Vue页面时默认使用Element UI组件库这是因为Element UI的表格和表单组件在管理后台场景下足够成熟、健壮而且组件API相对稳定。列表页List.vue的模板逻辑如下查询区自动生成根据字段列表中“查询标记”为true的字段生成输入框、下拉框或日期选择器。表格列自动生成默认展示所有字段主键列自动设置为索引列长文本字段如备注、描述自动隐藏或设置为tooltip展示。分页功能自动接入默认调用分页接口参数为pageNum和pageSize。表单页Form.vue的模板逻辑则更复杂一些根据字段能否为空的标记生成对应的校验规则。根据字段类型映射到不同的表单项组件varchar映射为inputtext映射为textareadate/datetime映射为date-pickerint/decimal映射为input-number。如果有枚举字段配置文件中定义会自动生成select下拉选项。这里有一个比较有意思的细节。对于日期范围查询比如“创建时间”字段通常前端会传一个包含两个日期字符串的数组但后端接口接收的是两个独立的参数。这个矛盾在模板层用了一个约定如果字段名为createTime那么在列表页查询区自动生成一个日期范围选择器并将查询参数拆分为createTimeStart和createTimeEnd两个字段分别传递给后端。这种约定优于在后端写复杂的时间解析逻辑。3.4 生成后的代码如何集成进项目生成是一回事用起来又是另一回事。如果生成的东西和你的项目基建不兼容那还不如不生成。t3code在设计时就把这个问题考虑在内了。标准情况是你已有的项目使用了MyBatis-Plus那么t3code生成的实体类会自带TableName、TableId、TableField等注解直接丢进项目就能用。Service接口继承IServiceUserInfoServiceImpl继承ServiceImplMapper, EntityController继承一个统一的BaseController这个BaseController里封装了分页查询、保存、更新、删除、批量删除等通用方法。只要你的项目里有对应的BaseController生成出来的Controller就会变得极其精简甚至只包含自定义接口的入口方法。但如果你没有用这些框架t3code也提供了一个common模式开关在配置中设置framework: mybatis-plus或framework: plain。plain模式下不会生成任何框架注解只会生成纯粹的POJO和Mapper接口方便你整合到自己的DAO体系中。集成过程中最常见的问题是包名不一致。t3code生成的代码包名基于配置中的package-name但很多项目的实际路径前缀是com.company.product。解决办法很简单先让t3code生成到一个临时目录然后用IDE的Move操作整体迁移到目标包路径下IDEA会自动修改所有import语句。这比自己一个个改要高效得多。4. 常见问题与排查技巧实录4.1 模板渲染报错Freemarker常见陷阱用Freemarker写模板有一类错误非常隐蔽——空变量处理。比如一张表没有主键时模板里引用primaryKey.name如果primaryKey本身为null渲染直接抛异常。而如果你加了#if primaryKey??判断又会遇到另外一个问题很多版本的Freemarker在空字符串和null之间行为不一致。t3code的兜底方案是在Java层构建数据模型时对可能为空的字段做统一的默认值填充。比如主键对象如果为null就填充一个PrimaryKey.UNKNOWN常量对象模板里永远不会遇到null引用。这套思路比在模板里到处加空值判断靠谱得多也让模板看起来更干净。另一个常见问题是换行和缩进混乱。Freemarker模板中如果你写了类似#list.../#list标签本身的换行会被原样输出到生成文件中。这会导致生成的Java代码空行特别多风格混乱。解决方法是模板中使用#compress标签包裹或者在写模板时把#list和#if标签放在同一行只让标签内部的表达式产生输出。4.2 数据库方言差异换个数据库就崩溃t3code最初的版本只适配了MySQL后来有同事拿到PostgreSQL上跑结果读取表结构信息时出来的字段类型完全对不上。PostgreSQL的大写字段名会被自动小写化、serial类型被映射成了Integer而没有识别出自增、注释信息需要通过专门系统表读取。这个问题的根本原因是JDBC元数据在不同数据库驱动下的表现不一致。t3code2.0开始将“元数据读取”逻辑抽象为接口MetadataProviderMySQLProvider和PostgreSQLProvider分别实现各自方言的读取逻辑。目前已经适配了MySQL、PostgreSQL、达梦国产化场景Oracle因为手头没有测试环境还没完全适配但接口设计上预留了扩展点。如果你在自己项目里集成t3code并且需要支持其他数据库只需要实现MetadataProvider接口重写getTables()、getColumns()、getPrimaryKeys()三个方法即可。这三个方法返回统一的数据结构TableMetaData和ColumnMetaData下游的模板渲染逻辑完全不用改。4.3 覆盖生成导致手写代码丢失血的教训这是一个非常严重的坑。最早用t3code给同事生成代码时他拿到UserInfoServiceImpl.java后在里面手写了很多自定义业务方法。结果因为调整了模板格式重新执行了一次生成操作整个文件被覆盖手写的代码全没了又花了一下午重新写。这个教训直接推动了t3code两个重要功能的落地。第一个功能是“仅生成缺失文件”模式生成前检测目标文件是否存在如果存在且用户未指定--force参数则跳过该文件。第二个功能是“自定义方法保护段”模板生成的Service实现类中默认在文件末尾生成如下结构// 自定义业务方法开始 // 此区域通过t3code生成后保留不会被重新生成覆盖 // 自定义业务方法结束 t3code在重新生成时读取已存在的文件提取保护区域内的内容用正则表达式替换掉新文件中对应的位置。实测下来这招非常有效只要开发者遵守“手写代码放在这个区域内”的约定反复重新生成也不会丢失自定义逻辑。4.4 编码与格式化问题生成的代码经常出现中文乱码和缩进不对的问题这个在Windows和Linux环境下表现不一致。t3code统一约定模板文件和输出文件编码为UTF-8输出文件末尾统一加一个换行符。生成后如果用的是IDEA建议做一次Code - Reformat Code因为Freemarker渲染出来的缩进和IDEA默认风格还是有一些细微差异尤其是链式调用和注解换行的情况。还有一个经验是Controller层的代码不要生成得太智能。我见过有些代码生成器试图在Controller层自动判断DTO与Entity的转换结果生成的代码里全是BeanUtils.copyProperties而且属性名一发生变化就悄悄失效。t3code生成的Controller只做最基础的事——接收参数、调用Service、返回结果。涉及参数转换的在Service实现里手动处理。这虽然让每个接口多写了那么四五行代码但代码的可读性和健壮性都大幅提升。5. 性能优化与高级配置技巧5.1 大批量表结构生成时的性能瓶颈如果你数据库里有几百张表一次性全量生成时每张表都需要建立数据库连接、读取列信息、读取注释、读取主键这些操作叠加起来的耗时可能长达数分钟。t3code的优化策略如下使用连接池HikariCP避免每次读取都建立新连接。合并元数据查询语句在MySQL下改用一条information_schema查询获取所有表信息而不是逐个表DESC。多表生成时使用线程池并行处理默认线程数为4可以根据机器配置调整。实测优化后在三百张表左右的项目上全量生成时间从五分钟缩短到四十秒左右。不过这里提醒一句并行生成时模板渲染和数据模型构建是线程安全的但文件写入时要留意不同线程可能写同一个目录需要做好目录初始化同步。5.2 自定义模板进阶玩法t3code支持用户自定义模板目录你完全可以在templates/custom下放置自己的模板。自定义模板的优先级高于内置模板同名文件会覆盖内置模板。比如你想调整实体类注释的格式或者想给Controller自动添加权限注解PreAuthorize只需要修改对应的controller.ftl模板并重启生成。但这里也有个容易踩的坑如果你在自定义模板中引用了一个内置模板才有的变量而t3code版本升级后这个变量被移除了你的模板渲染就会直接报错。建议在使用自定义模板时锁定t3code的版本升级前先跑一个最小测试集验证模板是否仍然有效。5.3 集成到Maven构建流程对于追求自动化的团队可以把t3code做成一个Maven插件绑定到generate-sources阶段这样每次编译前自动检查表结构变化如果表结构有更新就自动重新生成。我在项目里是这么配的plugin groupIdcom.t3code/groupId artifactIdt3code-maven-plugin/artifactId version2.1.0/version configuration configFile${project.basedir}/src/main/resources/generator.yml/configFile tableNamesuser_info,order_info/tableNames overwritefalse/overwrite /configuration executions execution phasegenerate-sources/phase goals goalgenerate/goal /goals /execution /executions /plugin不过集成到构建流程的做法有利有弊。好处是团队新成员拉下代码后不需要额外执行生成命令直接编译就能拿到最新代码。坏处是如果表结构发生变化而开发者并不知情编译时静默更新了代码可能导致debug时明明改了数据库字段但代码没同步。所以建议overwrite始终设为false让生成器只补缺失文件。这样即使执行了插件也不会破坏已有代码稳定性优先。6. 写在最后的一点体会t3code这个项目做了大概有半年多前前后后重构了三次最深的体会就是工具造得好不好取决于你对自己项目的理解有多深。代码生成器不是一个能“解决所有问题”的神器它真正擅长的是把那些“写起来无聊但还不得不写”的部分吞噬掉。人的精力应该放在异常流程处理、复杂的业务判断、性能优化这些真正需要思考的事情上而不是浪费在复制、粘贴、改类型、改名字这些机械操作上。如果你也想做一个类似的代码生成工具我的建议是先不要急着写模板先把你项目里现有的、你认为写得“最好的”Controller和Service代码拿出来看一眼。把那些反复出现的结构和散落在各个文件里的固定注释、固定注解、固定返回格式提取出来再开始设计模板。你会发现生成器本质上不是创造了一套新规范而是把你项目里已经存在但没人固化的隐性规范显性化了。这一步想通了后续的一切不过是按图索骥。
阅读完成 · 觉得有帮助?
咨询建站