
三年前我第一次接手团队里那个订单管理系统的时候第一件事是挨个翻后端Controller层的代码。三个开发各自维护了几百个接口同名接口的实现风格能差出两个版本有人把响应包在统一对象里有人直接返回裸Map还有人把参数校验写到了Service层。规范文档写了厚厚一叠但代码和规范对不上每次评审都要靠人肉提醒。那会儿我开始认真琢磨一个事逻辑这么相近的接口、列表页、数据访问代码能不能让一套代码自动生成框架来产出。到现在这套框架已经让团队里六七成的新模块不再手写底层的重复代码。这篇文章不聊宏大的低代码平台就围绕这个自建框架的设计思路、落地实现、踩坑记录把从零到跑通的完整链路讲透。1. 先想清楚要解决什么问题重复的不是代码是规范缺口做这个框架之前我们先用两周时间盘了盘现有项目里的重复情况结论很重要真正拖慢开发的不是打字的速度而是每个人对同一个功能该怎么写的认知不一致。框架要解决的不是帮你把键盘敲得更快而是把团队约定固化到产物里。1.1 三个层面的重复代码到底有多浪费我把日常开发里的重复分成三个层面看。接口层增删改查这四类接口高度相似。无非是接收请求参数、调用服务、组装响应Controller的结构几乎一模一样。差别只在类名、字段名、参数校验规则。数据访问层单表查询、按ID查询、按条件分页查询这些SQL和对应的映射配置单调但量大。每个新表进来都要把上一张表的SQL复制一遍再改表名改漏一个字段就是线上问题。前端表单和列表页后端定义了字段前端要照着写输入框、下拉框、表格列。后端改一个字段名前端要同步找三四个文件。这类重复最隐蔽因为大家默认本来就是两个端各写各的但效率损耗恰恰在这里。三个层面叠起来一个新模块从建表到前后端能跑通最快也要两三天其中一多半时间在复制粘贴。更麻烦的是每次复制都会引入风格偏移代码仓库慢慢就成了一锅杂烩。1.2 现有脚手架和生成工具为什么不够用团队一开始也尝试过市面上的通用生成工具但落地时发现几个问题。通用工具生成出来的代码是平均风格不会长成我们团队规约要求的样子。比如统一响应体、日志埋点、异常处理的写法每个团队自己都有约定通用工具没法默认匹配。另一个问题是通用工具只生成单点代码。它能帮你生成一个实体类但没法帮你把Controller、Service、Mapper、前端页面串成一条完整的业务链路。生成出来的碎片还要自己手动拼拼的过程又把风格差异带回来了。第三个问题更实际团队里几个项目的技术栈不完全一致有的用Java有的用Python写部分服务。通用工具通常只支持一种主流行做不到一套元数据描述多端出码。所以与其在别人工具外围包壳子不如按自己团队规范写一套生成器。这个决策是框架最大的根基不是找最强大的工具而是让生成结果严格等于团队规约。1.3 框架生成范围的边界划分边界问题在项目一开始就必须说清楚否则框架会变成什么都要管的怪物。我们最终划定框架负责生成有明确规律、可被模板完整描述的部分——实体定义、数据访问层基础方法、Controller的CRUD骨架、前端列表页和表单页的初版。框架不负责生成强业务逻辑比如订单金额计算、库存扣减、审批流转这类代码仍然由开发手写。这个边界的依据很简单模板能描述的部分才有固化价值一旦逻辑变成每个业务都不一样硬塞进模板只会让模板里堆满if else最后改模板比写业务还痛苦。划完边界后框架的定位就清晰了它是业务的装配工不是业务的发明家。它把百分之七十的重复劳动接走让开发把精力聚焦在剩下的百分之三十里。2. 元数据驱动用一份JSON描述把事实来源固定下来框架的核心不是模板而是元数据。这个认知我们费了不少周折才建立。最初一版生成器直接让用户填表单、勾选项结果发现配置界面本身成了瓶颈。后来改成用一份JSON描述模块的字段、类型、关系、展示规则一切生成动作都从这份JSON出发。2.1 元数据文件的核心字段设计一份元数据文件至少要覆盖这几类信息模块基本信息、字段列表、字段属性、页面展示规则、查询条件。模块基本信息包括模块名、中英文名称、所属应用、数据表名、主键策略。字段列表是核心每个字段要描述字段名、业务含义、Java类型、数据库类型、是否必填、默认值、是否为查询条件、前端控件类型、字典编码等。我拿订单模块举个例子元数据大概是这个风格{ moduleName: order, moduleAlias: 订单, tableName: t_order, primaryKey: id, fields: [ { fieldName: id, fieldAlias: 主键, javaType: Long, dbType: bigint, primary: true, autoIncrement: true }, { fieldName: orderNo, fieldAlias: 订单号, javaType: String, dbType: varchar(32), required: true, queryCondition: EQ, uiControl: input }, { fieldName: status, fieldAlias: 订单状态, javaType: Integer, dbType: int, dictCode: order_status, uiControl: select }, { fieldName: amount, fieldAlias: 订单金额, javaType: BigDecimal, dbType: decimal(10,2), uiControl: inputNumber }, { fieldName: createdTime, fieldAlias: 创建时间, javaType: LocalDateTime, dbType: datetime, uiControl: datetime } ] }这份JSON就是唯一事实来源。数据库表、Java实体、接口接收参数、前端表单组件全部由它推导出来。人工改代码之前先改它改完再重新生成从机制上让规范落地。实际落地时我们给元数据增加了schema版本号避免不同模块的配置因为版本不同而解析出错。这是后来被迫加上的早期没有版本号升级解析器后旧模块的配置就要全部人工核对非常痛苦。2.2 Java类型、数据库类型、前端控件类型之间的映射规则元数据写一次要同时服务于后端和前端所以类型映射是框架里最需要耐心的部分。我们的映射策略是元数据里只写逻辑类型由生成器按目标端转换成具体类型。逻辑类型大体有数值型、字符串型、布尔型、日期型、时间型、日期时间型、金额型、大文本型、枚举型这几类。逻辑类型表直接决定了模板怎么写逻辑类型数据库类型Java类型默认前端控件STRINGvarchar(n)StringinputINTEGERintIntegerinputNumberLONGbigintLonginputNumberDECIMALdecimal(p,s)BigDecimalinputNumberBOOLEANtinyint(1)BooleanswitchDATE_TIMEdatetimeLocalDateTimedatetimeTEXTtextStringtextareaENUMintIntegerselect这张表不是拍脑袋定的而是跟着团队项目里现有代码逐个核对出来的。比如团队约定金额一律用BigDecimal不允许用Double映射表里就固化了这个规约时间字段统一LocalDateTime不用java.util.Date这也是映射表强制的结果。所以映射表其实承担了双重职责它既是类型转换依据也是技术规范检查单。新增项目如果出现映射表里没有的类型会先卡在生成器外头逼着团队先讨论规约而不是各自随意引入。2.3 为什么会踩模型和模板脱节的坑框架第一版交付时我们犯过一个经典错误把类型映射逻辑写死在模板的if判断里。模板里堆了几十行如果是DECIMAL就输出BigDecimal如果是STRING就输出String。结果就是生成器内核很轻模板却越来越重。后来要加一个金额字段在列表页右对齐的需求我们得在每个相关模板里同步改判断逻辑漏改一个模板生成出来的页面就对不齐。教训是类型判断、控件映射不能混入模板必须前移到元数据解析阶段。模板只做数据填充和简单循环所有分支决策都发生在解析器里把渲染需要的最终值算好模板拿到的已经是答案。改版之后模板变得非常干净基本就是遍历字段列表输出属性声明这种程度。这让我后来跟人说框架设计时反复强调一句话模板是哑的逻辑都在解析器里。这样调试方便模板也几乎不需要单元测试。3. 生成器内核模板渲染只是最后一步很多人以为代码生成框架就是模板替换真正做下来才发现模板渲染只占整个生成器不到三分之一的代码量。更复杂的部分在解析、校验、冲突处理这些看不见的地方。3.1 解析、渲染、校验三段式架构生成器的处理流程我习惯分成三段解析元数据、渲染模板、校验产物。解析阶段把JSON元数据读进来完成类型映射、字段关系检查、命名规范化。这个阶段会输出一个中间模型里面每个字段都已经算好了Java类型、数据库列名、前端控件类型、校验规则等。中间模型是一个普通的内存对象模板能拿到什么字段、循环什么列表都在这里定义好。渲染阶段就比较机械了根据目标产物类型选择对应模板把中间模型的数据填进去。后端Java类、XML配置、前端Vue文件分别走不同的模板目录。校验阶段是框架区别于普通脚本的关键。生成完成后框架会自动做几件事检查产物目录里是否已有同名文件检查生成代码里的包名和import是否完整检查字段名是否包含非法字符必要时甚至调用本地的编译命令跑一次语法校验。这一步可以把大部分低级错误拦在落盘之前。三段式最大的好处是每段可以独立测试。解析器有问题只需喂元数据看中间模型模板有问题可以直接用写死的中间模型渲染校验有问题可以用生成产物做回归。排障范围小效率高。3.2 模板选型的对比与取舍模板引擎我们先后对比过好几种最终选了Velocity。选择理由不复杂语法简单、和Java无缝集成、模板文件不需要编译。Velocity的语法量很少新成员半天就能上手。这个对框架很重要因为模板不是框架作者一个人维护的团队里的后端、前端都会来调整自己那部分的模板。如果模板引擎本身学习成本高谁都不愿意改模板就会慢慢和团队实际风格脱节。FreeMarker也很成熟功能更丰富但对团队来说稍显重模板里能写复杂逻辑反而容易把前面的模板是哑的原则破坏掉。Java模板我们用过一阵调试方便但写遍历和条件判断非常啰嗦模板文件读起来不够清爽。最后还有一个选型标准容易被忽略生成器本身是用Java写的模板引擎最好能在Java进程里直接加载资源不依赖额外运行时。Velocity在这点上最省心一个jar包搞定服务器上不需要装任何东西。3.3 增量生成与冲突保护代码生成框架最怕的不是生成不出代码而是生成重复代码把原有逻辑覆盖掉。我们设计了一个先保护、后生成的策略。默认规则生成器执行前会先扫描目标目录如果发现目标文件已存在就把老文件改名成带.bak前缀的备份新文件正常写入。如果生成过程报错自动回滚把备份文件改回来。但备份策略也有失控的时候——人工改动过的文件频繁被备份git里会堆满垃圾。所以后来改成更严格的策略生成器在元数据里记录每个生成文件的指纹指纹是元数据版本的哈希。重新生成时如果目标文件指纹和当前元数据指纹一致说明文件从未被人工改过可以直接覆盖如果指纹不一致说明文件被改过就跳过并提示开发人工合并。这套思路解决了一个关键矛盾既想要生成代码的可重复性又不想覆盖人工逻辑。团队里把它叫聪明覆盖它的规则很简单机器生成的部分机器可以重来人工改过的部分机器绝不碰。4. 一个订单模块从元数据到运行代码的完整链路光讲架构有点虚我拿一个真实的订单模块走一遍全流程。这个模块当时从零开始跑框架前后不到两个小时就拿到了一份能编译、能联调的代码。4.1 实际的元数据配置样例订单模块的元数据比前面示例更复杂一点加了创建人、更新人等审计字段还有一个关联客户的字典。{ moduleName: order, moduleAlias: 订单管理, tableName: t_order, primaryKey: id, auditFields: [createdBy, updatedBy, createdTime, updatedTime], fields: [ { fieldName: id, fieldAlias: 主键, javaType: Long, dbType: bigint, primary: true }, { fieldName: customerNo, fieldAlias: 客户编号, javaType: String, dbType: varchar(32), queryCondition: LIKE }, { fieldName: orderNo, fieldAlias: 订单号, javaType: String, dbType: varchar(32), queryCondition: EQ, required: true }, { fieldName: status, fieldAlias: 状态, javaType: Integer, dbType: int, dictCode: order_status, uiControl: select }, { fieldName: remark, fieldAlias: 备注, javaType: String, dbType: varchar(255), uiControl: textarea } ] }写这份配置的人不需要会Java也不需要会前端只需要知道业务上有哪些字段、每个字段大概什么含义。这大大降低了上手门槛后来团队里测试同事也能独立承担元数据编写工作。4.2 模板与渲染产物如何对应元数据解析完成之后生成器会根据设定的目标清单依次调用模板。拿Java实体类举例模板只需要做循环和取值package ${basePackage}.entity; import lombok.Data; import java.time.LocalDateTime; import java.math.BigDecimal; Data public class ${entityName} { #foreach($field in $fields) /** * ${field.fieldAlias} */ private ${field.javaType} ${field.fieldName}; #end }模板里的$fields就是中间模型里的字段列表每个字段已经算好了javaType和fieldName。开发只需要关注要不要额外加个字段备注不需要操心大括号和import模板都处理好了。Controller模板类似会生成统一的CRUD端点分页查询、详情、新增、修改、删除。所有端点都自动包装了统一响应体并加了基础的参数校验注解。前端模板则会生成列表页、表单页、路由配置三项字段与控件类型一一对应。整个链路跑完产物目录大概是这个形态src/main/java/com/xxx/order/ controller/OrderController.java service/OrderService.java service/impl/OrderServiceImpl.java mapper/OrderMapper.java entity/Order.java dto/OrderQueryDTO.java src/main/resources/mapper/OrderMapper.xml src/main/webapp/src/views/order/ OrderList.vue OrderForm.vue orderRouter.js每个文件都是完整可编译的骨架业务开发拿到之后只需要往Service实现里填真正的业务逻辑。4.3 生成结果的验证方式跑完生成器只是第一步我们规定生成结果必须通过三级验证才能算完成。第一级是自动校验生成器用Java编译器的API对后端产物做一次内存编译把编译错误信息直接反馈出来。前端产物则做一次基础的标签闭合检查再用脚本核对import引用。第二级是人工抽查开发打开关键文件对照元数据确认字段没丢、类型没错、注释和别名一致。重点是看Controller参数和前端表单字段是否一一对应这是最容易漏的地方。第三级是联调冒烟启动应用用生成的接口走一遍增删改查前端页面跑一遍列表查询和新增提交。这个环节不测业务逻辑只验证框架生成的骨架本身可以端到端跑通。三级验证全部通过后模块代码才允许提交到主干。这套流程持续跑了一年多生成代码的返工率从最初的百分之三四十降到了百分之五以内。5. 引入AI辅助后自然语言如何变成可执行代码框架跑顺之后我们又开始琢磨怎么降低写元数据的心智负担。毕竟模块字段再多一张表几十个字段写配置也挺费劲。这个需求正好赶上了一波大模型能力爆发于是我们给框架加了一个智能辅助入口。5.1 为什么让AI只负责生成元数据关于AI我们定了一条底线AI不直接生成代码文件只负责把自然语言翻译成结构化元数据。代码文件的最终组装仍然由模板引擎完成。这么定的原因很实际。直接让AI生成代码输出风格不可控可能今天生成带lombok的明天生成带setter的模板的规范就很难统一。而把输出限制在JSON格式的元数据上AI的能力边界正好匹配——它能理解订单模块有哪些字段、状态是枚举这种描述也能输出规范的JSON。更大的好处是元数据是可校验的。AI生成的JSON可以被解析器先跑一遍字段缺失、类型不合法都能在生成前发现。我们的框架在解析前增加了一个元数据体检步骤做必填项检查和类型白名单检查AI输出不合格就直接打回重写。5.2 约束AI输出格式的关键写法要让AI输出可用的元数据最关键的是把格式约束写清楚。我们在提示词里固定了几个要素字段命名规范、类型枚举范围、必须包含的审计字段、JSON的层级结构。给AI的说明大概是这样一个套路先说明这是一个代码生成框架的元数据生成任务然后给一段示例JSON作为格式锚点再列出字段类型可选范围最后强调只输出JSON本身不要任何解释文字。只输出JSON这个要求很重要。AI一旦在JSON前后加解释解析就会失败。实测中大部分格式问题都是解释文字混入造成的所以提示词里要反复强调输出必须能被解析器直接parse如果无法确定字段类型就使用STRING这个兜底值。把兜底逻辑也交给AI的好处是它在不确定某个字段类型时不会自己瞎猜新增一种类型而是服从一个我们事先定好的保守值。这样哪怕AI理解有偏差生成结果至少还能通过解析不会把整个链路卡死。5.3 生成结果的人工校验清单AI生成的元数据只是草稿人工确认环节依然保留。我们把校验清单做成了一页纸开发对照着看就行。第一项查字段名是否遵循驼峰命名是否和建表脚本的注释一致。第二项查类型选择金额字段必须是DECIMAL时间字段必须是DATE_TIME状态字段要用ENUM并关联字典。第三项检查查询条件字段数量一个列表页的查询条件建议不超过五个多出来的要砍掉。还有一项容易被忽略AI会遗漏枚举字段的dictCode。如果状态是枚举但没配字典编码前端下拉框就渲染成普通输入框联调时才暴露。所以我们把dictCode设置成了必检项AI生成的配置里如果没有就补上。这个流程跑下来AI承担的其实是从人写JSON变成人审JSON工作量减少不少而且因为AI不会疲倦字段遗漏的概率比手工写配置要低。但核心的领域判断仍然留给人AI只是个翻译器。6. 上线后踩过的坑从命名混乱到模板失控框架不是上线就完事了真正让它变稳定的是后面半年里一个接一个的坑。我把印象最深的三个记录下来给想在自己团队搞类似框架的人提个醒。6.1 大小写命名不一致引发的编译问题早期框架对元数据的命名没有强校验导致同一个模块里有人把字段写成createTime有人写成create_time生成的Java代码一会儿是驼峰一会儿是下划线。这个问题很隐蔽单看某一个生成文件没问题一旦Java代码里用反射读取数据库列名就会出现字段对不上、运行时报错。我们花了一个多星期排查一个诡异的字段找不到问题最后定位到是生成器里两个模板对同一字段的命名规范化逻辑不一致。修复方案是统一在解析阶段做一次命名规范化字段名一律转驼峰数据库列名保留原始写法并由映射表维护两者对应。同时增加一个校验规则元数据里的字段名如果包含下划线就报错强制开发在配置里就用Java侧命名。这个改动上线后命名类问题基本清零。6.2 数据库保留字和多租户字段的映射坑第二个坑来自数据库保留字。元数据里一个字段名叫desc生成到XML里直接报语法错误。SQL里的desc是排序关键字列名必须加反引号才能用。我们最初的处理是在模板里统一对所有列名加反引号后来又发现有些数据库不支持这种写法导致SQL执行失败。最后改成维护一份保留字清单解析阶段检测到字段名命中保留字就在该列名上强制加反引号其它列名保持原样。和这个问题捆绑出现的是多租户字段。团队后来引入了租户隔离要求每张表都要有tenantId字段。我们开始在元数据里手工加漏了一张表就导致租户数据串号。后来直接在生成器的解析阶段统一注入audit字段和租户字段不允许在元数据里配置——该加的总是会加不该省的也不用纠结了。6.3 模板库版本升级时的幂等迁移框架用了两年后我们做了一次大的模板改版把前端从旧技术栈切换到新框架。这时候暴露了模板升级的最大痛点旧的生成代码散落在各个项目里模板改了不可能把所有已生成的代码重新跑一遍。我们采用的策略是只向前不回头。模板大版本只影响新生成模块老模块保持旧代码继续运行不强制重生成。同时把模板版本号记录在每个生成文件的首部注释里后续哪个模块要做技术栈升级先看版本号再决定是否需要重新生成。这个策略在管理上很讲得通生成代码一旦进入业务迭代就会和手写代码融为一体强行重生成反而会破坏已有业务。框架要提供的是新代码按新规范出生而不是历史代码按新规范回炉。想清楚这一点模板升级的压力就小了很多团队也敢放心去改模板了。用到第三年的时候我最大的感受是一套代码自动生成框架的价值不在于一开始生成了多少代码而在于它帮团队把规范从文档里搬进了流水线里。框架跑得越久团队里新老成员写出来的代码越像同一个人写的。最后的经验就一条别急着追求生成覆盖面先把哪些该生成、哪些不该生成的边界立住再把模板做得越笨越好。这样无论后面接AI还是换技术栈框架都不会被某一次技术变革颠覆掉。