ARTICLE DETAIL

资讯详情

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

eladmin代码生成器原理拆解:从数据库元数据到模板渲染

eladmin代码生成器原理拆解:从数据库元数据到模板渲染 先说说我为什么想写这篇。最近技术群里聊eladmin的人不少大家用它的代码生成器用得飞起——同步一张表、填好包名、点一下生成后端实体、DTO、查询条件、Controller连前端的Vue页面都齐活了。但一问到“它到底是怎么跑通的”能讲清楚的人就少了。我自己是把这个生成器的源码反复读了几遍还在团队里做过定制模板踩过一些坑才算是把它的工作原理摸透。这篇文章就把我理解的东西完整拆给你看适合两类人一是只想把功能用明白的二是打算在它基础上做二次开发的。1. 生成器要解决的问题每个后台工程都逃不掉的重复劳动先从一个最朴素的问题切入为什么要搞代码生成器1.1 一个CRUD模块需要手工交付多少东西假设业务上要新增一张学生表字段有学号、姓名、年龄、入学时间、备注。按eladmin自己的工程规范你需要写下面这一串文件Entity实体类字段映射、JPA注解、表名关联DTO和前端交互的数据载体加上参数校验注解QueryCriteria封装查询条件的专用类Service接口和ServiceImpl实现分页查询、新增、修改、删除Controller暴露REST接口加上权限注解和操作日志注解前端index.vue列表页、查询表单、新增编辑弹窗、分页组件。这还没算Vo、Mapper这类可能需要的类。实际操作里实体类的每个字段要对着表结构手敲DTO的校验注解要对照字段是否为空挨个加Controller里那套PreAuthorize、Log、Validated的固定写法更是在复制粘贴时最容易出错的地方。一个表就是几百行高度雷同的代码。业务系统动辄几十张上百张表累计下来的工作量非常可观。更要命的是人写重复代码会累一累就容易出低级错误——字段名拼错、注解漏加、类型映射不对。这类错误编译期可能还发现不了发布上线前才在测试环境里冒出来定位起来特别费劲。1.2 为什么eladmin选择“内嵌Web生成器”而不是命令行工具业界常见的代码生成器有两类一类是Maven插件或者命令行工具比如MyBatis Generator需要配置环境、写配置XML另一类是网页版的在线生成。eladmin选的是后者把它做成后台管理系统里的一个功能菜单。这个选择在我看来非常务实。项目里常有刚上手的新同学他们未必理解Spring Boot整个工程结构但只需要会填表单——选择数据表、填包名、点生成——就能产出一套格式统一的代码。团队代码风格的一致性靠这种工具比靠Code Review约束高效得多。而且生成器跑在后台系统里所有配置都在界面上完成对前端经验不足的成员也很友好。1.3 一个需要提前说清的定位生成的是脚手架不是完整业务这里先泼一盆冷水eladmin生成器产出的代码定位是“可用模板”不是“完整业务成品”。它能帮你把CRUD骨架、实体映射、基础校验这些完全不涉及业务逻辑的部分一次性铺好但跨表关联查询、复杂权限判断、特定的状态流转这些还是得自己补。很多使用者期望生成完直接就能上线这个期待不太切实际。理解这层定位后面用起来才不容易失望也知道该在哪些地方投入人工。2. 完整链路拆解从数据库表到Java文件中间发生了什么接下来进入正题。我用一个按时间顺序的流程来描述整个生成过程。2.1 第一步同步表结构把数据库元数据读进来打开“代码生成”菜单第一步是“同步数据表”。这一步背后干的活是把MySQL的information_schema库里的元数据读出来转成项目自己能管理的记录。具体来说它会执行类似这样的查询拿到所有用户表的名称、引擎、字符集、注释、创建时间SELECT table_name, engine, table_collation, table_comment, create_time FROM information_schema.TABLES WHERE table_schema DATABASE()再对单张表查它的列信息SELECT column_name, data_type, column_comment, is_nullable, column_key, column_default, character_maximum_length FROM information_schema.COLUMNS WHERE table_schema DATABASE() AND table_name student ORDER BY ordinal_position这里有个很多人忽略的点为什么不直接连业务库执行DESC因为information_schema提供了更完整的元数据——列注释、主键标记、是否可空、字符集、默认值全都有。这些信息是后面生成实体注释、校验注解、主键注解的基础。同步完这些元数据会落到项目自己的数据库表里。为什么不每次生成时现查因为用户在界面上对每一列做的配置需要持久保存——比如“这一列要不要出现在列表里”“查询方式用什么”。每次同步时再和information_schema做对比新列加进来、删掉的列清掉、类型或注释变了的更新但用户自定义的配置要保留。这是一个非常典型且合理的设计取舍。2.2 第二步配置生成参数同步完表界面会列出所有列。每一列都能配置是否作为列表字段展示是否出现在新增/编辑表单里查询方式等值、模糊、区间、不等于、非空是否允许修改。同时还需要填一组全局生成配置包名、模块名、作者、代码输出路径、是否覆盖已有文件。在eladmin的实现里这组配置对应GenConfig实体每次生成时从数据库读出来。这一步看起来只是填几个文本框但它是整个生成器设计里比较巧妙的地方——把“数据库的表结构”客观元数据和“业务上怎么用这些字段”主观配置做了一个拆分。表结构是死的怎么用是活的两者分开才能灵活适配不同业务场景。2.3 第三步触发生成模板渲染然后写文件点击生成按钮后后端做了三件事从配置表里取出GenConfig、TableInfo和该表所有ColumnInfo把这些数据组装成一个Map作为模板渲染的数据模型遍历所有模板文件用Velocity引擎逐个渲染渲染出来的字符串写到对应路径的.java或.vue文件里。文件路径由“包名 模块名 类名”拼出来。比如配置了包名com.example.xxx、模块名student那么实体类就生成到com/example/xxx/student/entity/Student.java。前端文件出现在配置的代码路径下的对应目录。也可以选择直接下载zip包把生成结果打包带走。2.4 核心类分工速览如果你打算自己翻源码重点关注这几个类类职责Generator核心工具类负责模板加载、数据组装、渲染输出GenConfig用户配置包名、模块名、作者、路径等TableInfo表的信息表名、注释、引擎、字符集ColumnInfo列的信息列名、类型、注释、主键、可空、查询方式GenUtil类型转换、驼峰命名等纯工具方法把类的关系理清楚后面看代码会顺手很多。重点提醒不同版本类名可能略有差异但职责划分基本一致。3. 元数据模型是生成器的心脏类型映射与命名转换我一直觉得代码生成器最考验功力的不在模板引擎而在元数据模型和类型映射。模板是死的类型映射要是做不好生成出来的代码根本编译不过。3.1 MySQL类型到Java类型的映射数据库类型五花八门Java类型就那么十几个中间必须有一套稳定的映射规则。以最常见的情况为例eladmin的映射大致是这样的MySQL数据类型Java类型说明varchar、charString最常见直接映射int、integerInteger配合Column注解使用bigintLong主键、外键最常见tinyintInteger或Boolean这里容易有歧义下面细讲decimal、numericBigDecimal金额场景必备别用Doubledatetime、timestamp、dateDate配合JSON格式化注解text、longtextString通常会加Lob或特殊类型声明bitBoolean0/1语义blobbyte[]二进制场景比较少用映射规则的实现通常是基于DATA_TYPE字符串的匹配判断。这里就有一个隐藏的坑MySQL的tinyint(1)和tinyint(4)在information_schema里拿到的DATA_TYPE都是tinyint没法只靠类型区分业务语义。所以生成器一般统一映射到Integer如果业务需要布尔字段手动改实体即可。别指望生成器替你猜业务意图。3.2 字段名到Java属性的命名转换数据库列名是snake_caseJava属性是camelCase。核心逻辑不复杂按下划线分割首段保持原样其余段首字母大写public static String toCamelCase(String columnName) { if (columnName null || columnName.trim().isEmpty()) { return ; } StringBuilder sb new StringBuilder(); boolean upperCase false; for (char c : columnName.toCharArray()) { if (c _) { upperCase true; } else if (upperCase) { sb.append(Character.toUpperCase(c)); upperCase false; } else { sb.append(c); } } return sb.toString(); }最常见的两个规则是student_name转换为studentNameorder_time转换为orderTime。真正麻烦的是字段名里带数字、其他分隔符或者原始列名用了全大写的缩写。比如user_id没问题但itemID这种列名如果表设计得比较随意转换结果会很不自然。所以成熟的生成器都会在同步后让你人工确认一遍字段名——这也是列配置要存库的原因之一给人工修正留入口。3.3 注释、主键、可空性如何变成代码数据库里每列注释在生成实体时会变成字段上的ApiModelProperty注解或Javadoc主键列会生成Id和GeneratedValue(strategy GenerationType.IDENTITY)数值字段能根据DATA_TYPE拼出precision和scale信息。可空性则主要影响DTO的校验注解字段不允许为空就在DTO对应属性上加NotBlank或者NotNull。这个逻辑看着简单但特别容易踩坑——默认值。举个例子一个字段在数据库里设置了DEFAULT NULL但逻辑上存的是“操作时间”IS_NULLABLENO生成器会给DTO加NotNull。可这个字段可能只在特定业务场景下才赋值其他场景前端根本不传。结果生成完的接口一调用就报校验错误排查半天才发现是生成器根据可空性加的注解太“死板”。遇到这种情况正确做法是同步后在配置里关掉该字段的表单展示或者手动去掉校验注解而不是指望生成器智能识别。3.4 列配置那几项开关分别控制什么eladmin生成器里对每一列通常有四个维度的控制列表展示控制前端表格el-table-column是否渲染表单展示控制新增/编辑弹窗里el-form-item是否生成查询方式决定查询表单里出现什么控件以及后端查询条件的写法可修改对应实体属性Column(changeable ...)影响更新时是否纳入判断。这四个开关组合起来基本覆盖了一张表在前端页面里的所有展示需求。注意一点主键列在表单里一般是要展示的编辑时需要知道在改哪条但“可修改”必须设成false不然更新操作会把主键也尝试写进去容易出问题。4. Velocity模板引擎代码是渲染出来的不是拼接出来的我曾经问过面试的候选人生成器是用字符串拼接生成代码还是用模板答字符串拼接的基本可以判断没写过类似的工具。eladmin用的是Apache Velocity模板引擎把静态代码骨架和动态变量做了很好的分离。4.1 为什么选Velocity而不是手写StringBuilder用模板引擎有三个直接好处代码骨架和变量分离.vm文件里全是Java代码的正常写法只有需要变化的地方是变量读起来一目了然改模板不用动主流程想调整生成出来的代码风格直接改.vm文件重启即可不用动Java类团队协作友好后端开发不一定精通Velocity语法但模板文件里大部分内容是普通Java代码照着样子改很容易上手。和另一个常用的FreeMarker相比Velocity更轻学习成本更低模板文件里#foreach、#if这些指令足够覆盖CRUD生成的所有场景。4.2 一次渲染的完整流程以生成一个Entity文件为例流程是这样的// 1. 初始化Velocity引擎 VelocityInitializer.initVelocity(); // 2. 从classpath加载模板 Template tpl Velocity.getTemplate( template/generator/back/Entity.java.vm, Constants.UTF8 ); // 3. 组装数据模型 MapString, Object data new HashMap(); data.put(config, genConfig); data.put(tableInfo, tableInfo); data.put(columns, columnConfigList); // 4. 渲染 StringWriter writer new StringWriter(); tpl.merge(data, writer); // 5. 写文件 Path target Paths.get(basePath, packagePath, moduleName, entity, className .java); Files.write(target, writer.toString().getBytes(StandardCharsets.UTF_8));注意细节文件写入用UTF-8避免Windows默认编码导致中文乱码模板渲染前要做一次Velocity语法解析模板本身有语法错误的话这一步会直接抛异常。4.3 一个简化的Entity模板示例为了直观感受.vm长什么样给一个精简版package ${config.packageName}.${config.moduleName}.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.Date; Entity Getter Setter Table(name ${tableInfo.tableName}) public class ${tableInfo.className} implements Serializable { Id Column(name id) GeneratedValue(strategy GenerationType.IDENTITY) private Long id; #foreach($column in $columns) #if($column.columnName ! id) /** ${column.columnComment} */ Column(name ${column.columnName}) #if($column.notNull true) NotNull #end private ${column.javaType} ${column.javaField}; #end #end }真实的eladmin模板比这复杂加了ApiModelProperty、时间字段的JsonFormat、BigDecimal的精度映射、changeable处理等。但核心结构就是这样#foreach遍历列#if处理分支${}引用数据模型里的变量。4.4 一个重要设计后端和前端模板相互独立eladmin的模板分两个目录back/放后端模板front/放前端模板。生成时可以只选后端也可以连带前端一起生成。这种独立性对团队协作很有用——有人只想要后端接口有人需要整套页面互不干扰。做二次开发的时候这种分离也让改动范围更可控。5. 前端Vue代码的生成逻辑CRUD页面是怎么拼出来的后端代码的生成逻辑相对直白前端反而有些门道在里面。我第一次看生成的index.vue时觉得挺神奇不同表生成的页面查询条件、表单字段、表格列居然都能对应得上。5.1 统一走crudMixin模板只负责配置声明eladmin生成的Vue页面并没有把CRUD逻辑全部写死在模板里而是统一引用项目里的crudmixin。mixin提供了分页查询、新增、修改、删除、导出这些通用方法生成的页面只需要把每个字段的展示方式声明出来show: true对应列表展示开关type字段对应控件类型。模板和运行时逻辑解耦想改交互细节直接在mixin里改不用重新生成代码。export default { mixins: [crud], data() { return { crud: { method: POST, url: /api/students, columns: [ { key: name, label: 姓名, show: true, type: input }, { key: age, label: 年龄, show: true, type: number } ] } } } }5.2 查询方式是怎么影响控件的这是前端生成逻辑里最有意思的部分。查询方式有几种每种在模板里生成不同的控件组合等值()生成一个普通输入框提交参数走后端精确匹配模糊(like)输入框不变但后端查询时会在QueryCriteria对应字段生成like的查询条件区间(between)生成一个日期范围选择器或两个输入框提交时带上起始和结束两个参数不等于(!)后端生成notEqual判断非空(notNull)不生成任何控件但后端查询条件固定是字段IS NOT NULL。这套逻辑在QueryCriteria模板里用#if判断逐一渲染。如果你翻过生成出来的xxxQueryCriteria类会发现它针对不同查询方式生成不同的条件判断块配合JPA的Specification把条件拼到查询里。这是eladmin代码风格里比较有辨识度的一部分也是生成器灵活性的集中体现。5.3 表单字段的必填标记与控件类型前端表单的rules校验规则也来源于列的元数据。IS_NULLABLENO的字段模板会在rules里加required: true字段类型是日期控件用el-date-picker并配上格式数字字段用el-input-number。这里有个针对性建议生成代码前把每列的类型和可空性在数据库层面整理好注释写规范前端页面生成出来基本不用大改。反过来如果数据库注释全是“temp”“flag1”这种生成的页面全是无名列改起来比从零写还痛苦。6. 实际使用中的坑我踩过、也看别人踩过的这一节的内容基本都是真金白银换来的经验。生成器用起来门槛不高但一到生产环境各种细节问题就冒出来了。6.1 时间字段的序列化问题数据库datetime类型生成后是java.util.Date如果DTO里没有配置JsonFormatSpring返回JSON时默认格式是时间戳前端显示成一串数字。eladmin的模板通常会加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)但如果你改过模板或者用的是旧版本很容易漏掉。检查生成的DTO凡是Date类型都要确认有这个注解。这个坑特别隐蔽因为后端接口本地测试时看着正常前端展示出来就是一堆数字。6.2 tinyint和Boolean的语义陷阱前面说过tinyint默认映射成Integer。但很多业务表喜欢用tinyint(1)表示布尔比如is_delete。生成下来是Integer字段前端表格里显示0和1用户根本看不懂。两种处理方式一是在表设计时直接用bit类型二是生成后手动把实体字段改成BooleanDTO和Vue模板同步调整。别指望生成器能区分这两种语义它只认DATA_TYPE。6.3 保留字作为列名列名叫order、desc、group这些MySQL保留字生成SQL和实体映射时都会出问题。如果表已经建好了没法改得在实体映射里用Column(name \order)手动转义或者在后端查询条件里特殊处理。最省事的方式还是建表时规避保留字这是我在代码评审里反复提的一条。6.4 生成路径和覆盖策略eladmin的“代码路径”配置决定文件写到哪。开发环境用IDE跑没问题但如果打成jar包部署在生产环境相对路径可能没有写权限生成就会失败。建议代码路径配置成绝对路径比如/data/workspace/这种部署机器上真实存在的目录。“是否覆盖”这个配置更要慎用。如果之前生成过代码、手工改过重新生成时开覆盖会把你的改动直接冲掉。工业上的稳妥做法是只生成新文件遇到同名文件时提示冲突但eladmin的做法是按配置决定。我的习惯是生产环境的表结构变动很频繁时把覆盖选项关掉改动通过手动合并进去避免一个大意让整段业务逻辑被模板覆盖掉。6.5 列注释里的特殊字符模板渲染时列注释会被拼进ApiModelProperty的value里。如果注释里有双引号、反斜杠、换行符生成的代码就是语法错误。最典型的是注释里写了英文引号或者从Excel复制过来的特殊符号。解决方法表设计阶段就不要在注释里用引号或者在生成前把那列的注释清干净。这个问题在团队协作里特别常见因为DBA建表时不会考虑下游代码生成的问题。6.6 再次同步会不会把自定义配置冲掉这是被问得最多的问题。设计得好的同步逻辑是增量式的从information_schema读最新的列信息和库里已有的列配置做对比。列还在就更新类型、注释这些“客观属性”保留查询方式、展示开关这些“主观配置”新增列按默认规则插进去删掉的列移除。所以放心同步不会把前面的定制冲掉——但前提是你用的版本没有同步逻辑的bug升级版本后最好先找个小表验证一次。7. 基于生成器做二次开发定制模板的几个实际思路聊完原理和坑最后说说怎么把它扩展成你自己的生产力工具。毕竟开箱即用的生成器只能覆盖通用场景真正贴合团队规范的生成器都得动几刀。7.1 找到并修改模板文件模板文件在项目src/main/resources/template/generator/下。想改生成代码的风格直接改这里。举个例子团队要求所有Controller都要继承一个BaseController并且在类上统一加Swagger的分组注解就在Controller.java.vm里对应的位置加上固定代码即可。改模板前先备份原文件强烈建议用一个测试表跑一遍完整生成把生成的代码编译过一遍再推进。模板的语法错误往往不会在你修改时立刻暴露要跑到渲染那一步才知道所以验证环节不能省。7.2 给数据模型加字段扩展模板能力有时候模板需要用到现成数据模型里没有的信息比如表所属的业务分组、接口排序号。做法是在Generator组装Map时追加自定义数据。比如想给生成的Swagger注解带上排序ApiSort(${sortNo})模板变量是活的数据模型加什么模板就能引用什么。前端模板同理想生成某些自定义组件就把对应的控件类型通过配置传进去。7.3 保持生成结果的可编译性这是给所有做二次开发的人的一句忠告。生成器是批量产出一旦模板改出语法问题影响的是所有后续生成的类。所以每次改动模板都要在本地做“生成→编译→修复→再生成”的闭环验证。建议把编译命令做成模板改完后的第一道检查关卡别把错误模板带到团队里去。我在团队里推行过一次模板改动因为偷懒跳过了编译验证结果一晚上产出了几十个编译不过的类后面的教训很深刻。7.4 一次完整的定制示例字段注释自动转成Swagger说明举一个实际场景。数据库里某列注释是“学生状态0-未入学1-在读2-毕业”默认模板只生成ApiModelProperty(学生状态0-未入学1-在读2-毕业)。但团队规范要求Swagger说明里带上枚举值和含义的映射所以我们在ColumnInfo里加一个枚举映射字段模板改成#if($column.enumMap) ApiModelProperty(${column.columnComment}【${column.enumMap}】) #else ApiModelProperty(${column.columnComment}) #end这样前端展示层可以从生成的注释里自动解析出下拉选项的文案后端文档也更友好。这类定制不需要动任何Java代码纯模板改动就能完成。结尾代码生成器的本质是用一套元数据模型加一组模板把数据库结构转化成符合工程规范的代码。eladmin的实现虽然不算复杂但设计里处处是工程实践的沉淀元数据持久化让配置可修改模板引擎让代码风格可定制前端mixin让生成的页面逻辑可复用。在实际使用中我的建议是先别急着改模板。用默认模板跑通流程理解每一层生成出来的代码是干什么的然后挑一个自己项目里最典型的表手工改一遍生成结果把差异整理成模板修改清单最后再动模板。顺序反了很容易在模板的语法里迷失方向。对我个人来说把生成器原理摸透之后最大的收获不是省下了多少敲代码的时间而是整个团队的CRUD代码风格从此有了基准线。新来的同事照着生成的模板扩展写出来的东西跟老代码几乎一个味道。这可能是代码生成器最容易被忽略、但也最值钱的价值。
返回列表