资讯动态

T3Code:面向Java后端的代码生成工作流设计与实践

发布时间:2026/10/26 17:02:01 来源:尧图企业网站定制
1. T3Code不是低代码平台它的真实定位与设计动机先交代一下背景。我所在团队大概三十个人分了五条业务线后端基本都是同一套技术栈Java、Spring Boot、MyBatis-Plus数据库是MySQL。听起来挺规整对吧但实际上手之后你会发现光是“创建一个标准CRUD模块”这件事每个人写出来的东西都能差出十万八千里。有人把查询逻辑全塞在Controller里有人习惯在Service层堆几百行业务代码还有人对分页参数的处理方式跟其他人完全不一致。代码风格倒是其次真正可怕的是接口签名、错误码格式、参数校验方式各搞一套联调的时候前后端互相甩锅是常态。我当时就在想一个问题既然大家对“标准模块长什么样”其实是有共识的那为什么不能把这个共识固化成一套东西让机器来生成这就是T3Code项目的起点。T3Code不是低代码平台它不做可视化拖拽也不打算让业务人员直接操作。它本质上是一套代码生成工作流核心是把“需求描述”转换成“工程代码”并且保证产出的代码符合团队规范。说白了T3Code是给开发人员用的提效工具不是给业务人员用的搭建平台。为什么叫T3三个T分别对应整个系统的三个核心要素Task任务、Template模板、Type类型契约。后面我会详细拆这三个东西但先记住一句话T3Code不是靠一个万能模板生成所有代码而是把需求拆成若干种标准任务每种任务匹配专门的模板所有模板共享一套类型定义。这样生成的代码既能保持统一规范又不会因为“一个模板套万物”而变得僵硬。这个定位是我们反复权衡后定的。最早其实纠结过要不要上AI生成代码用大模型直接根据自然语言描述输出代码。但评估完发现几个硬伤。第一大模型输出的代码不可预测今天生成的接口风格跟明天生成的可能是两回事团队规范没法约束它。第二审计成本高AI写出来的代码你得一行行review它内部帮你做了哪些决策你根本不知道。第三也是最重要的团队的脚手架逻辑其实是稳定的我们把可预测的东西用模板固化下来剩下的非标逻辑才值得花心思去设计这才是合理的分工。所以T3Code的适用范围很清晰适用于那些“有标准套路、有固定结构、可枚举可归纳”的代码产出场景。比如新增一个业务模块的增删改查、导出导入、字典管理、消息通知模板这类需求。反过来适用范围之外的场景也不要硬套比如复杂状态机流转、跨模块事务编排、需要深度业务判断的逻辑这些要么让模板预留扩展点要么干脆手写。搞清楚这个边界T3Code的价值才能发挥出来不会变成那种“为了用工具而用工具”的尴尬项目。另外说说适合谁参考这篇内容。如果你是搞后端基建、负责团队工程效率、或者正在纠结要不要自研一套代码生成器T3Code的整套设计思路和踩坑记录应该能给你不少启发。哪怕你团队只有五六个人只要存在“重复写同一类接口”的痛点这套思路就能迁移过去。2. 三要素如何驱动出码从需求描述到代码落地的完整链路先看整个T3Code的工作流程长什么样然后逐个拆解每个环节做了什么、为什么要这么设计。一条需求进入T3Code首先会转成一份schema描述文件我用YAML写比JSON可读性好也方便写注释。这份文件描述的是“这一条需求要做什么”而不是“代码长什么样”。举个例子如果需求是“为订单模块新增一个按订单号查询订单详情的接口”那schema大致长这样module: order task: query_detail entity: Order method: GET path: /api/v1/order/detail params: - name: orderNo type: string required: true rule: length_32 response: entity: OrderDetailVO description: 按订单号查询订单详情注意这份描述里的关键字段模块名、任务类型、实体名、入参出参、路由信息。它不涉及任何Controller怎么写、Service怎么组织、Mapper怎么实现——这些是模板的事。T3Code拿到这份schema之后会走一条完整的处理链路第一步任务规划器解析schema拆解出具体任务列表。这个环节是整个系统里最像“思考”的部分。同样是“订单模块查详情”任务规划器会拆出几个子任务生成Controller层接口、生成Service层方法签名和骨架实现、生成Mapper查询方法、生成DTO类、生成参数校验逻辑、注册路由、生成单元测试骨架。每个子任务本质上就是一个Task实例携带自己的上下文数据。为什么不用一个大模板直接渲染整个模块因为不同任务类型之间的组合方式太多了如果全部堆在一个模板里那个模板的复杂度会失控。把任务拆细每个Task对应一个或一组小模板渲染和调试都容易得多。第二步任务分派器根据Task类型查找对应的模板集合。每种Task类型都注册了它需要的模板列表比如“生成Controller”任务可能涉及Controller模板加一个错误码映射片段。查表过程很简单核心在模板注册表怎么维护这个后面讲模板分层的时候细说。第三步模板引擎渲染。这里关键点在于上下文数据从哪来。模板拿到的变量不是凭空产生的——一部分来自schema解析结果另一部分来自Type类型系统推导。比如模板要输出“实体对应的数据库表名”这个映射不是写死在模板里的而是通过实体类型去查全局的映射表拿到的。这样才保证模板和业务数据解耦。第四步后处理器整理产物。模板渲染出来的东西不能直接落盘。后处理器会统一格式化代码风格、补充缺失的import、生成或更新路由注册表、把生成的测试骨架挂到测试目录下。还会跑一次静态检查如果模板有问题导致生成了编译过不去的代码在这个环节就能拦下来不让脏数据进到工程里。第五步落盘和人工确认。所有生成的文件会被放到一个临时目录里人工review完、确认没问题了再通过脚本合并进项目。这一步是我坚持要保留的。T3Code再怎么样也只是辅助工具代码最终还是要开发人员负责。自动落盘直接改工程代码会让开发者失去对代码的掌控感出了bug回溯起来也很痛苦。现在这个“生成到临时目录人工确认”的机制既保留了效率又不至于让代码风格完全失控。上面是完整链路的概述。接下来我想重点展开任务规划器和模板引擎这两个最核心部分的实现思路因为它们决定了T3Code的扩展性和可维护性。2.1 任务规划器的规则设计为什么拆得细反而好维护任务规划器本质上是一组“schema到Task”的转换规则。最开始我图省事直接写了一个大函数根据schema的task字段switch到对应的任务集合。比如task是query_detail就返回那三五个Task。跑了一阵子发现不对劲随着任务类型增多这个switch函数越来越长而且逻辑开始互相交叉。比如query_detail这个任务在订单模块和用户模块下的拆解结果其实略有不同订单模块要多生成一个幂等性的校验逻辑用户模块不用。在某个模块里发现问题改代码时很容易影响其他模块的拆解逻辑。后来我重构成了规则表驱动的方式。每个模块在接入T3Code时会为它定义一份模块级配置里面声明这个模块支持哪些任务类型以及每个任务类型拆解成哪些子任务。T3Code内置一批默认规则模块配置里可以覆盖或追加。这样任务规划器本身变成了一个纯粹的规则执行器几乎没有任何业务心智负担新增一种任务类型就是加一条规则的事。举个例子某个模块接入时的配置片段tasks: query_detail: - task: controller template: controller_query_detail.ftl - task: service template: service_query_detail.ftl - task: service_impl template: service_impl_query_detail.ftl - task: mapper template: mapper_query_detail.ftl - task: dto template: dto_response.ftl - task: routing template: router_register.ftl这么设计最直接的好处是可预期——你看到这份配置就能预知生成哪些文件、每个文件用什么模板。Debug的时候不用翻代码直接看配置就能定位问题。而且每个模块的差异被显式声明出来不会“悄悄互相影响”。2.2 模板引擎选型我们为什么选了FreeMarker而不是自己造轮子这个决策当时有争议。有人觉得可以用Java字符串拼接硬写有人建议直接用Groovy模板引擎还有人提议自己写一套简单的占位符替换。最后选型考虑有几点。第一必须支持循环和条件判断。比如生成批量查询接口的VO类时要根据字段列表循环渲染字段定义参数校验规则有时有有时没有需要条件判断。用纯字符串拼接做这些会非常痛苦模板引擎天然支持这些控制结构。第二要能做模板嵌套和宏定义。一个模块生成十几个文件其中基类定义、公共import、统一返回结果包装这些片段是每个文件都要用到的。如果这些公共片段复制到每个模板里改一处要动十几个文件。FreeMarker的宏macro和include机制正好解决这个问题。公共片段定义在lib文件中各模板通过import引用。第三学习成本要低。模板最终是给团队里所有开发人员看的大家平时写Java不搞前端那套复杂语法。FreeMarker的语法跟Java表达式很接近${entityName}、#list fields as f这种写法基本不需要额外学习就能看懂。实际用下来FreeMarker最大的坑反而在变量空值处理上。默认情况下如果模板里引用的某个变量在数据模型中不存在FreeMarker会直接抛异常。这在调试期很折磨人——某个schema没写可选字段所有模板集体罢工。后来我们的解决方案是一是全局设置空值友好?default(“”)二是在渲染前的数据模型构建层做一个兜底所有模板变量统一通过一个Provider类获取Provider里给每个可能缺失的变量提供默认值。这样模板里几乎看不到空值判断的杂音代码也干净很多。3. 模板分层与变量体系模板可维护性的关键决策模板是整个T3Code里最容易写又最容易写烂的部分。开始的时候大家热情高涨每个任务类型都写个独立模板觉得自由度很高。结果不到两个月模板数量突破六七十个公共代码片段散落各处改一个返回格式规范要手动改三十几个文件改到怀疑人生。那次教训之后我们彻底重构了模板体系核心思路就是分层。3.1 三层模板结构基础层、模块层、任务层现在T3Code的模板分三层。基础层维护所有模块共享的代码骨架和规范。包括统一返回类型定义、全局错误码枚举、分页参数结构、公共工具类引用、代码风格配置缩进、import排序等。基础层的内容几乎每个模块都用得到改动影响面也最大所以这里的任何修改都要走严格的review流程。有个小细节基础层模板里定义的宏命名统一带前缀base_这样任务层在调用时一眼能看出来是公共能力不至于跟模块自己的宏混淆。模块层写入某一个业务模块特有的规则。比如资源模块可能需要额外的权限校验注解流程模块需要带上审批链路的状态枚举。模块层模板会覆盖基础层的某些默认设置比如Controller的父类可能不同、某些接口不需要包装返回体。这一层是模块接入T3Code时主要定制的地方。任务层对应我们上面说的Task类型是真正渲染文件主体的模板。比如controller_query_detail.ftl、service_impl_create_order.ftl这类。任务层模板通常不大几十行到两百行左右因为它做的事情是“搭骨架”把该调用的宏按正确顺序组织起来。这三层之间通过变量作用域规范串联。基础层定义“全局约定”模块层定义“局部覆盖”任务层只做组装。尽量避免任务层模板里出现复杂的业务逻辑表达式——如果发现某个任务层模板里的FreeMarker逻辑超过20行那基本说明这个模板应该把这块逻辑提取成基础层的宏。这个约束是硬性要求CR的时候看到大段模板逻辑直接打回。3.2 变量从哪里来Type类型系统的映射规则模板渲染需要大量变量比如实体类名、数据库表名、主键类型、字段列表、校验注解列表。这些变量绝对不是散落在模板里的魔法值而是通过一套Type类型系统推导出来的。Type类型系统维护了一张中心映射表记录“业务实体”和“代码要素”之间的对应关系。举个最简单的例子订单实体Order对应数据库表名t_order对应Java类名OrderEntity对应VO类名OrderDetailVO对应主键字段类型Long。这些映射关系统一写在一个entity-mapping.yaml里面entities: Order: table: t_order entityClass: OrderEntity voClass: OrderDetailVO idType: Long fields: - name: orderNo column: order_no javaType: String validation: NotBlank - name: amount column: amount javaType: BigDecimal validation: NotNull模板里需要任何实体相关的信息都通过变量访问器从这张映射表里取。比如模板里写${entity.idType}实际拿到的是类型映射里Order的idType值。这样带来的好处很明显类型定义和模板渲染彻底解耦。如果数据库字段类型变了只需要改映射表模板不用动。如果新增一种实体接进来的成本就是写一段yaml配置。同时这套映射表也是跨模块语言差异的收口点。比如有的模块字段是LocalDateTime有的模块业务上要求用Date这两个类型在模板里的import语句和注解写法都不一样。类型映射表里可以给每个字段配上不同语言层面的类型描述模板只管按类型名渲染不用关心类型背后那些乱七八糟的差异。3.3 渐进式迁移老代码与新模板的兼容策略模板体系重构之后一个绕不开的问题就是存量代码怎么办。团队里有大量老模块是手写的风格跟模板生成的代码有差异。如果要求所有老模块重新用T3Code生成一遍代价太大而且风险极高——很多老代码里已经混入了手写的业务逻辑重新生成会覆盖掉。我们采用的策略是渐进式迁移不搞一刀切。具体做法老模块按需接入。某个老模块要加新接口了新接口走T3Code生成生成的同时会对比一下同模块老代码的风格差异把共性差异记录下来补进模板。也就是说每次改动都是让模板更贴近这个模块的实际风格而不是生硬套用全局模板。风格差异文件机制。如果某个老模块的代码风格跟模板默认风格有明确不同比如包名结构、异常处理方式可以在模块层配置一个style-override.ftl覆盖基础层的部分宏实现。这样新老代码在我们允许的范围内有差异但这个差异是显式声明和受控的。不允许自动重写老文件。T3Code生成的临时目录里老文件不会出现只会出现新文件。人工确认后再决定是新增文件还是覆盖旧文件覆盖前脚本会自动做diff并生成审计日志。这套策略跑了一两个迭代之后团队对新模板体系的信任度明显上来了。大家发现它不是来添乱的——它是来把大家从重复劳动里解放出来的。4. 实测中踩过的三个坑模板渲染、类型映射与风格统一任何工具只有真正跑起来才知道问题在哪。T3Code从第一版到基本稳定中间踩了不少坑有些坑很典型值得展开说说。这三个坑我觉得对做代码生成类工具的人尤其有参考价值。4.1 坑一FreeMarker模板嵌套过深后调试信息只剩一堆问号最初我们为了减少模板重复把公共片段拆得很细一个任务层模板上层层叠叠include了七八层公共片段。逻辑上没问题但真出bug的时候就崩溃了。比如某个生成的文件里多了一行奇怪的import你顺着模板一层层找完全不知道是第几层哪个宏带出来的。FreeMarker渲染报错时直接报的是最底层某个模板的某一行但那一行往往只是个include指令问题的根源可能远在两三层之外。排查了整个下午终于想明白了问题本质“复用”和“可调试”之间需要平衡。include层级过深相当于把代码逻辑藏在一个黑盒里维护者面对的几乎是一堆间接层组成的调用链。后来我们做了两层改进控制include深度不超过三层。公共能力优先用FreeMarker的宏macro而不是include片段来实现。宏的调用点能直观看到传入了什么参数、从哪来的但include的话你只能看到一个孤零零的文件路径。自定义了FreeMarker的TemplateLoader在渲染上下文里记录每个文件的include来源链。渲染出错时输出不再是干巴巴的第几行报错而是完整的模板调用链类似controller.ftl - base_header.ftl - error_code_macro.ftl - 第12行 宏定义错误这一个小改动让模板调试效率翻了不止一倍。4.2 坑二类型映射规则在不同任务类型之间互相漏配类型映射表刚上线时只服务controller生成。后来service层、mapper层模板陆续接入这时候暴露出一个问题同一个实体类型在不同任务类型的模板里需要关注的字段维度根本不一样。Controller模板关注字段有没有校验注解Mapper模板关注字段对应哪一列、是不是主键Service模板关注字段在业务操作中需不需要做存在性检查。如果映射表只维护一份“字段全量信息”每个模板拿到后自己去筛选就会发生各种漏配——比如某个字段在Controller模板里用到了校验注解但这个注解没在映射表里配全生成出来的代码编译过不去。我们的解决思路是把映射表从“单层结构”改成“按任务类型分类的结构”。每个实体字段下面挂多份视图定义entities: Order: fields: - name: orderNo column: order_no controller: validation: NotBlank(message订单号不能为空) paramBind: RequestParam mapper: whereCondition: true service: existenceCheck: true模板渲染时只取自己任务类型对应的视图。这样每种任务类型只关心自己需要的字段属性互不干扰。代价是映射表yaml文件会大一些但换来的是模板逻辑更简单、映射关系更显式很值得。4.3 坑三生成的代码能跑但Lint和格式检查过不去这个问题看起来小实际很磨人。我们的工程里有配置好的Checkstyle和SpotBugs规则手写代码要过这些规则才能合并。T3Code生成的代码一开始经常撞规则——不是逻辑问题是格式问题import顺序不对、行尾多了空格、缩进用了Tab而不是空格、注释格式不符合规范。最开始我们想靠FreeMarker模板本身控制输出格式后来发现不现实。模板里写死了缩进和换行但代码里的条件块深浅不一模板无法完美预测最终缩进。正确解法是引入后处理阶段的格式化工具。T3Code的后处理器集成了google-java-format作为代码格式化器生成的所有Java文件在落盘前统一走一遍格式化。接着按工程内的Checkstyle规则跑一遍静态检查如果有规则被违反直接报错并标明来自哪个模板的哪个宏。这两步加进去之后生成的代码基本一次通过CI检查很少再因为格式问题被打回。这个经验特别想分享给做代码生成器的朋友不要试图在模板里控制输出格式格式化交给专门的工具处理模板只负责结构和内容。这真的能省太多精力。5. 量化结果与边界T3Code真正省掉的时间在哪里工具做得怎样最终要用数据说话。T3Code跑了大概两个季度之后我们做了一次认真的效果统计。先给结论它没有也不需要覆盖所有代码但在覆盖到的那部分场景里效率提升非常明显。5.1 数据反馈生成速度与人均提效我们选了三个新接入的模块让开发人员用T3Code生成标准CRUD接口并记录耗时。三个模块分别是一个字典管理模块、一个消息通知模块、一个简单的订单查询模块。手写的标杆时间是之前历史记录里类似模块的平均耗时从建表语句写完到接口联调通过大约需要1.5个人天。用T3Code之后从写完schema描述到生成代码、跑通静态检查、单元测试骨架通过大概半小时搞定。后续的业务校验逻辑再花小半天手写补齐。整体下来一个标准CRUD模块普遍从1.5人天压缩到0.5人天左右效率提升约三倍。而且因为生成的是标准结构code review的时间也明显缩短了——大家只需要看业务逻辑本身不用再对着机械代码逐行看规范。实际生成速度方面单次渲染一个模块的全部文件大约10到15个文件耗时在3秒以内。主要瓶颈竟然是模板文件首次加载时的磁盘IO后续走缓存基本无感。5.2 哪些代码坚决不能模板化这是我觉得T3Code做得最对的一个决定从一开始我们就把模板化的边界划得很清楚。适合模板化的实体类、DTO、VO类定义Mapper接口及基础SQL方法Service接口和实现类的空壳骨架Controller层的参数绑定、路由注册、统一返回包装错误码枚举、异常处理骨架单元测试的基础用例骨架不适合模板化的复杂业务规则比如订单状态流转的多种分支判断跨模块的分布式事务编排需要深度调用第三方服务的适配逻辑极其个性化的数据组装逻辑为什么这么划核心原因是“代码生成器对确定性负责开发人员对业务正确性负责”。模板化代码的生命周期长、复用率高、改动频繁一旦把业务规则也塞进模板模板就从一个稳定的骨架变成了一个快速腐化的业务容器最后模板和手写代码互相打架谁都改不动谁。这个教训我们是从另一套老的代码生成器项目里学来的——那套系统试图把业务规则也模板化最终维护成本高到几乎弃用。5.3 让模板跟着团队演进而不是反向束缚团队模板体系最忌讳的就是“一劳永逸”的心态。团队规范是活的技术栈也会有升级模板必须持续更新。我们在团队里定了一个机制任何人在代码评审中发现生成代码有可优化空间都可以提MR去改模板库。模板库本身像正式代码库一样维护——有版本管理、有review流程、有变更日志。模板改完之后基础层变更会导致所有模块重新生成并做回归测试。这套机制跑起来之后模板库的迭代速度非常快。第二季度我们统计过模板库累计收到四十多次有效变更其中超过一半来自业务开发人员的主动贡献。大家不再是“被工具生成代码”的人而是“共同塑造工具”的人。这个过程反过来也强化了代码规范在团队内的认同感——因为规范已经不是贴在文档里要求大家遵守的东西而是直接体现在每个人每天用的工具里。6. 从第一版到基本稳定我复盘出来的几个关键判断T3Code做到现在这个状态如果让我重新来一遍有一些决策我会坚持有一些地方会直接改掉。挑几条最核心的说说。坚持的第一点是schema驱动而不是AI驱动。哪怕现在大模型生成代码的质量越来越高我仍然会坚持用结构化的schema描述需求。核心原因是“可审计性”——审计一条需求生成的代码你能把每一步逻辑都讲清楚但审计一段AI生成的代码很多时候只能靠人工review心里是不踏实的。T3Code这套体系里schema是需求方和代码之间的桥梁它既是输入也是文档这条链路是透明且可控的。坚持的第二点是临时目录人工确认的落盘机制。虽然多了一步操作但保证开发人员对代码的所有权。用得久了之后大家发现确认过程中经常会微调一部分生成代码这些微调反馈回模板库形成正循环。如果当初设计成“一键自动覆盖”两道工序之间没人检查模板出一次错就会污染整个工程信任感建立不起来后面推广会非常艰难。会改掉的一个地方是初期模板粒度太细、分层太激进。当时为了让模板复用最大化我们拆了大量公共宏和基础层片段。确实复用率很高但过度抽象带来的理解成本也不低——新同学第一次看模板库的时候会在层与层之间迷路。下次再做类似系统我会先从适度粒度开始让模板保持“一眼能看懂它在渲染什么”的直觉等真正出现重复再抽公共层。过早优化真的会吞噬维护者的时间。最后一个建议给想做同类方向的朋友别想着一次性把所有场景都覆盖进去。选一个频率最高、结构最稳定的场景切入比如用户模块的标准CRUD把它做到极致让团队真正感受到效率变化。有了第一个成功案例后面推广起来阻力会小非常多。工具覆盖面是逐步长出来的不是一开始规划出来的。T3Code还在持续演进后续我们打算把测试数据的Mock生成、多环境配置文件的模板化、以及基于OpenAPI的Schema自动生成都接进来。但这篇文章写到这里我觉得更值得沉淀下来的不是功能列表而是我们为什么这么设计、过程中踩过哪些坑、事后怎么看这些决策。希望这份复盘对打算做类似工具的团队有点用处。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