新闻详情

新闻详情

首页 / 资讯中心 / 详情

模板驱动代码生成器实战:Java分层CRUD代码一键生成

发布时间:2026/10/1 4:34:21来源:尧图网络
模板驱动代码生成器实战:Java分层CRUD代码一键生成
1. 模板驱动的代码生成器到底在解决什么问题先聊一个很多人都有过的经历新项目启动建包、建类、写实体、写Mapper、写Service、写Controller一套常规的增删改查下来半小时起步。如果表有三十张呢如果每张表还要配查询条件、分页、DTO转换呢一天基本就搭进去了。更让人崩溃的是这套流程里的代码高度相似无非是字段名、类型、表名不一样结构几乎雷同。写多了之后人会麻木而且极其容易出错——漏一个字段、写错一个类型、少加一个注解调试的时候才发现来回折腾的时间比手写还多。我这次做的这个“自定义代码生成器”核心思路很简单不依赖任何重量级框架不引入复杂的元数据模型只靠一套模板引擎加数据库表结构信息把“建包、建类、写注释、写CRUD”这些重复劳动全部自动化。输入是一张表结构输出是Controller、Service、Mapper、Entity、DTO、VO一整套分层代码并且每层的代码风格完全可控因为模板是自定义的。这个生成器适合谁适合那些还在用传统SSM、Spring Boot单体项目、或者内部老项目里需要频繁写CRUD的团队。也适合想理解“代码生成”本质的开发者因为这里没有黑魔法有的是模板、数据模型、渲染引擎这三样东西的组合。你不需要懂AI不需要懂复杂的低代码平台原理只要会Java、会MyBatis、会Spring Boot的基础用法就能把一个生成器从零搭起来。先把话说在前面这次分享的代码基于Java Maven MyBatis Plus Velocity模板引擎但核心思路可以迁移到任何语言和任何ORM框架。我用这套方案在我自己维护的几个内部项目里跑了大半年累计生成的代码超过三十个业务模块稳定性和效率都经得起重复调用所以敢拿出来写这么一篇实战笔记。2. 整体方案选型为什么是模板驱动而不是代码拼串或注解反射2.1 模板引擎的不可替代性很多人第一次写生成器的时候第一反应是“拼字符串”。比如用一个StringBuilder循环字段把Entity类的代码一段一段拼出来。这种做法在小范围、固定表结构的前提下确实能跑通但一旦遇到这些情况就崩了字段类型需要映射数据库的datetime对应Java的LocalDateTime还是Datetinyint对应Boolean还是Integer不同表需要不同注解逻辑删除字段要加TableLogic乐观锁字段要加Version需要生成的文件数量多、结构嵌套深包名、导入、类注释、方法注释都要动态变化拼字符串的本质是“手写代码生成器”逻辑和内容耦合在一起改一处要动很多处。而模板驱动的本质是“把代码写进模板把变化留成占位符”逻辑和内容分离。改模板就相当于改生成规则不用碰Java代码。这一层分离在维护阶段的价值会无限放大——尤其是当你的生成器已经支撑几十个模块之后需求一变更改一个模板文件就能全局生效。我选的是Velocity原因是它在Java生态里历史最久、依赖最简单、语法对普通开发者来说也足够直观。用$!{entityName}这种占位符去引用数据模型用#foreach循环字段列表用#if处理特殊字段写起来就和写普通Java文件差不多学习成本很低。2.2 分层代码生成的难点到底在哪所谓“分层代码”不是简单地把文件分开就行。真正的难点在于层与层之间的依赖关系和命名规则必须保持一致。比如Controller要注入ServiceService要注入MapperEntity里的字段要和数据库列一一映射DTO和VO又要和Entity字段对应。如果在生成的时候各写各的命名不一致哪怕代码生成出来了也根本跑不起来。所以我在设计数据模型时不只是存一张表的字段列表还会预计算所有层的类名和包名Entity叫UserEntityMapper接口叫UserMapperService接口叫UserService实现类叫UserServiceImplController叫UserControllerDTO叫UserDTOVO叫UserVO。这些名字在生成之前就全部确定好模板里引用的所有变量都从同一个TableModel对象取值这样层与层之间的引用关系天然一致。这算是这次实战里最核心的一个设计决策先构建完整的上下文模型再去渲染模板而不是拿到表结构就开始生成。2.3 再来看看为什么不选重型代码生成工具市面上现成的代码生成器很多MyBatis Generator、MyBatis Plus Generator、还有各种基于IDEA插件的生成工具。那为什么还要自己写原因很现实公司的代码规范通常和默认生成器不一致改模板有时候比重新写一个还麻烦默认生成器输出的代码风格偏老比如MyBatis Generator生成的XML极其啰嗦维护成本高大多数现成工具不生成Service层和Controller层或者生成了也是鸡肋基本要自己重构团队里如果还有特殊需求比如统一返回类型、统一异常处理、日志注解、权限注解默认工具完全没法满足自己写的生成器虽然前期投入两三天时间但之后每一次生成都是按团队规范输出的不用二次修改。长期来看省下来的时间非常可观。3. 核心实现拆解从数据库表到完整分层代码的全流程3.1 必须梳理清楚的数据模型整个生成器最底层的东西是表结构信息。我用的是JDBC的DatabaseMetaData来读取不依赖任何ORM框架的元数据接口。这样可以让生成器保持独立以后哪怕换ORM框架读表结构的部分也不用动。读取到的信息会封装成三个层级对象ColumnModel字段名、数据库类型、是否主键、是否自增、是否允许为空、注释。TableModel表名、表注释、字段列表、主键字段、转换成Java的属性名、类名等派生态。GlobalModel基础包名、作者名、生成日期、输出路径、是否覆盖文件等全局配置。其中字段类型映射是最容易出问题的地方。MySQL的datetime、timestamp、date要区分处理tinyint(1)通常映射为Booleantinyint其他长度映射为Integerbigint映射为Longdecimal映射为BigDecimal。这个过程我单独抽了一个TypeMapper类输入数据库类型字符串输出Java类型字符串和需要导入的包路径。核心逻辑就是一张映射表加少量特殊判断不需要搞得很复杂。3.2 Velocity模板的模块化设计模板文件我按照Java文件类型分开存放每个类型的模板独立一个文件放在templates目录下templates/ ├── entity.vm ├── mapper.vm ├── service.vm ├── serviceImpl.vm ├── controller.vm ├── dto.vm ├── vo.vm └── xml.vm每个模板内部再用Velocity的#parse引入公共片段。比如entity.vm开头有一段包名和导入的公共信息dto.vm和vo.vm都复用同一条公共模板这样能避免在多个模板里重复维护相同的导入列表。模板内容的核心思路是“Java文件长什么样模板就长什么样”只是把可变的地方替换成占位符。拿Entity模板的关键段落举例package $!{packageName}; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; Data public class $!{entityName} { #foreach($column in $columns) #if($column.isPrimaryKey) TableId(value $!{column.columnName}, type IdType.AUTO) #elseif($column.javaType LocalDateTime) TableField(value $!{column.columnName}, fill FieldFill.INSERT_UPDATE) #else TableField(value $!{column.columnName}) #end private $!{column.javaType} $!{column.propertyName}; #end }注意这里我对LocalDateTime类型的字段自动加了fill FieldFill.INSERT_UPDATE这是配合MyBatis Plus的自动填充功能。如果你不需要自动填充把这个#if分支删掉即可。模板的灵活性就在这里——它完全是根据你的持久层框架和业务规范定制的。3.3 生成器的门面类一脚油门到底所有生成动作都从CodeGenerator类的generate()方法开始。它的职责是读取全局配置、连接数据库、解析表结构、构建模型、调用Velocity渲染、创建目录结构、写文件。整个流程跑下来大概是这样public void generate(String tableName) throws Exception { TableModel table metadataReader.readTable(tableName); TableModel enrichedTable modelEnricher.enrich(table); MapString, Object context buildVelocityContext(enrichedTable); renderAndWrite(context, enrichedTable); }每个步骤都很轻量但有一个细节值得展开说modelEnricher。这一步专门负责把原始的TableModel转换成“包裹了各类规范信息的完整模型”。比如把create_time转成createTime把is_deleted识别为逻辑删除字段把version识别为乐观锁字段并且给TableModel增加一个templateFileList属性里面列出这次生成要渲染的所有模板文件及其输出路径。这样设计的好处是渲染模板的时候生成器本身不需要关心业务逻辑只需要遍历templateFileList逐个渲染、逐个写文件。如果某天你想额外生成一个QueryDTO或者一个ExcelExportUtil只需要在modelEnricher里加一项模板文件里加对应的.vm其他部分完全不用动。4. 实操过程记录用一张“用户表”把整个流程跑通4.1 准备阶段数据库表设计和项目依赖我拿一个最简单的用户表来演示表名t_user字段如下字段名类型备注idbigint主键自增usernamevarchar(64)用户名passwordvarchar(128)密码nicknamevarchar(64)昵称emailvarchar(128)邮箱phonevarchar(20)手机号statustinyint(1)状态0禁用1启用create_timedatetime创建时间update_timedatetime更新时间deletedtinyint(1)逻辑删除生成器依赖里只需要三样东西Velocity引擎、MySQL JDBC驱动、Lombok模板生成的代码要用。Maven依赖配好之后写一个最简单的启动入口传入表名就能生成。4.2 生成过程一条命令七层代码落盘我实际的调用代码是这样的public static void main(String[] args) throws Exception { GlobalConfig config new GlobalConfig(); config.setBasePackage(com.example.demo); config.setAuthor(老周); config.setOutputDir(./generated-code); config.setUrl(jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8); config.setUsername(root); config.setPassword(123456); CodeGenerator generator new CodeGenerator(config); generator.generate(t_user); }执行之后控制台会打出每一步日志。最终目录结构长这样generated-code/ └── com/example/demo ├── controller/UserController.java ├── service/UserService.java ├── service/impl/UserServiceImpl.java ├── mapper/UserMapper.java ├── mapper/xml/UserMapper.xml ├── entity/UserEntity.java ├── dto/UserDTO.java └── vo/UserVO.java七层代码一次全出来。我截一段生成后的Controller代码让大家感受一下输出质量package com.example.demo.controller; import com.example.demo.dto.UserDTO; import com.example.demo.service.UserService; import com.example.demo.vo.UserVO; import com.example.demo.common.PageResult; import com.example.demo.common.Result; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/user) RequiredArgsConstructor public class UserController { private final UserService userService; GetMapping(/{id}) public ResultUserVO getById(PathVariable Long id) { return Result.success(userService.getById(id)); } PostMapping(/page) public ResultPageResultUserVO page(RequestBody UserDTO dto) { return Result.success(userService.page(dto)); } PostMapping public ResultBoolean save(RequestBody UserDTO dto) { return Result.success(userService.save(dto)); } PutMapping public ResultBoolean update(RequestBody UserDTO dto) { return Result.success(userService.update(dto)); } DeleteMapping(/{id}) public ResultBoolean delete(PathVariable Long id) { return Result.success(userService.delete(id)); } }每一层的代码都按公司规范来Controller统一返回Result包装类型分页统一用PageResultService接口暴露的方法命名统一Entity里的逻辑删除字段自动加上TableLogic乐观锁字段自动加Version。这些规范在模板里写死之后团队里任何一个人生成出来的代码都是同一种风格看代码的人再也不用费劲去适应不同的写法。4.3 命名策略和类型映射的避坑清单这一块是生成器最容易翻车的地方我把实际踩过的坑整理成一份清单照着避坑能省很多事。数据库字段名和Java属性名的转换MySQL里常见的下划线命名create_time要转成createTime。直接用字符串拆分再拼接即可不需要引入第三方工具。但要注意首字母大写的场景比如表名t_user转换成类名UserEntityt_order_item转换成OrderItemEntity转换规则要对下划线分割后的每个单词做首字母大写同时把开头字母去掉。类型映射的边界情况tinyint(1)和tinyint(4)在MySQL JDBC驱动里返回的ColumnTypeName不一样前者往往是TINYINT(1)后者是TINYINT。如果只按TINYINT处理逻辑删除字段会被错误映射成Boolean而状态字段被映射成Byte。实际项目里最好做一个黑名单处理在读取列信息时如果字段名包含deleted、enabled、status这类关键词单独指定映射类型。我的做法是维护一个customTypeMap优先查自定义映射查不到再用默认规则。主键类型的问题如果表主键不是自增的TableId的type要设置成IdType.INPUT或者IdType.ASSIGN_ID不能一刀切用IdType.AUTO。我在模板里通过#if($column.isAutoIncrement)去做判断这样遇到雪花ID场景也能正确生成。注释和作者信息表注释自动生成类注释字段注释自动生成属性注释。这里的细节是如果字段注释为空模板里不能输出一个空的/** */块要在渲染前把空字符串处理好。我在ColumnModel里加了hasComment属性模板里用#if($column.hasComment)判断。5. 分层代码里那些容易“看起来对、跑不起来”的细节5.1 Service层接口与实现类的对应关系如果只生成实体、Mapper、Controller其实很多项目也能跑但分层规范要求Service接口和实现类分开。这里有个特别容易出现的隐患实现类的Service注解和接口的自动装配。如果Controller通过构造器注入UserService接口实现类必须加上Service并且实现UserService接口否则Spring启动直接报No qualifying bean of type。我在模板里做了两个保险措施。第一UserServiceImpl的类注释上自动写明“该实现类由代码生成器生成业务逻辑请在此类中扩展”提示开发者在生成后补充自定义逻辑。第二所有生成类都加上RequiredArgsConstructorService实现类里自动注入MapperController里自动注入Service省去手写Autowired的样板。但这里有个前提你的项目里必须开启Lombok否则这套生成方案要改成传统getter/setter加构造器模板改起来也不复杂。5.2 DTO和VO的字段映射策略DTO、VO和Entity之间字段基本一致但完全复用Entity会带来问题Entity直接暴露给前端可能会把password字段返回出去。所以生成DTO和VO时模板里要做一次字段黑名单过滤。我在modelEnricher里配置了默认排除字段password、secretKey、deleted等敏感或逻辑删除字段在生成VO时直接跳过而在生成DTO时保留除deleted以外的字段。这样即使后续有新的敏感字段加入表结构也只需要在配置里加一个字段名不用改模板。DTO和VO之间怎么转换我直接在VO里生成一个静态的fromDTO方法这样VO和DTO的映射逻辑写死在生成代码里改字段时重新生成即可。如果你用的是MapStruct这类工具模板里也可以生成对应的Mapper接口看团队选型。5.3 MyBatis XML模板的兼容性问题虽然MyBatis Plus有BaseMapper提供的CRUD方法但多表联查、复杂SQL还是得写XML。我生成的UserMapper.xml里包含了基础的ResultMap、BaseColumnList和单表的selectPage查询。这里有个刚踩过的坑如果表里有deleted字段基础的查询SQL必须带上deleted 0逻辑删除条件否则MyBatis Plus的TableLogic只对内置方法生效对你在XML里手写SQL不生效。生成XML模板时我加入了#if($table.logicDeleteFieldName)判断自动在where条件中追加一条逻辑删除过滤这个细节当时排查了很久才发现。另外XML文件的namespace一定要和Mapper接口的完全限定名一致这个在模板里直接用${mapperInterfaceName}变量生成天然保证不错。6. 常见问题与排查技巧实录6.1 表结构变化后重新生成如何避免覆盖业务代码这是生成器用得越久越容易遇到的问题。第一次生成的代码没问题但开发了一个月后你在这个文件里加了很多业务逻辑这时候数据库加了一个字段如果直接重新生成整个文件被覆盖业务代码全没了。我的处理方式生成器默认不覆盖已有文件。writeFile方法里如果目标是Java文件且已经存在就改成增量更新。增量更新的策略是把新生成的类文件中的字段、方法对比旧文件只把新增的字段和方法合并进入旧文件。这个合并逻辑我用的是JavaParser来做AST解析有点重但对于经常改表的项目这是必须的投资。如果你不想引入JavaParser有一个轻量替代方案新建一个DTO来承载新字段把新旧转换逻辑写在代码里。但这会破坏“分层规范”所以我最终选了AST合并路线。实现细节不展开了核心思路是解析旧文件删除旧字段中已经不存在的字段新增新字段保留自定义方法。实测下来只要业务代码写在自定义方法里不被合并逻辑误删基本是安全的。6.2 模板渲染报错的定位方法Velocity报错日志有时不太直观比如VelocityException: Reference table not found这一类。出现这种情况八成是context里没有放这个变量或者是变量名拼错了。我的定位方法很原始但很有效在模板最前面加一行$!{debugInfo}调试时把模型对象直接打印成JSON字符串放进context里错一眼就能看出来。再有就是#foreach循环里少了#endVelocity会直接报语法错误但错误信息有时指向文件开头不指向具体问题行。我建议写模板时先写一个最小可用版本只渲染包名、类名和几个核心字段确认语法没问题之后再逐渐增加复杂逻辑。这样即使出错也知道是哪一段新增内容引入的问题。6.3 生成代码风格被同事吐槽之后我做了什么第一版生成的代码Controller里每个方法都带一大段注释Service接口里还有日志输出同事看完直接说“这代码比我手写的还难看”。后来我把模板里的注释统一精简成三行类注释一句话、方法注释一句话、字段注释一句话。生成出来的代码干净了很多。另一个被吐槽的点是生成器自动生成了一堆没用的import。比如BigDecimal类型没有用到但模板里的import列表写了它。这个问题很好解决——我在渲染完成后增加了一步“清理无效import”的后处理简单写一个正则删除未使用的import行或者直接用IDE的格式化插件。其实最省事的方案是不在模板里写死import列表而是在modelEnricher阶段根据实际用到的类型动态拼出import集合这样每个文件的import列表都是精确的。7. 模板驱动方案的扩展方向有人可能会问这套生成器只能生成Java CRUD吗当然不是。模板驱动的本质是“结构相同、内容变化”的文本输出所以只要你能描述出目标代码的模板语法生成器就能输出。我后来在同样的框架上扩展了三种用法。第一种是生成前端代码。给同一个TableModel配一套Vue模板就能生成列表页、新增弹窗、删除逻辑和API调用方法。只不过前端模板的复杂度更高因为要处理表单校验、弹窗状态、路由跳转这些逻辑但大方向是一样的。第二种是生成数据库初始化脚本。把表结构信息渲染成CREATE TABLE语句团队里每个人本地建库时不再需要手动执行一份SQL迁移脚本直接跑一下生成器全流程自动完成。第三种是生成接口文档的骨架。把字段注释和类型信息渲染成Markdown格式的接口文档给前端同学看省去手写文档的时间。这就是模板驱动方案的价值——它不绑定一个特定场景而是给你一种“把规范固化到模板里”的思维方式。以后团队里任何新项目只要遵循这套规范生成的代码就能直接跑起来规范和落地之间的鸿沟被填平了。8. 我踩过几次坑之后的经验谈回到标题那句话“模板驱动一键生成分层代码”。一键生成从来不是核心卖点模板驱动才是。整个过程里最花时间的不是写模板也不是调Velocity而是想清楚“哪些内容是可变的哪些内容是固定的”。这个边界一旦划清楚了生成器的代码可以精简到几百行维护成本极低。我个人建议每个中等以上规模的Java团队都值得花两三天做一次这种自建生成器。不为别的就为把代码风格统一这件事从“靠人自觉”变成“靠工具保证”。我自己最大的体会是生成器写完之后团队新人上手老项目的速度明显加快因为代码结构都是同一套看一个模块等于看所有模块。如果后续要扩展我建议你优先把AST增量合并做扎实这是生成器从“一次性工具”升级为“长期伴随工具”的关键一步。再往下走可以结合接口文档、数据库版本管理、前端页面生成慢慢形成一个微型的低代码底座。这条路没有想象的那么难核心永远在那个几百行代码的解析器、那个模板文件夹、那套清晰的模型定义里。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Jenkins离线部署实战:构建可信软件供应链 2026/10/1 5:40:23

