
在中小团队里待久了你会发现一个很现实的问题团队里真正稀缺的并不是会写复杂算法的“架构师”而是能把大量重复性CRUD代码写得又快又稳的人。我们团队去年数据中台项目里的订单模块、用户模块、权限模块加起来近两百个接口真正需要动脑子的业务规则不超过20%剩下80%都是按既定模式生成的样板代码。也就是在这个背景下我们开始用T3Code这套“三级代码生成”的模式把整个团队的交付节奏从“一星期一个模块”提升到“一天三个模块”。这篇文章不是给T3Code做广告它本身也只是一套我们自己梳理后沉淀下来的方法论配合几款开源工具落地。我写这篇内容的目的是想把“三级代码生成”这件事从头到尾讲透它解决什么问题、核心思路怎么拆、实操的时候每一步怎么配、以及我们在真实项目里踩过的坑。如果你也在做中后台系统、微服务拆分、或者要给多个端Web、小程序、管理后台提供统一接口这套东西大概率能帮你省下一大笔时间。T3Code里的“T3”代表三个层级数据层Table、业务层Business、接口层API对应着一张数据库表从建模到被前端调用全链路中需要手工重复劳动的部分。核心做法是先定义好数据模型然后分三层生成代码每一层的产出都是完整可编译、可运行的工程文件而不是零散的代码片段。这套模式特别适合那种“数据库表结构一确定CRUD就基本定型”的业务场景比如后台管理类的用户管理、订单管理、配置管理、审计日志管理。1. T3Code的概念拆解一个代码项目如何分三层生成1.1 为什么要把代码生成拆成三个层级很多团队用过“一键生成代码”的工具比如根据数据库表自动生成实体类、Mapper和基础Controller。这类工具在小项目里确实爽但放到真实企业项目里会发现一个问题它生成的代码和你的业务代码混在一起而且生成的逻辑太“死”了——如果你想在里面加一个缓存、加一个操作日志、加一个数据权限过滤要么把生成器改一遍重新跑要么手工改生成结果不但麻烦而且后续维护会非常痛苦。T3Code的思路是把生成过程拆成三个独立的阶段让每一阶段产出的代码有明确边界数据层Table只负责和数据库打交道包含实体类、数据库映射接口如MyBatis的Mapper、基础的数据访问方法单表增删改查、分页查询、批量操作。业务层Business包含Service接口和实现类里面放和具体业务场景相关的逻辑比如字段唯一性校验、状态流转检查、数据权限过滤、缓存读写策略。这一层是手工编码密度最高的地方也是代码生成最该“留白”的地方。接口层API包含Controller层、RESTful接口定义、DTO对象数据传输对象、接口参数校验逻辑。这一层直接面对前端调用方需要保证出入参结构的稳定性。这样拆有三个直接好处第一生成的代码和业务代码不会互相“污染”。实体类和Mapper这种相对固化的代码可以随时重新生成覆盖而Service层里你写的业务逻辑不会因为表结构变化被一键冲掉。第二不同的角色可以各司其职。实习生或者初级开发可以负责数据层和部分接口层的实现核心开发集中精力在业务层。我们在项目里就是让两个刚入职的校招生负责对接生成器的模板调优三天内把所有模块的基础CRUD代码全部铺完效率非常明显。第三架构边界清晰代码评审压力小。Controller不写业务、Service不写SQL、Mapper不做复杂查询这种分层约束靠人管很难坚持但靠生成器从源头约束就很自然。1.2 T3Code核心技术栈与选型背景这个方案实际上不需要特定的软件我们落地时用的是一套组合数据库端用MySQL 8.x持久层用MyBatis Plus配合MyBatis Generator做数据层生成的底座业务代码用Spring Boot 3.x JDK 17接口文档用SpringDoc OpenAPI 3。模板引擎方面我们用Freemarker自己写了生成模板控制生成的代码风格。这套技术选型是经过对比后确定的。选MyBatis Generator的原因主要是它能从数据库表反向生成实体类和Mapper接口而且支持自定义注释、支持Lombok这类简化样板代码的注解。MyBatis Plus则在MyBatis Generator基础上提供了非常完整的基础CRUD方法比如selectPage、selectOne、insertBatch使得数据层生成的代码量能大幅减少。我们还在其上封装了一层BaseService把分页参数标准化、把操作日志自动植入项目里的Service实现类不需要处理分页细节。接口层我们坚持在生成时直接产出OpenAPI注解。很多团队是在代码写完后才补接口文档甚至文档已经和代码不一致了。我们的生成器会在Controller上自动加Tag、在DTO上加Schema描述做接口设计时先通过OpenAPI文件做前置评审再生成代码这样“文档即代码、代码即文档”的效果就达成了。2. 数据模型与生成机制从建表到代码的映射关系2.1 数据库表设计与代码生成之间的约束用T3Code这套模式有个前提要确认数据库表的设计质量会直接决定生成代码的可用性。如果你打算靠设计一张字段命名混乱、类型模糊、注释缺失的表来生成一套完整后端代码结果大概率是推倒重来。我们内部会先约定一套建表规范比如必须有主键字段id BIGINT、必须包含create_time和update_time两个公共字段、每个字段必须有COMMENT注释、逻辑删除字段统一叫deleted TINYINT、乐观锁版本字段统一叫version INT。这些规范看起来麻烦但有了它们生成器才能“读懂”表结构。以一张用户表为例CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(64) NOT NULL COMMENT 登录名, password_hash VARCHAR(128) NOT NULL COMMENT 密码哈希, real_name VARCHAR(64) DEFAULT NULL COMMENT 真实姓名, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, mobile VARCHAR(32) DEFAULT NULL COMMENT 手机号, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0未删1已删, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;这张表已经包含了我们约定的大部分规范字段生成器看到这样的表结构能自动产出以下内容实体类SysUser字段类型自动映射BIGINT→Long、VARCHAR→String、DATETIME→LocalDateTime自动带上Lombok的Data注解、MyBatis Plus的TableName和TableId注解。接口类SysUserMapper继承BaseMapperSysUser基础单表操作方法全部可用。Service接口SysUserService及其实现类SysUserServiceImpl继承我们封装的BaseServiceImpl包含分页查询、根据ID批量查询、逻辑删除等通用方法。Controller类SysUserController提供GET /api/sys-user/page、GET /api/sys-user/{id}、POST /api/sys-user、PUT /api/sys-user/{id}、DELETE /api/sys-user/{id}这些标准接口。这一套产出下来一段零业务、但已经完全能启动和联调的后端代码就出现了。2.2 字段类型映射规则与命名转换逻辑很多初次尝试T3Code的人会卡在一个小细节上数据库字段是user_name下划线风格实体类是userName驼峰风格Mapper XML里要把两者正确对应。生成器处理这个问题的方案是内置一套规则引擎在解析表结构时统一做字段名转换。我们平时用的转换规则可以分为几类普通字段表字段real_name→ 实体属性realName同时保留MyBatis Plus的自动驼峰映射不需要额外写ResultMap。公共字段create_time、update_time默认插入填充改成自动填充策略。我们在MyMetaObjectHandler里统一处理插入时自动写入LocalDateTime.now()。逻辑删除字段字段名为deleted的会被识别为逻辑删除字段加上TableLogic注解后续所有deleteById操作自动变成UPDATE ... SET deleted 1而不是物理删除。乐观锁字段字段名为version的会被自动加入乐观锁插件处理更新操作会带上WHERE version #{oldVersion}防止并发覆盖。之所以要在生成器里内置这些约定是因为“能够生成代码”和“能够生成不用返工的代码”是两回事。实体类的字段类型映射、逻辑删除识别、乐观锁支持这些如果靠人工逐张表去写不仅效率低而且一旦漏掉某张表后面上线出了数据问题排查成本会非常高。另外生成代码时Controller的RESTful路径命名也有讲究。我们的模板会把实体类名SysUser转换为sys-user生成/api/sys-user前端调用时看到这个路径和表名一一对应排障时可以直接从URL反查数据库表效率非常高。表名前缀sys_在生成路径时会被自动去掉避免URL里出现/api/sys-sys-user这类冗余命名。3. 实操过程详解基于T3Code生成一套用户管理模块3.1 生成前的环境准备和初始化配置实际操作层面我们是在一个Spring Boot空项目的基础上来配置T3Code的。先说明一下这里为了让你能直接复现我把关键的配置文件都贴出来这些配置都是我们在生产环境验证过的。项目的Maven依赖核心部分如下dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.7/version /dependency dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version2.3.32/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.3.0/version /dependency除了Maven依赖我们还要在src/main/resources下准备三个东西Freemarker模板目录存放Controller、Service、ServiceImpl、Mapper、Entity的模板文件、生成器配置类Java类用来指定数据源和命名规则、以及MyBatis Plus的分页插件配置。分页插件的配置建议直接在启动类或者一个Configuration类里注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(500L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }提示maxLimit一定要设置。如果不设置前端传一个size999999的分页参数会把整张表的数据全量返回这在生产环境是非常危险的。我们把这个值设为500意味单次分页上限就是500条超了直接报错。3.2 代码生成的执行入口与模板定制生成器的执行逻辑是写在一个Java主类里的通过命令行直接运行。核心部分代码如下public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create(jdbc:mysql://localhost:3306/your_db, username, password) .globalConfig(builder - builder .author(your-name) .outputDir(D:/generated-code) .dateType(DateType.TIME_PACK) .commentDate(yyyy-MM-dd) ) .packageConfig(builder - builder .parent(com.company.project) .entity(entity) .service(service) .serviceImpl(service.impl) .mapper(mapper) .controller(controller) ) .strategyConfig(builder - builder .addInclude(sys_user, sys_role, sys_permission) .addTablePrefix(sys_) .entityBuilder() .enableLombok() .logicDeleteColumn(deleted) .versionColumn(version) .controllerBuilder() .enableRestStyle() .enableHyphenStyle() .build() ) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } }这里面有两个小点容易被忽略第一是enableHyphenStyle()。这个配置会让Controller的路径映射变为中划线风格也就是sysUser变成sys-user。这个细节看起来简单但如果团队用了保持默认驼峰风格的生成器前端联调时就会发现URL里带大写字母非常别扭而且和后端网关集成的路径规则往往对不上。第二是addTablePrefix(sys_)。表名前缀是业务模块的归属标识但在生成实体类时我们希望类名是SysUser而不是SysUserTable这种无意义的名字模板配置会自动把表前缀从类名中去掉。这个规则在多模块项目里更有价值比如order_前缀的订单表、user_前缀的用户表生成出来的类名能自然区分归属。3.3 生成结果的二次加工补全业务层代码代码生成器跑完之后得到的是一个能启动、能跑基础接口的后端项目。但真实的业务系统肯定不止CRUD比如用户管理里会有“创建用户时检查用户名是否重复”这种业务规则这是T3Code模式中最有发挥空间的地方。我们的做法是在生成器里给Service实现类预设一个扩展包的“钩子”区域。每次生成时模板会输出这样一段代码Service RequiredArgsConstructor public class SysUserServiceImpl extends BaseServiceImplSysUserMapper, SysUser implements SysUserService { // 业务扩展区域生成时不会被覆盖 Override public void checkUsernameUnique(String username) { long count this.lambdaQuery().eq(SysUser::getUsername, username).count(); if (count 0) { throw new BizException(用户名已存在); } } // 业务扩展区域结束 }为什么我们要在生成模板里预留这么一块区域因为在真实项目里表结构会变化、新需求会加字段生成器重新跑的时候会把整个文件覆盖如果你在生成器输出的文件里直接写了业务代码下次一覆盖全没了。我们在模板里加了两行注释标记“业务扩展区域”配套的自定义模板里做了一层判断重新生成时如果文件已存在就读取文件里“业务扩展区域”之间的内容回填到新生成的文件里。这个机制让我们既能享受代码生成的高效率又不会因为重新生成而丢失手工逻辑。这块机制本质上就是一个简单的代码合并策略用Python脚本实现的遍历每个Java文件匹配开始标记和结束标记提取中间内容再拼接到新生成的文件对应区域。这种方式看似绕实际用起来非常可靠团队跑了半年多没有出现一次因为重新生成覆盖而丢代码的事故。3.4 接口层生成逻辑与联调效率提升Controller部分的生成我们盯得比较紧因为这是前端的直接入口。字段校验规则、参数名规范、返回包装结构前端希望有统一格式而后端又希望每个模块有一些差异这个矛盾用生成器从源头统一解决。生成的Controller代码大致如下Tag(name 系统用户管理) RestController RequestMapping(/api/sys-user) RequiredArgsConstructor public class SysUserController { private final SysUserService sysUserService; Operation(summary 分页查询用户列表) GetMapping(/page) public ApiResultPageResultSysUserView page(Validated SysUserQuery query) { return ApiResult.success(sysUserService.pageCustom(query)); } Operation(summary 查询用户详情) GetMapping(/{id}) public ApiResultSysUserView detail(PathVariable Long id) { return ApiResult.success(sysUserService.getDetailById(id)); } Operation(summary 创建用户) PostMapping public ApiResultLong create(RequestBody Validated SysUserCreateReq createReq) { return ApiResult.success(sysUserService.createUser(createReq)); } }这里有一个值得展开说明的实践生成器在生成Controller的同时还会生成一套与实体类并不完全相同的视图对象VO和请求对象DTO。我们强制要求接口出入参不做直接用实体类接收而是生成SysUserCreateReq、SysUserUpdateReq、SysUserView三个独立类。因为实体类SysUser里包含passwordHash、createTime这些字段如果直接暴露给前端要么把密码哈希也返回出来要么每次手动屏蔽这都极其容易埋雷。生成器把三个对象分开生成之后各层之间的参数边界自然就清晰了。联调效率提升也是肉眼可见。因为每个接口的出入参结构都是生成器按照规范生成的前端在接口还没有开始联调时就能拿到OpenAPI文档直接根据文档Mock数据。很多后端团队还在“前端等后端代码写完才能开始”的状态用了T3Code后可以做到前后端并行开发整个交付周期缩短了接近1/3。4. 常见问题与检查清单T3Code落地过程中的坑4.1 生成的代码和手写代码如何避免冲突这是用代码生成器最常被问的问题。很多团队拒绝用生成器的理由就是“生成的内容不好改改了后面没法再生成”。我们的经验是用好三个策略解决策略一是生成物分离。我们约定生成器生成的完整类比如实体类和Mapper接口统一放在entity、mapper包里这些包里的代码就是被执行的不需要手改。要改数据库字段的话直接改表结构然后重新生成覆盖绝不手工去改这些类。策略二是业务代码和模板代码隔离。Service层的具体业务实现放在“业务扩展区域”内这个区域在重新生成时会被保留。我们不建议在实现类里写任何和ORM映射、参数校验相关的代码这些都由基类和注解来处理。策略三是自定义模板而不是改生成结果。如果你觉得生成出来的代码风格不符合团队规范比如注释格式不对、没有加Slf4j、缺少某个基础字段校验正确做法是修改Freemarker模板而不是每张表生成完再去手工改。我们早期犯过一个错误要求开发在生成代码后手动给所有Controller加一个Log注解结果大家经常忘了加后来直接把Log写进模板里这个问题彻底消失。4.2 多模块项目与团队成员协作的最佳实践T3Code做单模块项目很容易但真正复杂的场景是多模块多团队协作。我们现在的项目分了api、model、service、provider四个模块T3Code的生成策略也需要相应调整。数据层模型的生成输出到model模块这个模块只包含实体类、DTO、VO不依赖Spring容器。服务接口和实现类生成到service模块它依赖model模块。接口层再生成到provider模块负责对外暴露HTTP接口。这种模块划分的出发点是为了支持多团队并行开发。比如订单团队负责order模块用户团队负责user模块两个团队可以各自维护自己的数据表前缀和生成配置代码产物的依赖关系通过Maven互相引用不会出现互相改代码的情况。协作时的另一个经验是生成后的代码一定要走代码评审流程不能因为“是生成器产的”就不看。生成器代码有一个风险换版本、换模板后产出代码可能出现潜在的破坏性变化比如MyBatis Plus升级后lambdaQuery()的用法轻微变化、某个方法签名不通这类问题如果不做代码评审会在编译阶段才暴露出来甚至到了跑起来才会发现某个接口行为变了。我们的应对是每次升级生成器或调整模板之后先跑一遍全部生成的代码做差异对比只保留必要的变更。4.3 数据库表结构变更后如何安全地重新生成在实际项目中没有哪张数据库表是一成不变的。T3Code模式下表结构变更后的操作流程我们的经验是分三种情况处理纯新增字段直接在数据库表加字段然后重新运行生成器实体类会自动带上新字段前端联调不受影响不需要额外处理。字段类型变更比如从VARCHAR(32)改成VARCHAR(64)或者从VARCHAR改成TEXT这类变更会在实体类中反映如果前端接口出入参有校验长度限制需要同步更新DTO里的Size注解和OpenAPI文档再重新生成。删除字段或者改名这类变更风险最大因为它可能导致线上运行的代码里引用了不存在的字段。我们建议的做法是在数据库层面先做兼容比如加一个透明映射视图同时把代码里的旧字段标记为Deprecated并缓一个版本等没有调用方后再从表和生成模板里移除。直接删字段然后重新生成代码这在开发环境没太大问题但生产上非常容易出现“因为某个字段被删代码里做序列化反序列化时直接报错”的情况。注意上线前的回归测试无论如何都跳不过。即便所有代码都是生成的只要表结构变了接口的HTTP响应结构就可能变前端如果缓存了旧结构就会报解析错误。我们的流程是在重新生成代码之后、发版之前用自动化接口测试脚本跑一遍所有核心接口对比响应JSON Schema有变更就主动同步给前端。5. 扩展思路与团队效率总结T3Code这套模式用了一整个迭代周期之后我对它的感受从“省时间”变成了“省心”。省时间说的是代码量减少一个模块的CRUD代码量少了至少70%新人来了只要懂模板规则就可以快速交付省心说的是它把团队里很多需要“自觉”的事情变成了“默认如此”比如注释规范、异常统一处理、分页结构统一、接口路径风格统一这些都不是靠人盯出来的而是生成器在这个模式里规定死的。它至少有两个方向值得进一步挖掘。第一个方向是往前端延伸。现在后端代码能按三层生成前端API调用层也可以设定对应的生成策略根据OpenAPI文档自动生成TypeScript的API封装代码、接口路径常量、DTO类型定义。我们已经在管理后台项目里用了一版小型脚本效果还可以前端同学不用再手工敲api/user.ts里的几十个接口定义了。第二个方向是和低代码平台或内部工具平台结合。既然生成器的输入是数据库表结构输出是完整工程代码那么完全可以再接一层管理界面让产品经理在里面画表结构、配置字段校验规则然后点一个按钮就生成后端代码和前端代码。我们团队做了一个内部版的“表结构管理页面”把生成器对接到一个Web界面上让后端工程师不用打开IDE也能完成80%的增删改查接口交付。对非核心系统而言这种“界面化配置代码生成”的模式会比从零手写更可控。如果你所在的项目也遇到类似的痛点比如大量重复CRUD、团队协作成本高、接口文档和后端代码不一致我非常建议你先花两天时间在自己项目里试试T3Code这套方法的轻量版本拿一张数据表跑通生成流程看看产出代码的边界是不是清晰、二次修改是不是方便。多数场景下一旦跑通一条线你会自然而然想把所有模块都接入进来。但请注意这套方法要适配自己团队的代码规范不要照搬我的模板配置否则一定会出现“生成的代码和手写的代码风格打架”的问题。