t3code:把重复CRUD代码从手写变为半自动生成

发布时间:2026/10/9 18:01:03
t3code:把重复CRUD代码从手写变为半自动生成
“t3code”这个名字最初是我在内部项目里随手起的代号意思是“Tier 3 Code”即第三层代码。在一个分层架构里Controller、Service、DAO这些偏向胶水性质的代码恰好都属于第三层。它们不像业务核心那么有技术含量却又绕不开、躲不掉写起来枯燥、重复、容易出错还得花大量时间。t3code要解决的问题很简单把这部分代码的产出方式从“手写”变成“半自动生成”。这篇文章我会把这个方案的核心思路、完整搭建过程、实际使用效果以及我踩过的那些坑一次性讲清楚。适合被重复性开发工作折磨的开发者也适合想在团队里建立统一代码规范的负责人。1. 为什么需要t3code一个被重复劳动逼出来的想法1.1 我在实际项目中遇到的三个痛点先说痛点不然你不知道这个工具到底在解决什么事。第一个痛点是“复制粘贴改字段”。那时候团队接手了一个老系统里面大量模块用的是同一套CRUD结构一个实体类、一个Mapper接口、一个XML文件、一个Service、一个ServiceImpl、一个Controller。新需求来的时候开发流程基本是找到最接近的表结构复制整个模块全局替换表名和类名再逐个改字段类型。表面上很快实际上到处都是隐患。字段漏改、类型对不上、XML里的resultMap忘记加列——这些问题靠代码评审根本拦不住因为肉眼很难在一堆相似文件里发现细微差别。第二个痛点是“代码风格无法统一”。五个人写代码就有五种写法。有人喜欢在Service里直接操作Mapper有人非要经过DAO层有人用Lombok有人手写getter/setter有人Controller返回包装类有人直接返回实体。代码规范文件写在Wiki里没几个人真的照做Reviewer也没精力逐行抠。时间一长一个维护了多年的项目代码风格比调色盘还丰富。第三个痛点是“新手上手成本高”。一个刚入职的应届生光理解“我要新增一张表需要动哪些文件、每个文件里要写什么”这件事就要花一两天。中途可能还会因为漏了某个注解导致整个服务启动失败。而这类问题本质上跟业务能力没关系纯粹是流程性、工具性的损耗。1.2 t3code的设计定位不是魔法是流程化有人可能会问这跟那些现成的代码生成器有什么区别确实市面上有不少类似的工具比如MyBatis Generator、基于模板的各类脚手架。但我当时希望t3code做到的不是“从零生成一个项目”而是“在一个已有项目里根据一张表快速生成符合团队规范的一组代码”。这两者的定位完全不同。现成生成器的问题在于它们的默认输出跟团队规范之间总有那么一层“隔阂”。要么返回类型不对要么命名风格不一致要么缺少团队自定义的统一处理逻辑。每次生成完你依然得花大量时间手工调整。而t3code的目标是把团队规范直接固化在模板里生成出来的代码就是可以直接提交Review的水平。所以t3code本质上不是魔法而是“流程化”。把散落在各个开发脑子里的经验、约定、踩坑记录集中沉淀成模板和脚本。以后再遇到类似的活不需要重新思考直接按流程走一遍就完了。1.3 核心原则模板先行、约定优于配置、生成之后可修改这个方案从设计之初就定了三条原则。第一条原则叫“模板先行”。在写任何生成逻辑之前先找团队里公认写得最好的一个模块把它的代码结构、命名方式、注释风格全部提取出来做成模板。这句话说起来简单做起来需要克制。因为每个人都会忍不住把自己的偏好加进模板里最后模板比代码还复杂。第二条原则叫“约定优于配置”。所有能从代码结构推导出来的信息就不应该让使用者额外填写。表名约定、字段命名约定、主键约定、软删除字段约定统统在模板和脚本里统一定义。使用者只需要告诉t3code“我要生成哪张表”剩下的全部推导出来。第三条原则叫“生成之后可修改”。这跟很多代码生成工具不一样我不追求生成出来的代码100%可以直接运行而是追求“90%可以直接运行剩下10%是业务特有的逻辑留给你自己填”。这样做的好处是模板保持简洁不会为了兼容所有场景而堆一堆条件分支。生成的代码读起来、改起来都跟手写代码没有区别。2. t3code的整体架构与核心模块拆解2.1 三个核心模块模板引擎、元数据解析、代码注入器t3code整体上分成三个核心模块各自职责非常清晰。第一个是模板引擎负责把模板文件和数据模型组合成最终代码。我选择了类Jinja2语法的模板引擎因为它的语法足够简单逻辑控制也能满足需求团队里几乎不用学习成本。模板文件存放的目录结构跟项目结构一一对应。第二个是元数据解析器负责读取数据库表结构生成代码生成所需的元数据模型。这个是整个方案最关键的部分。它得从表结构里解析出表名、字段名、字段类型、是否为主键、是否允许为空、注释、索引信息等等然后转换成一套约定好的命名模型。第三个是代码注入器负责处理“生成代码”和“已有代码”之间的关系。一个模块往往不是一次生成就结束的。后续你可能会加一个字段或者新增一个接口方法。代码注入器要做的就是在不破坏已有代码的前提下把新的内容合并进去。这块实现起来比想象中麻烦后面我详细说。2.2 元数据到代码的映射规则表结构如何变成类定义这是t3code最重要的设计。先说命名映射规则这是元数据解析器的核心输出。表名映射到类名遵循驼峰命名法。比如表名是student_info映射出来的实体类名是StudentInfo。字段映射也类似student_name变成studentName。这些规则直接用脚本处理没有任何需要人工参与的地方。类型映射规则分两部分。一部分是数据库类型到Java类型的映射比如varchar变Stringbigint变Longdatetime变Datetinyint变Integer。另一部分关系到代码生成的实际用途比如生成查询条件时String类型的字段查询方式默认走like数值类型的字段查询方式默认走equals。这些默认规则后续可以在模板里覆写。主键标识也非常重要。解析时一旦发现主键字段就会把它单独标记出来。因为主键决定了生成代码里的selectById、updateById、deleteById这些方法的实现逻辑。在实际项目中有的物理主键是自增ID有的是业务流水号有的是UUID它们的生成策略和更新逻辑都有区别。t3code在解析阶段就把这信息融入模型模板里就可以按主键类型做分支处理。2.3 为什么选择“模板脚本”而不是独立工具开发t3code的时候我也纠结过是做成一个独立部署的工具还是做成项目里的一个脚本模块独立工具的好处是通用性强团队之外的人也能直接用。但坏处也明显需要额外维护一套独立的代码库、构建环境和分发渠道。而且独立工具脱离项目天然有“信息滞后”的问题——项目里的规范更新了工具却可能需要同步更新。像我这种“顺手做个内部提效工具”的场景这个成本不值得。最终我选择了“模板脚本”的方式模板文件放在项目根目录下的t3code/templates里脚本用Python编写直接读取模板目录连接数据库取元数据生成代码到目标目录。整个工具就是一个文件夹跟着项目走换人接手没有任何部署成本。3. 从零搭建t3code核心实现步骤3.1 第一步提炼团队模板先修好“样板房”这一步耗时最长也最值得做。我当时找了项目里一个写得最规范、业务最简单、且覆盖了完整CRUD链路的模块把它当作“样板房”。然后把样板房里的代码复制到模板目录把里面跟具体表相关的部分全部替换成模板变量。举个例子实体类的模板大致长这样我用简化版示意package com.example.project.entity; import lombok.Data; import java.time.LocalDateTime; /** * {{tableComment}}实体类 */ Data public class {{entityName}} { {% for field in fields %} /** {{field.comment}} */ private {{field.javaType}} {{field.javaName}}; {% endfor %} }关键点在于模板里不能混入太多个人偏好。我当时整理模板时刻意删掉了一些“好看但没必要”的代码比如自定义的JSON序列化注解、特殊的日志打点方式。因为这些未必适合所有场景留下来的应该是团队里大多数人达成共识的部分。3.2 第二步搭建元数据解析器打通数据库到代码的桥梁元数据解析器是t3code的技术核心。我用Python写连接数据库后执行几条information_schema的查询把表结构拉出来变成Python字典。核心查询大致是这样的SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_COMMENT, COLUMN_KEY FROM information_schema.COLUMNS WHERE TABLE_SCHEMA {database_name} AND TABLE_NAME {table_name} ORDER BY ORDINAL_POSITION;拿到结果后做三件事第一清洗字段名去掉无意义的公共前缀比如某些老表喜欢所有字段都叫f_xxx生成实体的时候应该去掉f_只保留xxx。第二类型映射。这个映射关系需要跟团队技术栈对齐并且允许在配置文件里额外补充规则。例如项目的数据库字段json类型默认映射到String但如果某个模块的Json字段要作为独立对象参与序列化模板可以根据配置决定是否额外加一个JsonFormat注解。第三构建额外的上下文信息。比如判断这张表是否有deleted_flag这种软删除字段有没有create_time、update_time这类审计字段主键是自增还是业务生成。这些信息直接影响后面生成的 Service 实现类里的各种默认行为。解析器跑通之后我习惯先打印一份元数据JSON看看确认解析结果正确再连线到模板生成阶段。这一步相当于把数据库结构“翻译”成模板引擎能理解的上下文肉眼确认无误后后续的生成就是水到渠成的事情。3.3 第三步实现代码生成与覆盖策略生成阶段我直接在脚本里调用模板引擎的render方法把元数据字典传入模板然后把渲染结果写入目标文件。但这里有个非常关键的问题覆盖策略。第一次生成的时候基本没有冲突直接写文件就行。但后续重新生成就可能出现以下情况开发者在生成的 ServiceImpl 里已经写了额外的方法这时候如果直接覆盖增量代码就丢了这是绝对不可接受的。我最终的策略是分文件类型处理文件类型覆盖策略原因Controller全量覆盖基本都是标准的、不承载业务逻辑的代码Service接口全量覆盖接口里通常只有CRUD方法后续新增方法一般加在不生成的文件里Service实现部分合并如果文件已存在只生成缺失的方法不删除已有内容Entity全量覆盖实体类本身应该跟表结构完全同步手写内容一律不加Mapper XML智能合并追加缺失的查询语句片段保留已有自定义SQL这个表格看着简单实际实现时部分合并这种策略需要写一些代码。我的做法是先读取目标文件内容判断里面是否已经存在某个方法签名比如public int updateById(...)如果存在就跳过不存在才追加。这个方法虽然粗暴但实际跑下来够用。3.4 第四步命令行参数设计让工具成为“一条命令”的事生成工具最终要落地使用体验必须简单粗暴。我设计的使用方式是这样的python t3code.py --table student_info --module student --overwrite参数含义table要生成的表名必填。module模块名会作为包名和目录名的一部分比如--module student会生成com.example.project.student下的代码。overwrite可选加上它才允许覆盖已存在的Controller和Entity文件。db可选的数据库连接配置默认从项目配置文件读取。参数少意味着使用门槛低。团队里没有任何人需要去看文档才能用起来只需要知道三四个参数的意思就够了。这也是我后来才意识到的一件事内部工具的易用性往往直接决定了大家愿不愿意用。4. 实际使用场景把一个新模块从3小时压缩到20分钟4.1 一个真正发生的需求新增“课程表”管理模块为了让你更直观地感受这套方案的价值我描述一个实际发生过的场景。某天业务提需求需要新增一个“课程管理”模块对应数据库表course_info字段大约十来个包含课程名、授课老师、上课地点、选课人数上限等主键是自增ID带有create_time、update_time、deleted_flag这些公共字段。放在以前这个需求写完整套CRUD大概需要3个小时手动建实体类手写Mapper接口和XMLService和Controller还得对照旧代码逐字敲最后还要检查各种注解和类型。如果用t3code整个过程只需要三步。4.2 实际操作过程记录第一步执行命令生成基础代码python t3code.py --table course_info --module course脚本执行过程中会打印日志说明它正在解析表结构、渲染模板、写入文件。执行完毕直接在项目结构里看到了新生成的文件entity/CourseInfo.javamapper/CourseMapper.javamapper/CourseMapper.xmlservice/CourseService.javaservice/impl/CourseServiceImpl.javacontroller/CourseController.java第二步打开数据库执行脚本生成菜单SQL。这一步其实不是t3code自动做的但从实际业务出发新模块大概率需要新增菜单权限。因为操作频繁我把它写成了一个模板SQL文件改一下路径和名称就能复用。第三步补充业务特有逻辑。课程表有一个字段比较特殊course_code课程编号业务上要求它不能重复且新增时由前端传入。这不再属于“标准CRUD”的范畴需要在ServiceImpl里补充一句唯一性校验逻辑。由于模板生成的代码结构清晰定位到新增方法后追加几行代码就可以搞定。整个流程加在一起20分钟是很宽裕的估计。4.3 生成结果的检查清单工具不是万能的生成完代码后我建议团队执行一个“三查”动作每次都不要跳过第一查实体类里字段类型是否正确。虽说类型映射覆盖了绝大多数场景但总有几个特殊字段比如decimal(10,2)到底该用BigDecimal还是Double得人工确认一下。第二查XML里的查询语句是否满足需求。生成出来的列表查询是通用的selectByCondition它拼接where子句时用的是动态SQL。如果业务要的是某个特定查询比如“只查未删除的课程”可能需要加一个自定义条件。第三查ServiceImpl里的默认实现是否影响性能。生成的deleteById如果是物理删除而表结构里有deleted_flag那就不太合适需要改成逻辑删除。我的模板默认对这类字段做条件判断但不同团队约定不同检查一下总是没错的。5. 常见问题与排查技巧实录5.1 问题速查表这套工具在团队里跑了一年多遇到的典型问题和解决办法汇总如下问题现象原因分析解决方法生成的实体类没有继承基类模板里没加继承配置在模板中引入公共基类或配置base_entity参数数据库字段is_deleted被当成了普通字段解析器没有识别软删除约定在类型映射配置中添加is_deleted标识字段名重新生成时 ServiceImpl 里手写代码被覆盖覆盖策略没有区分文件类型确认overwrite参数没勾选手动合并差异XML里出现重复的if片段模板动态SQL和手写SQL混在一起改动XML模板后重新生成前先备份手写部分生成的Controller返回结构不符合团队规范团队规范变更后模板没有同步更新模板文件并建立模板Review机制数据库连不上导致解析失败连接配置写死在了脚本里改成读取项目配置文件或支持--db临时指定5.2 最容易踩的三个坑每个都交过学费坑一模板里的空白字符。这是模板新手最容易忽略的问题。模板引擎渲染时{% for %}和{% endfor %}之间的缩进、空行都会被自动压缩或保留但不同引擎的处理方式不同。我第一次渲染实体类时发现每个字段前面都多出两个空格因为模板里写了额外的缩进。后来在模板两侧加了{%- -%}这种去掉空白的控制符才算彻底解决。建议在模板提交入库之前先跑一次生成人工对比一下格式是否符合规范。坑二字段注释里的特殊字符。数据库字段注释经常出现换行、引号、甚至HTML标签。模板引擎对特殊字符不做转义直接渲染到代码里会导致生成的注释断行、注解被破坏。我在解析器里加了一步清洗把换行替换成空格、去掉多余字符。实际数据里真的见过注释里带引号和括号的场景不处理的话代码直接编译失败。坑三表结构字段顺序变化。数据库的字段顺序可能因为版本迭代发生变化。如果模板是按照字段顺序生成的重新生成后代码里字段位置会变Git Diff会变得很难看有时候还会引发不必要的Merge冲突。我最后的处理办法是解析器输出字段时按“约定顺序”排序不是按数据库里的物理顺序主键排第一常用字段排中间审计字段排最后。这样版本之间的Diff就干净很多。5.3 模板也需要评审和维护很多人容易忽略一件事代码生成工具的模板本身就是“团队的代码规范实体化”。模板一旦定下来生成的代码会以几何倍数复制所以模板质量比普通代码质量更需要被严肃对待。我的经验是t3code的模板在团队里要走和正式代码一样的Review流程。任何人要改模板必须先提交变更说明和一段生成示例Diff让其他成员看到改动的影响。另外建议每半年审视一次模板团队用的框架升级了某些注解就该调整了项目里开始统一返回结构了Controller模板就该同步改。模板维护跟不上生成的代码就会慢慢变成“旧规范产物”。6. t3code可以扩展的方向6.1 从CRUD生成走向业务场景生成t3code的第一阶段只做标准CRUD但实际项目中有些代码也是高度模式化的。我正计划扩展的场景包括定时任务模块生成固定格式的Scheduled方法自动注册JobHandler。MQ消息消费生成消费方的消息处理骨架根据主题自动生成消息模型。对外接口对接根据约定的接口文档生成签名验签、参数转换的调用骨架。这些场景跟CRUD有个共同点结构高度标准化差异点只在业务细节。只要模板设计得当t3code可以大幅减少这些重复工作的耗时。6.2 把数据库变更同步纳入生成流程目前t3code只在“新增表”的场景下使用。但实际项目里更多反复出现在“表结构变更”时新增字段、修改字段类型、删掉废弃字段。手工同步的话涉及实体类、Mapper XML、DTO等好几处。我把数据库变更也纳入t3code的生成范围在配置里增加--sync选项它会自动计算两张表结构的差异然后只针对差异部分生成增量代码。这个功能实现之后日常的字段增删工作会变得特别轻量。目前我已经把表结构对比的逻辑搭起来了增量代码生成还在打磨预计很快就能用上。6.3 我的实践体会用了一年多t3code我最大的感触是工具能解决掉“结构性的重复”但解决不了“理解性的匮乏”。哪怕代码生成到了90分开发者在做需求时依然需要理解业务、理解数据含义。t3code的价值是把省下来的时间还给真正的业务思考而不是让团队变成纯“填代码的机器”。如果你是那种重复代码已经压得喘不过气的团队我建议你认真考虑沉淀一套自己的t3code从最基础的一张表生成开始跑通流程再逐步扩展场景。它会成为项目里最值得的工程投资之一。