Jenkins离线部署实战:构建可信软件供应链

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Realtek PCIe GBE Family Controller 驱动安装与断流排查全攻略 2026/10/1 5:40:23

Realtek PCIe GBE Family Controller 驱动安装与断流排查全攻略

简介:这是面向 Windows 7 系统的 Realtek PCIe GBE Family Controller 网卡驱动官方安装包,适合 32 位与 64 位 Win7 用户解决设备管理器中网卡无法识别、网络无法连接等问题。压缩包共 197 个文件,大小约 5.61MB,除核心的 sys 驱…

阅读更多 →
HarmonyOS 7游戏快启实战:GAK内存镜像与预启动技术解析 2026/10/1 5:40:23

HarmonyOS 7游戏快启实战:GAK内存镜像与预启动技术解析

1. 这不是“优化”,是重新定义游戏启动体验HarmonyOS 7 游戏快启实战——这个标题里藏着三个被绝大多数开发者忽略的关键信号:Graphics Accelerate Kit(GAK)、内存镜像、预启动。它不是在讲“怎么让App启动快一点”,而…

阅读更多 →
HarmonyOS 7游戏秒启:GAK内存镜像与预启动协同优化 2026/10/1 5:40:23

HarmonyOS 7游戏秒启:GAK内存镜像与预启动协同优化

