EasyCode 与 Velocity:从数据库表到可维护代码模板的工程化实践
EasyCode 将 IntelliJ IDEA 的数据库元数据转换为可供模板访问的表、字段、类型等对象,再通过 Velocity 模板生成 Entity、DAO、Service、Controller 等代码。真正决定代码生成质量的并不是“右键生成”本身,而是类型映射、命名规则、模板上下文、文件路径和团队代码规范。本文从 EasyCode 的工作机制出发,系统梳理 Velocity 模板语法、模板变量、多模板组织、对象分层、调试方式、版本兼容问题以及团队级代码生成实践。
为什么需要 EasyCode
一个典型的 Java 后端项目中,大量代码具有明显的结构重复性。
例如数据库里存在一张:
1 | |
那么围绕它经常会出现:
1 | |
这些代码并不是完全相同,但通常具有高度稳定的结构:
- 类名来自数据库表名;
- 字段来自数据库列;
- Java 类型由数据库类型映射而来;
- Mapper、Repository、Service 等类名围绕实体名称组合;
- package、import、注释、接口继承关系基本固定;
- CRUD 方法通常具有稳定格式。
如果每增加一张表都手工复制这些代码,不但浪费时间,还非常容易产生复制错误。
例如:
1 | |
复制成:
1 | |
但内部方法参数、Mapper 注入、泛型或者注释忘记修改,这就是典型的机械劳动错误。
EasyCode 的目标就是把这些可以根据数据库元数据确定的代码结构模板化。
官方项目将 EasyCode 定义为基于 IntelliJ IDEA Database Tool 的代码生成插件,通过自定义模板生成代码,常见目标包括 Entity、DAO、Service、Controller,也支持同时处理多张表、多套模板、自定义类型映射、附加列、列属性和分组配置。
从工程角度看,它解决的并不是:
如何替程序员设计业务代码?
而是:
如何把项目中重复、稳定并且可以从元数据推导出来的代码结构自动生成?
这两件事情的边界非常重要。
EasyCode 的整体工作原理
EasyCode 可以理解为一个非常典型的:
1 | |
系统。
其核心流程可以抽象成:
flowchart LR
A["数据库 Schema"] --> B["IntelliJ Database Tool"]
B --> C["表 / 字段元数据"]
C --> D["EasyCode 元数据模型"]
D --> E["类型映射"]
D --> F["附加列 / 附加属性"]
D --> G["命名规则"]
E --> H["Velocity Context"]
F --> H
G --> H
H --> I["Velocity 模板"]
J["全局变量 / 宏"] --> I
I --> K["模板渲染"]
K --> L["文件名"]
K --> M["保存目录"]
K --> N["Package"]
L --> O["Entity"]
M --> O
N --> O
K --> P["Mapper / DAO"]
K --> Q["Service"]
K --> R["Controller"]
K --> S["XML / DTO / VO 等"]
EasyCode 本身负责从 IntelliJ Database Tool 获取数据库结构,并把这些信息组织成模板可以访问的对象;Velocity 负责执行模板中的变量替换、条件判断、循环和宏。
因此使用 EasyCode 时实际上存在三套需要区分的概念:
1 | |
很多 EasyCode 模板看起来像下面这样:
1 | |
其中:
1 | |
属于 Velocity。
而:
1 | |
则属于 EasyCode 放进 Velocity Context 的对象。
这个区别非常关键。
例如:
1 | |
并不是 Velocity 内置函数,而是 EasyCode 暴露给模板使用的工具对象方法。
同样,一些旧 EasyCode 模板中可以看到:
1 | |
其中 $define 通常来自 EasyCode 的全局变量配置,而类似 #save 的内容可能由该全局变量中的 Velocity Macro 定义,并不是 Velocity 官方语言天然提供的指令。EasyCode 官方 Wiki 也明确把“全局变量”作为独立能力,并说明其常用于定义 Velocity 宏或者保存重复模板代码。
理解这一层以后,EasyCode 模板就不会再像一种“神秘 DSL”。
它实际上只是:
1 | |
安装与基本使用流程
EasyCode 官方仓库中的历史使用说明以 IntelliJ IDEA Ultimate 和 Database Tool 为基础,因为插件本身依赖 IntelliJ 的数据库能力。仓库 README 仍保留着 IntelliJ IDEA Ultimate(172+) 的历史说明,并指出支持范围基本跟随 Database Tool。当前 IDE 版本是否兼容则应以实际插件市场和当前 IDE 插件依赖为准。
安装插件
最直接的方式是在 IntelliJ IDEA 中进入插件市场,搜索:
1 | |
安装完成以后根据 IDE 提示重启。
官方仓库还保留了离线 ZIP 安装方式:安装 ZIP 时直接选择插件包,不需要提前解压。官方说明同时强调,插件升级不会主动覆盖用户已有模板,这一点在团队维护自定义模板时尤其重要。
不同 IntelliJ IDEA 版本的设置菜单已经发生过多次变化,因此旧教程里的:
1 | |
不应该被理解为永久不变的菜单结构。
更稳妥的方法是在:
1 | |
中直接搜索:
1 | |
配置数据库
EasyCode 的数据来源不是 Java Entity,而是 IntelliJ IDEA Database Tool 中已经连接的数据源。
流程大致是:
1 | |
官方 Wiki 将“添加数据源”“生成代码”“添加类型映射”“自定义模板”等分别作为入门步骤,而官方项目本身也明确说明 EasyCode 建立在 Database Tool 之上。
配置完成后,在 Database 窗口选择一张或多张表,通过 EasyCode 的生成入口选择:
- Module;
- Package;
- 输出目录;
- Template Group;
- 需要执行的模板。
然后生成代码。
EasyCode 支持一次选择多张数据库表,也支持一次执行多个模板,因此没有必要把 Entity、Service、Controller 等全部塞进一个巨型模板。
EasyCode 真正核心的东西:模板上下文
学习 EasyCode 最重要的不是记菜单,而是理解:
模板执行时到底有哪些对象?
在常见 EasyCode 模板中,核心角色可以整理为:
| 对象 | 含义 | 常见用途 |
|---|---|---|
$tableInfo |
当前数据库表对应的模板模型 | 表名、注释、字段、主键、保存路径 |
$column |
当前字段模型 | 字段名、数据库列名、Java 类型、注释 |
$tool |
EasyCode 工具对象 | 字符串拼接、名称转换、类型处理 |
$callback |
输出控制对象 | 设置生成文件名、保存路径 |
$author |
配置中的作者信息 | 代码注释 |
$time |
时间辅助对象 | 生成时间 |
| 全局变量 | 用户自定义公共内容 | 宏、公共代码、共享配置 |
实际可用属性和方法可能随 EasyCode 版本以及配置变化,因此编写复杂模板时应结合插件的模板调试功能查看当前对象,而不是只依赖多年以前的博客代码。EasyCode 官方 Wiki 本身专门提供了“常用的原始对象属性”和 Debug 章节。
$tableInfo
这是整个模板最重要的对象。
典型模板会访问:
1 | |
或者:
1 | |
例如:
1 | |
表示遍历当前表的全部字段。
一些 EasyCode 模板还会使用:
1 | |
处理主键字段,使用:
1 | |
处理非主键字段。类似属性在 EasyCode 的实际模板示例中长期存在。
$column
$column 通常来自:
1 | |
可以继续访问字段信息。
例如:
1 | |
通常用于生成 Java 属性名,而:
1 | |
在旧版模板中常被用来取得底层数据库列名称。
类型可以通过:
1 | |
取得,再结合 EasyCode Tool 获取短类名:
1 | |
例如数据库类型映射完成后:
1 | |
最终模板可能只需要:
1 | |
旧 EasyCode 模板正是通过类似方式生成实体字段。
$tool
$tool 可以理解为 EasyCode 为模板准备的 Helper。
资料中的典型调用包括:
1 | |
用于组合字符串。
例如概念上:
1 | |
得到:
1 | |
还经常看到:
1 | |
将:
1 | |
处理为:
1 | |
以及:
1 | |
将完整 Java 类型转换成适合代码中使用的短类名。
需要再次强调:
1 | |
不是 Velocity API。
它是 EasyCode API。
$callback
模板不仅需要决定“生成什么内容”,还必须告诉 EasyCode:
1 | |
这就是 $callback 的职责之一。
旧模板中经常出现:
1 | |
以及:
1 | |
因此一套模板实际上完成两件事:
1 | |
Velocity 基础语法
EasyCode 的模板能力很大程度上来自 Apache Velocity。
只要把 Velocity 的几个核心语法掌握,绝大多数 EasyCode 模板已经可以看懂。
Reference:变量引用
Velocity 使用:
1 | |
访问变量。
例如:
1 | |
访问属性时可以写:
1 | |
Velocity 官方文档将引用主要划分为 Variable、Property 和 Method。Property 形式在对象模型下会映射到相应 Getter 等方法,而 Method 则允许显式传递参数。
例如:
1 | |
本质上可能访问:
1 | |
而:
1 | |
则属于显式方法调用。
${}:Formal Reference
如果变量后面紧跟普通文本:
1 | |
Velocity 无法知道你希望访问的是:
1 | |
还是:
1 | |
因此应该使用:
1 | |
Apache Velocity 将这种写法称为 Formal Reference。
在代码生成中非常常见:
1 | |
$!:Quiet Reference
EasyCode 模板中大量出现:
1 | |
例如:
1 | |
这里的 ! 表示 Quiet Reference。
普通变量不存在时,Velocity 可能保留原引用或者暴露异常行为;Quiet Reference 则倾向于输出空字符串。
这在生成代码时很方便。
例如:
1 | |
如果数据库表没有 Comment,就不希望代码里出现:
1 | |
但是 $! 也存在一个工程上的副作用:
它会隐藏模板错误。
例如你误写:
1 | |
最终可能什么都没有。
因此调试模板时,一个很好用的办法是暂时减少 Quiet Reference 的使用,让未解析的变量尽早暴露出来。
#set:定义变量
Velocity 使用:
1 | |
定义变量。
例如:
1 | |
之后:
1 | |
就表示当前实体名称。
代码生成中非常建议使用局部变量,因为这能显著降低模板复杂度。
相比反复写:
1 | |
可以先写:
1 | |
后面直接:
1 | |
模板会更容易维护。
Velocity 1.7 中 #set 的一个隐藏坑
Velocity 1.7 官方文档特别说明:
如果 #set 右侧表达式计算结果是 null,旧版 Velocity 不一定会把原变量重新赋值成 null,它可能保留之前的值。
例如:
1 | |
假设:
1 | |
第二轮可能仍然残留第一次的值。
因此涉及可能返回空值的变量时,更稳妥的模板结构是:
1 | |
这个问题尤其值得注意,因为它看起来不像模板语法错误,而更像“为什么某个字段莫名其妙拿到了上一个字段的数据”。
字符串:单引号和双引号
Velocity 中:
1 | |
双引号字符串可以进行变量插值。
例如:
1 | |
而单引号:
1 | |
在 Velocity 1.7 默认语义下不会进行变量插值。
因此 EasyCode 模板里如果需要变量参与字符串拼接,通常应该使用:
1 | |
或者直接通过:
1 | |
完成。
#if:条件判断
基本语法:
1 | |
例如:
1 | |
Velocity 支持:
1 | |
例如:
1 | |
Velocity 1.7 对 Truth Value 的判断和现代 JavaScript、Python 等语言并不完全相同。官方文档的基本规则是 Boolean true 为真、null 和 Boolean false 为假,因此在模板里不要过分依赖“空字符串是不是 false”“空集合是不是 false”这种跨版本容易变化的隐式语义。
对于关键逻辑,更明确的判断通常更安全。
#foreach:字段生成的核心
实体类生成最常见的 Velocity 结构就是:
1 | |
假设表结构为:
1 | |
模板经过三次循环后,就可以形成类似:
1 | |
当然最终 Java 类型取决于 EasyCode 中配置的类型映射。
Apache Velocity 1.7 为 #foreach 提供:
1 | |
等循环状态。
例如生成 SQL 字段列表:
1 | |
目的就是避免最后多一个逗号。
$velocityHasNext 与 $foreach.hasNext
很多老 EasyCode 模板可以看到:
1 | |
这种写法来自更早期的 Velocity / 模板习惯。
当前更容易兼容 Apache Velocity 1.7 文档所描述循环模型的方式是:
1 | |
官方 EasyCode 仓库后续提交信息也曾明确处理过旧 $velocityHasNext 写法失效的问题。Velocity 1.7 官方文档则明确提供 $foreach.hasNext。
因此维护旧模板时,如果发现:
1 | |
或者:
1 | |
原样出现在生成文件里,应该优先检查这一处。
#macro:把公共模板逻辑抽出来
随着模板越来越复杂,复制代码的问题会从 Java 转移到 Velocity。
例如 Entity、DTO、VO 都需要生成字段注释。
与其复制三遍:
1 | |
可以抽成 Macro:
1 | |
使用:
1 | |
Velocity 的 #macro 就是 Velocimacro,用于创建可复用模板片段。
这也是 EasyCode 全局变量真正有价值的场景之一。
官方 Wiki 对全局变量的定位就包括:
1 | |
#include 与 #parse
Velocity 同时提供:
1 | |
和:
1 | |
它们的区别很容易混淆。
#include
更接近:
1 | |
被包含内容不会作为 Velocity 模板再次解释。
#parse
则会:
1 | |
因此如果公共文件里本身包含:
1 | |
通常应该使用:
1 | |
Apache Velocity 官方文档明确区分了两种行为,并且说明 #parse 存在递归深度限制。
注释
Velocity 单行注释:
1 | |
多行注释:
1 | |
这些注释不会出现在最终 Java 文件里。
因此应该区分:
1 | |
和:
1 | |
前者属于模板源码。
后者属于模板输出。
一个最小 EasyCode 实体模板
理解前面的模型以后,可以构造一个非常精简的模板。
例如:
1 | |
它实际上包含四个阶段。
1. 得到实体名称
1 | |
2. 确定输出文件
1 | |
3. 生成 package
1 | |
4. 遍历数据库字段
1 | |
最终 Velocity 做的事情其实很简单:
flowchart TD
A["tableInfo"] --> B["entityName"]
B --> C["计算文件名"]
A --> D["fullColumn"]
D --> E["Column 1"]
D --> F["Column 2"]
D --> G["Column N"]
E --> H["Java Field"]
F --> H
G --> H
C --> I["输出 Java 文件"]
H --> I
真正复杂的是:
1 | |
这些才是一套 EasyCode 配置最终能不能长期使用的关键。
类型映射:模板生成正确代码的第一道防线
数据库类型和 Java 类型并不是一一对应。
例如数据库可能存在:
1 | |
最终 Java 类型取决于:
- 数据库类型;
- JDBC Driver;
- 项目编码规范;
- ORM;
- EasyCode 类型映射规则。
EasyCode 官方明确支持自定义数据库类型到 Java 类型的映射,而且支持正则匹配。官方 Wiki 也将类型映射单独列为入门配置步骤。
因此推荐把整个代码生成过程理解为:
1 | |
而不是直接在模板里写:
1 | |
把几十种数据库类型全部硬编码进模板,会导致模板迅速失控。
正确的职责分离应该是:
1 | |
表名和字段名:不要让命名规则散落在每一个模板中
数据库经常存在表前缀:
1 | |
最终 Java 类可能希望得到:
1 | |
而不是:
1 | |
老 EasyCode 实践中可以看到通过全局模板逻辑判断:
1 | |
前缀,再改变生成类名的方式。
这种方式可以工作,但团队环境中应该注意:
命名规则属于全局生成规则,不应该在 Entity、Mapper、Service、Controller 四个模板里分别实现一遍。
否则很容易发生:
1 | |
于是编译器开始替你主持一场命名规则辩论赛。
更好的设计是:
1 | |
如果确实通过修改 $tableInfo 来实现去前缀,还需要测试同一轮生成中的其他模板是否依赖原始名称,因为共享模板对象一旦被修改,就可能产生连锁影响。
原始数据库名称和 Java 名称要区分
代码生成器通常同时面对两个名字:
数据库:
1 | |
Java:
1 | |
因此模板中不要默认:
1 | |
旧 EasyCode 模板经常分别使用类似:
1 | |
和:
1 | |
前者用于数据库原始列标识,后者用于 Java 属性名称。
例如生成 JPA:
1 | |
模板需要同时知道:
1 | |
如果这两个概念混在一起,最终 SQL、XML 或 ORM 映射迟早会出现问题。
主键是最容易被模板“假装支持”的地方
很多简单代码生成模板直接写:
1 | |
其潜台词其实是:
1 | |
如果数据库是:
1 | |
组成联合主键,那么:
1 | |
只取第一个字段就已经改变了数据模型语义。
因此模板至少应该明确回答几个问题:
1 | |
如果当前项目明确规定:
1 | |
那么模板假设单主键完全没有问题。
问题不是存在约束。
问题是:
模板偷偷假设存在约束,但项目规范里没人知道。
代码生成工具最危险的地方恰恰是它可以非常高效地批量生成错误代码。
不要把所有文件塞进一个 Velocity 模板
EasyCode 支持同时选择多个模板。
因此更合理的模板结构是:
1 | |
而不是:
1 | |
里面连续几百行:
1 | |
分离以后,每个模板都只负责:
1 | |
例如:
1 | |
这样做的收益不仅是可读性。
还包括:
- 某一层可以单独开启和关闭;
- 修改 Mapper 不影响 Entity;
- 不同项目可以复用部分模板;
- Git Diff 更清楚;
- 模板版本升级更容易;
- 测试失败时更容易定位。
使用全局变量解决跨模板重复
当多个模板都需要相同内容:
1 | |
不要在所有模板中复制。
EasyCode Wiki 中的 Global Config / 全局变量就是为这类需求准备的,而且官方文档明确建议全局变量可用于宏以及较大块的重复代码。
可以把整体结构设计成:
flowchart TD
A["Global Config"]
A --> B["公共 Macro"]
A --> C["公共变量"]
A --> D["命名约定"]
B --> E["Entity Template"]
B --> F["Mapper Template"]
B --> G["Service Template"]
B --> H["Controller Template"]
C --> E
C --> F
C --> G
C --> H
D --> E
D --> F
D --> G
D --> H
不过全局变量存在另一个风险:
名称污染。
官方 Wiki 特别提醒,全局变量名称不能和模板中的其他变量产生冲突,否则可能出现替换问题。
因此团队级模板最好给全局变量制定命名规则。
例如:
1 | |
不要创建过于泛化的:
1 | |
PO、DO、DTO、VO:先统一对象语义,再开始生成
代码生成器可以生成任何 Java 类。
但:
能生成,不代表应该全部根据数据库表生成。
这是使用 EasyCode 时非常容易忽略的一点。
围绕 Java 分层对象,常见术语包括:
| 名称 | 常见含义 | 主要作用 |
|---|---|---|
| POJO | Plain Old Java Object | 广义普通 Java 对象 |
| PO | Persistent Object | 持久化对象 |
| DO | Data Object / Domain Object | 含义依项目规范而异 |
| DTO | Data Transfer Object | 层间或接口数据传输 |
| VO | View Object / Value Object | 展示对象或领域值对象,需结合语境 |
| BO | Business Object | 业务对象 |
| DAO | Data Access Object | 数据访问组件,不是普通数据载体 |
这些名称更像架构和团队约定,而不是 Java 语言规定。尤其 DO 和 VO 在不同团队中的含义并不完全一致:DO 可能表示 Data Object,也可能在 DDD 上下文中表示 Domain Object;VO 也可能分别表示 View Object 或 Value Object,因此项目必须先建立自己的术语表。
EasyCode 最适合生成哪一种对象
EasyCode 的信息来源是:
1 | |
因此它天然最清楚的是:
1 | |
所以最容易可靠生成的是与持久化结构接近的对象:
1 | |
而 DTO:
1 | |
这里的:
1 | |
数据库很可能根本不存在。
VO:
1 | |
其中:
1 | |
可能来自关联查询,更不是一张 user 表可以推导出来的。
所以把数据库表:
1 | |
机械复制成:
1 | |
虽然几秒钟就能生成四套对象,却很可能只是把未来的维护工作批量制造出来。
更合理的边界是:
flowchart LR
DB["Database Schema"]
DB --> PO["PO / Entity"]
PO --> DAO["DAO / Mapper"]
DAO --> SERVICE["Service"]
API["API / Use Case"] --> DTO["DTO"]
SERVICE --> DOMAIN["Domain / BO"]
SERVICE --> VO["VO"]
DTO --> SERVICE
EasyCode 非常适合自动化左半边。
右半边是否生成,要取决于:
1 | |
而不能只看数据库 Schema。
EasyCode 也不负责运行时对象转换
EasyCode 做的是:
1 | |
而不是:
1 | |
例如:
1 | |
运行时可以使用手写转换器或者 MapStruct 等工具完成。
这个过程和 EasyCode 是两个不同阶段:
sequenceDiagram
participant DB as Database
participant EC as EasyCode
participant SRC as Source Code
participant APP as Running Application
DB->>EC: Schema Metadata
EC->>SRC: Generate Java Code
Note over EC,SRC: 开发/代码生成阶段
APP->>SRC: 加载编译后的类
APP->>APP: PO -> DTO / VO 映射
Note over APP: 程序运行阶段
不要因为 EasyCode 能生成 DTO、VO 模板,就把它误解成对象映射框架。
模板调试比反复 Generate 更重要
EasyCode 官方项目明确列出了动态模板调试能力,官方 Wiki 也单独提供 Debug 进阶章节;旧版实践资料同样建议通过模板设置中的调试能力查看对象和方法。
一套复杂模板不应该通过下面这种方式开发:
1 | |
更推荐:
1 | |
特别是当你不确定:
1 | |
到底有没有这个字段时,优先调试对象,而不是从十年前的博客里继续猜属性名。
给模板准备一组“测试表”
代码生成模板和普通代码一样,需要测试边界。
团队至少应该准备一些具有代表性的表。
普通业务表
1 | |
覆盖:
1 | |
没有字段 Comment 的表
测试:
1 | |
会不会产生异常注释。
特殊类型表
包含:
1 | |
验证类型映射。
单主键表
验证:
1 | |
联合主键表
用于暴露:
1 | |
这类隐藏假设。
带前缀的表
例如:
1 | |
测试命名规则。
最终就形成一套简单的:
1 | |
每次修改模板后,对这些表重新生成一次,通过 Git Diff 就可以发现模板是否意外影响其他类型的代码。
不要让 $! 把所有错误吃掉
假设模板写成:
1 | |
但实际上根本不存在:
1 | |
Quiet Reference 的结果可能只是:
1 | |
最终你看到:
1 | |
然后开始怀疑:
1 | |
实际上是模板变量写错了。
因此推荐:
开发阶段
关键变量适当使用:
1 | |
让错误明显暴露。
模板稳定以后
对真正允许为空的字段:
1 | |
再使用 Quiet Reference。
Quiet Reference 应该表达:
这个值允许不存在。
而不应该表达:
不管有没有错误都别告诉我。
JPA 模板中的版本问题
较早的 EasyCode JPA 示例经常生成:
1 | |
以及 Hibernate 相关注解。
这类模板的价值在于展示 EasyCode 如何根据:
1 | |
生成注解,但不应该直接视为适用于所有现代 Java 项目的最终模板。
代码生成模板实际上属于你的技术栈资产。
项目如果发生:
1 | |
模板也必须升级。
否则很容易出现:
1 | |
然后每次生成代码都需要手工修正。
这种代码生成工具如果长期不维护,就会从:
1 | |
逐渐进化成:
1 | |
EasyCode 本身的版本也要和旧教程区分
官方仓库当前可检索的 build.gradle 中版本信息为:
1 | |
该版本信息对应 2024 年 11 月的更新;构建配置使用 Java 11,并依赖 IntelliJ Java 与 Database 插件。值得注意的是,仓库中仍能看到:
1 | |
这样的注释历史配置,但它已经是注释状态,因此不能仅凭这行代码断言当前运行时一定直接依赖该 Maven Artifact。
这解释了为什么阅读 EasyCode 历史资料时最好采用下面的思路:
1 | |
而不是:
1 | |
插件升级为什么可能没有修复你的模板
EasyCode 官方安装说明明确提到:
插件版本更新不会覆盖已有模板。
这其实是一个合理设计。
否则开发团队辛苦维护的模板:
1 | |
升级插件以后突然全部恢复默认,恐怕比生成失败更刺激。
但它意味着:
1 | |
和:
1 | |
实际上是两个独立的版本。
例如:
1 | |
并不代表:
1 | |
这样的历史模板语法已经自动迁移。
因此团队最好明确记录:
1 | |
三者的关系。
把 EasyCode 模板当成真正的源代码
个人使用 EasyCode 时,经常是:
1 | |
团队使用则完全不够。
模板实际上决定了:
1 | |
最终生成什么代码。
它的影响范围可能比普通 Java 类大几十倍。
因此模板应该具备与源代码相似的治理方式:
1 | |
而不是:
1 | |
一个推荐的团队模板仓库结构
可以在项目或独立模板仓库中维护:
1 | |
这里的重点不是目录名字,而是把:
1 | |
从个人 IDE 状态提升为:
1 | |
EasyCode 官方 Wiki 本身也包含配置导出、多设备同步、统一配置以及版本控制同步表配置等主题,说明当模板从个人使用走向团队后,配置管理本身就会成为代码生成体系的一部分。
模板不要生成太多“有意见的代码”
代码生成应该优先生成:
1 | |
的内容。
例如:
1 | |
非常稳定。
而下面这种业务逻辑:
1 | |
显然不适合根据数据库自动生成。
一个健康的生成边界通常是:
flowchart TB
A["适合自动生成"]
A --> A1["PO / Entity"]
A --> A2["Mapper / DAO"]
A --> A3["基础 Repository"]
A --> A4["基础 CRUD Skeleton"]
A --> A5["Mapper XML Skeleton"]
B["谨慎生成"]
B --> B1["DTO"]
B --> B2["VO"]
B --> B3["Controller"]
B --> B4["Service"]
C["不应依赖数据库模板生成"]
C --> C1["核心业务规则"]
C --> C2["领域行为"]
C --> C3["权限策略"]
C --> C4["事务边界"]
C --> C5["复杂校验"]
Service 和 Controller 不是绝对不能生成。
可以生成:
1 | |
或者基础 CRUD。
但模板越深入业务行为,后续重新生成的风险越大。
“重新生成”应该是模板设计时就考虑的问题
第一次生成代码最简单。
真正困难的是:
1 | |
例如:
1 | |
如果重新运行 EasyCode,而开发者已经在:
1 | |
写了 500 行业务代码,那么直接覆盖显然不可接受。
因此在决定模板结构之前,应该先确定生成策略。
策略一:一次性脚手架
生成:
1 | |
然后以后不再重新生成。
优点是简单。
缺点是数据库变化无法自动同步。
策略二:只持续生成纯数据层
例如:
1 | |
人工代码:
1 | |
独立维护。
这样可以反复生成数据层,而不覆盖业务逻辑。
策略三:生成 Base 类型
例如:
1 | |
EasyCode 只维护:
1 | |
人工逻辑写在:
1 | |
这是一种典型的:
1 | |
分离策略。
无论使用哪一种,原则都是:
不要让重新运行代码生成器拥有无条件覆盖手写业务逻辑的能力。
一个更完整的 EasyCode 生成体系
真正适合团队长期使用的 EasyCode,不应该只是一个模板。
它更接近一个小型 Code Generation Platform:
flowchart TD
DB["Database Schema"]
DB --> META["EasyCode Metadata"]
CONFIG["Generation Config"]
CONFIG --> TYPE["Type Mapping"]
CONFIG --> NAME["Naming Rules"]
CONFIG --> GLOBAL["Global Variables"]
CONFIG --> ATTR["Additional Columns / Attributes"]
TYPE --> META
NAME --> META
ATTR --> META
META --> CONTEXT["Velocity Context"]
GLOBAL --> CONTEXT
CONTEXT --> T1["Entity Template"]
CONTEXT --> T2["Mapper Template"]
CONTEXT --> T3["XML Template"]
CONTEXT --> T4["Service Template"]
CONTEXT --> T5["Controller Template"]
T1 --> CODE["Generated Code"]
T2 --> CODE
T3 --> CODE
T4 --> CODE
T5 --> CODE
CODE --> FORMAT["Formatter"]
FORMAT --> COMPILE["Compile"]
COMPILE --> TEST["Architecture / Unit Tests"]
TEST --> REVIEW["Review"]
这样理解以后,一个成熟的生成流程不应该停止在:
1 | |
而应该继续:
1 | |
常见问题与排查
生成代码出现空类名或空变量
例如:
1 | |
优先检查模板中的:
1 | |
Quiet Reference 是否隐藏了错误。
临时改成普通 Reference,再通过 Template Debug 查看对象。
生成 SQL 最后总有一个逗号
例如:
1 | |
应该检查循环:
1 | |
以及:
1 | |
老模板如果仍然使用:
1 | |
需要重点检查版本兼容。
Java 类型不正确
例如数据库:
1 | |
生成:
1 | |
不要先修改 Entity 模板。
先检查:
1 | |
因为 EasyCode 本身提供独立类型映射配置。
数据库字段名正确,Java 属性名错误
需要区分:
1 | |
并检查命名转换规则。
不要把:
1 | |
直接当成 Java 属性名处理。
表前缀没有去掉
例如:
1 | |
生成:
1 | |
但项目期望:
1 | |
应该统一处理表命名规则,不要分别修改 Entity、Mapper、Service 模板。
生成目录错误
例如希望:
1 | |
却生成到:
1 | |
检查:
1 | |
的组合。
目录和 package 是两件不同的事情,不要只修改其中一个。
单主键项目正常,联合主键表生成异常
检查模板是否直接:
1 | |
如果是,那么这个模板本身就只实现了单主键模型。
应该修改模板,或者把“只允许单主键”升级为数据库设计规范,而不是让代码生成器默默猜测。
修改模板后某些表正常、某些表失败
这通常不是 Velocity 本身的问题,而是不同表触发了不同元数据边界:
1 | |
用测试 Schema 做回归会比逐表猜问题快很多。
#set 中某个值莫名沿用了上一轮循环结果
如果运行环境遵循 Velocity 1.7 的相关语义,需要检查:
1 | |
右侧是否可能得到 null。
旧版 Velocity 的 Null Assignment 行为可能保留旧值。
可以在每轮循环开始先设置明确默认值。
#include 中的变量没有执行
如果需要被包含的文件继续执行 Velocity:
1 | |
应检查是否应该使用:
1 | |
而不是:
1 | |
两者在 Velocity 中语义不同。
团队级 EasyCode 最佳实践
把前面的内容收敛到工程实践,可以形成几条比较稳定的原则。
模板就是代码
模板应该:
1 | |
而不是只存在某个人的 IDEA 配置目录中。
数据库类型映射和模板分离
让:
1 | |
负责:
1 | |
让模板负责:
1 | |
不要让 Entity 模板变成数据库类型判断大全。
公共逻辑使用 Macro
重复的:
1 | |
统一抽取。
避免维护十份几乎相同的模板。
一个模板负责一个文件
优先:
1 | |
而不是一个几百行模板负责全部文件。
EasyCode 本身支持同时运行多个模板,没有必要在模板内部重新造一个模板系统。
把生成边界设计清楚
明确哪些文件:
1 | |
哪些文件:
1 | |
哪些文件:
1 | |
这一条比模板写得多漂亮更重要。
DTO 和 VO 不要机械复制 Entity
数据库 Schema 能告诉代码生成器:
1 | |
但不能完整告诉它:
1 | |
因此 DTO、VO 应该根据业务边界决定是否生成。
模板升级必须做回归
至少准备:
1 | |
每次模板升级重新生成,比较 Diff。
插件升级和模板升级分别管理
因为 EasyCode 插件升级不会覆盖已有模板,所以:
1 | |
不等于:
1 | |
尤其遇到 Velocity 行为变化时,要主动检查模板兼容。
生成以后必须能够编译
一个模板最基本的质量标准不是:
1 | |
而是:
1 | |
更进一步应该达到:
1 | |
如果一个模板每次生成后都需要手工修三处 Import、两个泛型和一个路径,那么它其实并没有完成自动化,只是把手工劳动换了一个位置。
从“代码生成插件”重新理解 EasyCode
第一次接触 EasyCode,很容易把它理解为:
1 | |
但真正有价值的部分其实在后面。
EasyCode 提供的是:
1 | |
Velocity 提供的是:
1 | |
而团队真正需要补充的是:
1 | |
三者组合以后,EasyCode 才不再只是一个“少写几个 Getter 和 Service 的 IDEA 插件”,而会变成项目级代码规范的一部分。
总结
EasyCode 的核心思想并不复杂:
1 | |
真正值得投入时间学习的是这个流程背后的边界。
Velocity 中的:
1 | |
解决模板控制问题;EasyCode 的:
1 | |
解决代码生成上下文问题。Apache Velocity 1.7 官方文档对 Reference、Method、#set、#foreach、Macro、#parse 等语义都有明确描述,而 EasyCode 则在这些基础上叠加自己的对象和工具。
如果只是个人开发,配置几个模板就已经可以节省大量重复工作;如果要进入团队项目,更重要的是把模板视为真正的软件资产:统一类型映射和命名规则,将模板、全局变量和配置纳入版本管理,明确 PO、DTO、VO 等对象边界,处理单主键与联合主键等特殊模型,通过测试 Schema 做回归,并确保生成结果能够直接编译和接受代码检查。
代码生成真正追求的并不是:
1 | |
而是:
1 | |
这也是 EasyCode 最适合在工程项目中承担的位置。