ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

t3code实战:基于元数据驱动的三层架构代码生成器设计

t3code实战:基于元数据驱动的三层架构代码生成器设计 做后端快十年各种代码生成器、低代码平台也见识了不少但大部分时候都是围观别人家的产品真正自己动手写一个还是头一回。今天要聊的t3code就是我自己在项目空档期折腾出来的一个小工具定位非常明确专门给三层架构Controller-Service-DAO/Repository项目生成基础代码。核心思路不复杂就是读取数据库表结构把 Entity、Mapper、Service、Controller 这些重复性极高的代码一次性输出同时把 VO、DTO、Mapper XML 也一并生成。它解决的是我长期以来的一个痛点项目一多每个模块的增删改查代码翻来覆去都是同样的套路手写浪费时间还容易出错团队里每个人写出来的风格还不一样。这个项目适合后端开发、全栈工程师以及想做工程化提效的团队参考尤其是被重复 CRUD 代码折磨过的朋友看完应该能get到不少实用思路。1. 项目定位与整体设计思路1.1 “三层”到底指什么以及我为什么非要自研很多刚接触的人会问现在现成的脚手架一堆Spring Initializr 、MyBatis Generator 、甚至 IDEA 插件都能搞定为什么要自己写一个取名t3code的东西实际上“三层”这个说法在 t3code 里有两层含义。第一层是经典的三层架构也就是 Controller 层、Service 层、DAO 层。这是绝大多数 Web 项目的落地骨架。第二层是“三段时间”读取表结构花一段渲染模板花一段校验输出花一段。我把工具设计成三段式每段都可以独立调试出了问题时不用整个重跑能快很多。至于为什么不直接用现成方案我用真实体验来说。MyBatis Generator 很强但它的代码风格非常固定生成的注释丑、方法冗长而且和现代 Spring Boot 风格多少有点脱节。市面上一些在线生成平台倒是漂亮但表结构一敏感就不能外传内网部署又要钱。IDEA 插件嘛换个 IDE 就得换一套肌肉记忆。所以我的结论是与其费劲去适配别人的工具链不如花一两天写一套自己可控的生成器模板完全捏在自己手里想怎么改就怎么改。对于长期有建表需求的团队来说这套投入回报率非常高。1.2 方案选型从“手写模板”到“元数据驱动”早期我做 t3code 的时候思路还停留在“字符串拼接代码”的层次就是在 Java 里写一堆 StringBuilder把 package、import、类名拼起来。这么做的问题很明显只要代码结构调整一次拼接逻辑就得改一遍维护成本高到崩溃。后来我换成了“元数据驱动 模板引擎渲染”的方案。具体来说通过 JDBC 的DatabaseMetaData接口读取表名、字段名、字段类型、注释、主键、索引信息把这一坨信息封装成统一的元数据对象然后传给模板引擎去渲染。t3code 用的是 Freemarker主要原因是它语法简单、对 Java 开发者友好而且支持局部变量、条件判断、循环遍历写模板时非常顺手。这一步看似简单其实是整个项目的地基。元数据模型设计成“表信息”、“字段信息”、“主键信息”三块分别对应 Java 里的TableMeta、ColumnMeta、PrimaryKeyMeta。每个字段要记录的信息包括数据库物理名、Java 属性名驼峰转换后、Java 类型、JDBC 类型、是否主键、是否自增、是否允许为空、字段注释、默认值。把这些信息完整塞进模型后面的模板渲染工作就能很轻松。2. 核心架构与关键技术拆解2.1 t3code 的四个核心模块t3code 整体分成四个模块每个模块各管一段逻辑清晰测试也好写。配置解析模块负责读入数据库连接配置、输出路径、包名、作者名、是否覆盖文件等参数。它支持 YAML 和 Java 原生的Properties两种格式我个人推荐 YAML可读性好团队协作时改动也直观。元数据读取模块基于 JDBC 的DatabaseMetaData和ResultSetMetaData实现。这里有一个很多人容易忽略的细节不同数据库返回的元数据大小写规则不一样MySQL 默认小写Oracle 默认大写PostgreSQL 则常见小写带下划线。所以 t3code 里统一做了一次lowerCase归一化再进入后续处理。模板渲染模块加载 Freemarker 模板传入元数据模型输出字符串内容。这个模块本身不关心文件写到哪只负责“渲染”所以单测非常好写。代码输出模块负责创建目录结构、处理文件覆盖策略、修正换行符、输出最终文件。Windows 和 Linux 混用的团队输出前统一转成 LF能避免不少莫名其妙的 diff。每个模块之间的通信都靠数据模型不共享可变状态。这就意味着后面我想加“生成 TypeScript 代码”或者“生成数据库迁移脚本”只需要新加模板和适配器不需要动已有的渲染逻辑。2.2 数据库方言与类型映射的细节讲一个我踩过的坑数据库字段类型到 Java 类型的映射绝不是一个switch-case就能解决的。以 MySQL 为例datetime映射到LocalDateTime没问题但date如果也按某些旧框架的思路映射成Date在 Spring Boot 3 的jackson-datatype-jsr310下序列化就会出问题。我在 t3code 里维护了一张类型映射表核心规则如下数据库类型Java 类型说明BIGINTLong主键常用注意无符号整型INT/INTEGERInteger如果字段可能超 21 亿要手动改成LongTINYINTInteger不直接映射Boolean因为业务上经常存 0/1/2 多状态VARCHARString长度映射到Column(length ...)方便 DDL 对齐DECIMAL/NUMERICBigDecimal金额字段必用禁止用DoubleDATETIME/TIMESTAMPLocalDateTime新项目统一LocalDateTimeDATELocalDate只存年月日TEXT/LONGTEXTString同时生成Lob注解BOOLEAN/BITBoolean只有确定是布尔语义时才用这张表不是死的我特意在配置里留了“自定义类型映射”的口子用户可以加一条DOUBLE - Float之类的覆盖规则。因为真实项目的字段语义千奇百怪生成器能做的是给一个合理的默认值而不是替业务做决定。2.3 为什么不直接套用现成脚手架看到这里估计有人会问既然这么麻烦为什么不用现有的微服务脚手架再配合一个在线接口文档反向生成代码我做 t3code 时其实权衡过。现成脚手架的问题在于“重”。你拉一个开源项目下来里面有几百个类一半用不上。而 t3code 生成的代码只包含当前表相关的几个文件不引入多余依赖也不绑架你的架构决策。生成出来的 Controller 是继续用 Spring MVC 还是换 WebFlux由你后续的模板调整决定工具本身不做限制。这种“轻”对于追求代码可控的老手特别重要我可以把生成的代码轻易融入现有项目而不需要为了迁就框架去改我的分层习惯。另一方面自己写生成器的过程中你会重新审视项目中哪些代码其实是重复劳动哪些才是核心价值。这个思维转变比工具本身还值钱。3. 从零实现核心代码与实操流程3.1 第一步定义数据源与生成配置我习惯用一个t3code.yaml做总入口里面直接写明数据库连接和生成选项。database: url: jdbc:mysql://localhost:3306/your_db username: root password: your_password driver: com.mysql.cj.jdbc.Driver generator: # 输出根路径默认是当前目录下的 generated-code outputDir: ./generated-code basePackage: com.example.module author: zhangsan # 需要生成的表支持通配符 tables: - sys_user - sys_role - sys_permission # 要生成的分层组件按需开关 layers: entity: true mapper: true service: true controller: true dto: true vo: true # 是否覆盖已存在的同名文件 overwrite: false配置项不多但每一条都有讲究。basePackage决定了整个包名路径com.example.module意味着生成路径是com/example/module和 Maven/Gradle 的标准目录结构对齐。tables我支持通配符比如sys_*因为它比手写一张张表名高效得多。overwrite默认是false这个必须提一下——如果你第一次生成后发现模板有小 bug修正后想要重新生成部分文件直接覆盖很容易把手工改动过的代码冲掉。所以我的习惯是模板稳定前一律不覆盖模板确定没问题的文件再手动开启覆盖。3.2 第二步编写模板文件t3code 的核心资产就在src/main/resources/templates目录里。我最初的版本直接写死 Java 代码后来全部换成了.ftl文件。目录结构如下templates/ ├── entity.ftl ├── mapper.ftl ├── mapperXml.ftl ├── service.ftl ├── serviceImpl.ftl ├── controller.ftl ├── dto.ftl └── vo.ftl拿最基础的entity.ftl来举例核心渲染逻辑大概是这样的package ${meta.packageName}.entity; import java.time.LocalDateTime; import java.math.BigDecimal; /** * ${meta.tableComment} */ public class ${meta.className} { #list meta.columns as column /** * ${column.comment} */ private ${column.javaType} ${column.camelName}; /#list #list meta.columns as column public ${meta.className} set${column.pascalName}(${column.javaType} ${column.camelName}) { this.${column.camelName} ${column.camelName}; return this; } public ${column.javaType} get${column.pascalName}() { return this.${column.camelName}; } /#list }注意几个关键点。meta是传给模板的根对象它有packageName、tableName、className、columns等属性。column.camelName是数据库字段user_name转换得到的userNamecolumn.pascalName则是UserName。这类转换逻辑在元数据读取模块里提前完成模板里不写大小写转换代码保持模板干净。为什么这么设计因为 Freemarker 里写字符串转换逻辑会让模板变得很难调试最好把所有业务/格式逻辑全部前置到 Java 代码里模板只负责做“填空”和“循环”两件事。Mapper XML 的模板更有意思。insert语句的列名、#{}占位符、set子句、where条件都得根据字段元数据动态渲染。比如selectByPrimaryKey的where部分只渲染主键字段的条件其余字段一概忽略。这就是为什么元数据模型里一定要有isPrimaryKey这个属性否则模板里没法区分普通字段和主键字段。3.3 第三步运行生成与结果校验t3code 的主程序入口在GeneratorApplication.java核心逻辑只有三步加载配置、读取元数据、批量渲染。别整花活命令行工具最重要的就是“可预期”。执行完生成后我习惯性做三件校验目录结构校验看包名路径是否和basePackage一致防止 Windows 下反斜杠问题导致目录创建错位。语法编译校验写一个简单的 Maven 子工程把生成代码丢进去mvn compile可以快速抓出注解缺失、 import 不完整等低级错误。内容抽查手动打开 Controller、ServiceImpl 各一眼确认没有把时间格式化写死、没有生成多余的空行、没有把数据库注释带成乱码。别小看第三步里“乱码”这件事。数据库表注释如果是中文生成到 Java 文件里经常出现????。这里有两个坑一是数据库连接串没带characterEncodingutf8二是模板文件本身编码不是 UTF-8。t3code 在加载模板时强制 UTF-8又统一在 JDBC URL 上默认追加编码参数问题才彻底解决。4. 生成代码的工程化落地与团队协作4.1 生成代码与 Spring Boot 的集成工具生成出来的代码要能用必须无缝融入 Spring Boot 项目。t3code 生成的 Entity 直接放在entity包下Mapper 接口放在mapper包下并且自动加上Mapper注解。这样 Spring Boot 启动时组件扫描能识别到 Mapper 接口配合MapperScan更是双保险。Controller 上默认标注RestController和RequestMapping(/api/${meta.lowerCamelName})Service 接口和 ServiceImpl 分开后者加Service。这就是典型的三层结构没有魔法够用且直白。这里要特别说一下 Lombok 的处理。t3code 模板默认生成手写的 getter/setter而不是依赖 Lombok 的Data。理由有两层。第一层是兼容性总有一些项目因为各种历史原因不愿意引入 Lombok手写访问器能降低接入门槛。第二层是可控性生成器输出的代码不仅是给机器看的更多时候是给新人当样例学的手写字段和方法更直观。当然配置里也留了useLomboktrue的选项打开后模板跳转生成Data、Builder等注解代码量能少一半。两种模板我都实际跑过团队自己选就好。4.2 通过 Checkstyle 与自定义规则保证生成质量生成器最尴尬的时刻是生成的代码自己都没通过团队的代码规范检查。所以我给 t3code 内置了和 Checkstyle 对齐的常见规则主要有类名使用 PascalCase方法名使用 camelCase常量使用 UPPER_SNAKE_CASE。每行代码不超过 120 个字符超长的链式调用要换行。方法体不允许超过 80 行针对生成的 ServiceImpl 里容易出现的超长事务方法。魔法值不允许直接出现比如状态字段比较时要用常量或者枚举。这些规则本身不复杂真正有价值的点是模板里写代码时就直接遵守规则而不是生成后再靠工具去格式化。我举个例子模板里所有if (xxx) {的后面必须跟一个空行再进入业务处理这样人类读起来舒服IDE 格式化也不会反复改。这种事你只有自己写一次生成器才会有深刻体会现成的生成工具永远不会根据你团队的 Code Style 去定制模板。4.3 在团队中推广的落地流程工具做出来不用就是浪费。我在团队内部推广 t3code 时定了一个非常轻的流程大家接受度很高开发者在数据库里新建表字段注释写清楚。把表名填入t3code.yaml运行一个命令t3code -c t3code.yaml。生成代码直接落到本地模块里然后mvn compile验证通过。人工 review 的重点不再是“代码重复”而是“业务逻辑有没有遗漏”。这里我特别强调“注释写清楚”这一个点。因为数据库字段注释会被直接带进 Entity 和 VO 的 Javadoc注释写得烂生成出来的代码文档也烂。有些同事一开始不以为然结果生成的注释全是“备注”“类型”这类废话等于没写。后来我们约定字段注释必须是一个能看懂业务含义的完整短句比如“用户最近一次登录时间”而不是“登录时间”。这个约定用了一段时间后生成的代码文档质量明显提升连接口文档的维护压力都小了很多。5. 常见问题与排查技巧实录5.1 高频问题速查表我整理了这段时间用 t3code 实际踩过的问题做成一张速查表问题现象根因解决办法生成文件里中文注释全是问号模板文件或 JDBC 连接编码不对模板强制 UTF-8URL 后加?characterEncodingutf8自增主键插入后获取不到 IDuseGeneratedKeys没打开在 Mapper XML 模板中加入useGeneratedKeystrue keyPropertyid表字段是create_timeJava 属性却叫createTimeSQL 无法识别未进行驼峰到下划线的映射在mybatis-config里开启mapUnderscoreToCamelCase同时在模板中保证列名引用正确多表生成的 Mapper 接口互相冲突不同模块同名类包名没有区分basePackage按模块拆开或者表名前缀映射到子包覆盖文件后手工优化代码丢失overwrite设置为 true开发期保持false代码确定不需要调整后再开启覆盖生成的 Controller 没有CrossOrigin前端联调报跨域模板里没加跨域注解可根据团队规范在模板中统一加入该注解而不是每个接口手动加BigDecimal字段返回 JSON 出现科学计数法未配置 JsonSerialize在实体字段上增加JsonSerialize(using ToStringSerializer.class)或全局配置统一处理以上问题里最常出现的是第一项中文乱码其次是第三项字段映射。遇到问题别急着改模板先打开生成的文件看一下实际输出再顺着元数据模型排查很容易定位是渲染问题还是数据读取问题。5.2 三个最值得注意的工程化问题第一字段类型映射不能闭门造车。我最初的版本把TINYINT(1)一律映射成Boolean后来有位做电商的同事提醒我他们的订单状态字段用TINYINT存 0、1、2、3 四种状态生成成Boolean直接输出灾难。所以我调整了默认映射只在字段名包含flag、enabled这类明确布尔语义时才映射为Boolean。这个策略对于真实业务更友好。第二模板别写太死。最早我把 Controller 里的分页参数写成了pageNum和pageSize结果有个项目前端用的是page和limit改模板要改好几个文件。后来我统一从配置中心读取分页参数名模板里只引用变量。这在做通用工具时非常重要凡是你觉得“每个团队可能不一样”的东西都应该做成可配置项不要凭自己喜好硬编码。第三别让生成器承载过多的权限控制逻辑。有个很自然的想法是“在模板里根据用户角色生成不同的代码”听起来很智能实际上非常难维护。t3code 的定位就是生成基础 CRUD权限这块我在 Service 层生成一个空壳方法checkPermission里面抛出“TODO 请根据业务实现权限校验”。这种刻意留白反而比强行生成一套权限脚手架更适合真实项目。6. 个人经验与扩展建议做了这个项目之后我对“代码生成”有了新的看法。工具不一定要做得多万能也不一定要覆盖所有设计模式它真正改变的是团队的交付节奏。以前接到一个新后台模块花一上午把 CRUD 写完再调格式现在一分钟生成代码剩下的时间全部留给业务梳理、权限设计和接口文档这才是这个工具真正的价值。最后再分享一个小技巧t3code 里我加了一个--dry-run参数运行时不真正写文件只打印每个模板将要生成的路径和文件大小。这个参数对排查模板错误特别有用因为你不必等代码写到一半才报错可以提前捕捉异常。后续我还在尝试的扩展方向是把数据库反向解析能力抽出成独立 SDK这样不仅能在生成器里用也能在写数据字典文档时直接读表结构。如果你也在为重复 CRUD 头疼真心建议画一个下午把这个问题想清楚自己动手写一套轻量生成器的收益绝对比你想象的更大。
返回列表