1. 这不是“加载动画优化”,而是HarmonyOS 7游戏启动逻辑的底层重写你有没有试过点开一个中型3A级手游,手机屏幕先黑一下,接着弹出“资源加载中… 32%”,然后卡在68%不动,最后才跳进主界面?这种体验在Harmo…

阅读更多 →
从回形针最大化器看AI目标错位:原理、案例与工程防范 2026/10/1 5:40:23

从回形针最大化器看AI目标错位:原理、案例与工程防范

如果把AI安全领域最出圈的几个概念排个名,“回形针最大化器”(Paperclip Maximizer)绝对稳居前三。这个由瑞典哲学家尼克博斯特罗姆(Nick Bostrom)在2003年提出的思想实验,用一根毫不起眼的回形针&#xff…

阅读更多 →
Java Web图书管理系统:MySQL 8.0+适配与Servlet全流程实战 2026/10/1 5:40:16

Java Web图书管理系统:MySQL 8.0+适配与Servlet全流程实战

简介:这是一份面向计算机专业本科生的Java课程设计与期末大作业实战资源,基于B/S架构实现功能完备的图书管理系统,帮助学习者掌握JDBC数据库连接、Servlet前后端交互、JSP页面渲染及MySQL数据管理等核心技能。资源包共93个文件,含…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