EasyCode 与 Velocity:从数据库表到可维护代码模板的工程化实践

EasyCode 将 IntelliJ IDEA 的数据库元数据转换为可供模板访问的表、字段、类型等对象,再通过 Velocity 模板生成 Entity、DAO、Service、Controller 等代码。真正决定代码生成质量的并不是“右键生成”本身,而是类型映射、命名规则、模板上下文、文件路径和团队代码规范。本文从 EasyCode 的工作机制出发,系统梳理 Velocity 模板语法、模板变量、多模板组织、对象分层、调试方式、版本兼容问题以及团队级代码生成实践。

为什么需要 EasyCode

一个典型的 Java 后端项目中,大量代码具有明显的结构重复性。

例如数据库里存在一张:

1
sys_user

那么围绕它经常会出现:

1
2
3
4
5
6
7
8
9
UserPO
UserMapper
UserRepository
UserService
UserServiceImpl
UserController
UserDTO
UserVO
UserMapper.xml

这些代码并不是完全相同,但通常具有高度稳定的结构:

  • 类名来自数据库表名;
  • 字段来自数据库列;
  • Java 类型由数据库类型映射而来;
  • Mapper、Repository、Service 等类名围绕实体名称组合;
  • package、import、注释、接口继承关系基本固定;
  • CRUD 方法通常具有稳定格式。

如果每增加一张表都手工复制这些代码,不但浪费时间,还非常容易产生复制错误。

例如:

1
2
public interface OrderService {
}

复制成:

1
2
public interface UserService {
}

但内部方法参数、Mapper 注入、泛型或者注释忘记修改,这就是典型的机械劳动错误。

EasyCode 的目标就是把这些可以根据数据库元数据确定的代码结构模板化

官方项目将 EasyCode 定义为基于 IntelliJ IDEA Database Tool 的代码生成插件,通过自定义模板生成代码,常见目标包括 Entity、DAO、Service、Controller,也支持同时处理多张表、多套模板、自定义类型映射、附加列、列属性和分组配置。

从工程角度看,它解决的并不是:

如何替程序员设计业务代码?

而是:

如何把项目中重复、稳定并且可以从元数据推导出来的代码结构自动生成?

这两件事情的边界非常重要。


EasyCode 的整体工作原理

EasyCode 可以理解为一个非常典型的:

1
Metadata + Template -> Source Code

系统。

其核心流程可以抽象成:

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
2
3
4
5
数据库元数据

EasyCode 提供的模板对象和工具

Velocity 模板语言

很多 EasyCode 模板看起来像下面这样:

1
2
3
4
5
$!callback.setFileName(...)
$!tool.append(...)
#foreach($column in $tableInfo.fullColumn)
...
#end

其中:

1
2
3
4
#foreach
#if
#set
#end

属于 Velocity。

而:

1
2
3
4
$tableInfo
$column
$tool
$callback

则属于 EasyCode 放进 Velocity Context 的对象。

这个区别非常关键。

例如:

1
$tool.append(...)

并不是 Velocity 内置函数,而是 EasyCode 暴露给模板使用的工具对象方法。

同样,一些旧 EasyCode 模板中可以看到:

1
2
3
$!define
#save(...)
#setPackageSuffix(...)

其中 $define 通常来自 EasyCode 的全局变量配置,而类似 #save 的内容可能由该全局变量中的 Velocity Macro 定义,并不是 Velocity 官方语言天然提供的指令。EasyCode 官方 Wiki 也明确把“全局变量”作为独立能力,并说明其常用于定义 Velocity 宏或者保存重复模板代码。

理解这一层以后,EasyCode 模板就不会再像一种“神秘 DSL”。

它实际上只是:

1
2
3
4
5
Velocity
+
EasyCode Context
+
团队自己定义的 Macro

安装与基本使用流程

EasyCode 官方仓库中的历史使用说明以 IntelliJ IDEA Ultimate 和 Database Tool 为基础,因为插件本身依赖 IntelliJ 的数据库能力。仓库 README 仍保留着 IntelliJ IDEA Ultimate(172+) 的历史说明,并指出支持范围基本跟随 Database Tool。当前 IDE 版本是否兼容则应以实际插件市场和当前 IDE 插件依赖为准。

安装插件

最直接的方式是在 IntelliJ IDEA 中进入插件市场,搜索:

1
EasyCode

安装完成以后根据 IDE 提示重启。

官方仓库还保留了离线 ZIP 安装方式:安装 ZIP 时直接选择插件包,不需要提前解压。官方说明同时强调,插件升级不会主动覆盖用户已有模板,这一点在团队维护自定义模板时尤其重要。

不同 IntelliJ IDEA 版本的设置菜单已经发生过多次变化,因此旧教程里的:

1
2
3
Settings
-> Other Settings
-> Easy Code

不应该被理解为永久不变的菜单结构。

更稳妥的方法是在:

1
Settings / Preferences

中直接搜索:

1
EasyCode

配置数据库

EasyCode 的数据来源不是 Java Entity,而是 IntelliJ IDEA Database Tool 中已经连接的数据源。

流程大致是:

1
2
3
4
5
6
7
8
9
10
11
Database

配置 DataSource

选择 Schema

选择 Table

EasyCode

Generate Code

官方 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
$tableInfo.name

或者:

1
$tableInfo.fullColumn

例如:

1
2
3
#foreach($column in $tableInfo.fullColumn)
...
#end

表示遍历当前表的全部字段。

一些 EasyCode 模板还会使用:

1
$tableInfo.pkColumn

处理主键字段,使用:

1
$tableInfo.otherColumn

处理非主键字段。类似属性在 EasyCode 的实际模板示例中长期存在。


$column

$column 通常来自:

1
#foreach($column in $tableInfo.fullColumn)

可以继续访问字段信息。

例如:

1
$column.name

通常用于生成 Java 属性名,而:

1
$column.obj.name

在旧版模板中常被用来取得底层数据库列名称。

类型可以通过:

1
$column.type

取得,再结合 EasyCode Tool 获取短类名:

1
$tool.getClsNameByFullName($column.type)

例如数据库类型映射完成后:

1
java.lang.String

最终模板可能只需要:

1
String

旧 EasyCode 模板正是通过类似方式生成实体字段。


$tool

$tool 可以理解为 EasyCode 为模板准备的 Helper。

资料中的典型调用包括:

1
$tool.append(...)

用于组合字符串。

例如概念上:

1
$tool.append($tableInfo.name, "Service")

得到:

1
UserService

还经常看到:

1
$tool.firstLowerCase(...)

将:

1
UserService

处理为:

1
userService

以及:

1
$tool.getClsNameByFullName(...)

将完整 Java 类型转换成适合代码中使用的短类名。

需要再次强调:

1
$tool.append()

不是 Velocity API。

它是 EasyCode API。


$callback

模板不仅需要决定“生成什么内容”,还必须告诉 EasyCode:

1
2
生成什么文件?
放到哪里?

这就是 $callback 的职责之一。

旧模板中经常出现:

1
$!callback.setFileName(...)

以及:

1
$!callback.setSavePath(...)

因此一套模板实际上完成两件事:

1
2
3
文件内容渲染
+
文件输出位置计算

Velocity 基础语法

EasyCode 的模板能力很大程度上来自 Apache Velocity。

只要把 Velocity 的几个核心语法掌握,绝大多数 EasyCode 模板已经可以看懂。


Reference:变量引用

Velocity 使用:

1
$variable

访问变量。

例如:

1
$tableInfo

访问属性时可以写:

1
$tableInfo.name

Velocity 官方文档将引用主要划分为 Variable、Property 和 Method。Property 形式在对象模型下会映射到相应 Getter 等方法,而 Method 则允许显式传递参数。

例如:

1
$user.name

本质上可能访问:

1
user.getName()

而:

1
$tool.append($tableInfo.name, "Service")

则属于显式方法调用。


${}:Formal Reference

如果变量后面紧跟普通文本:

1
$tableInfo.nameService

Velocity 无法知道你希望访问的是:

1
2
3
tableInfo.name
+
Service

还是:

1
tableInfo.nameService

因此应该使用:

1
${tableInfo.name}Service

Apache Velocity 将这种写法称为 Formal Reference。

在代码生成中非常常见:

1
2
public interface ${tableInfo.name}Service {
}

$!:Quiet Reference

EasyCode 模板中大量出现:

1
$!variable

例如:

1
$!{tableInfo.name}

这里的 ! 表示 Quiet Reference。

普通变量不存在时,Velocity 可能保留原引用或者暴露异常行为;Quiet Reference 则倾向于输出空字符串。

这在生成代码时很方便。

例如:

1
2
3
/**
* $!{tableInfo.comment}
*/

如果数据库表没有 Comment,就不希望代码里出现:

1
$tableInfo.comment

但是 $! 也存在一个工程上的副作用:

它会隐藏模板错误。

例如你误写:

1
$!tableInf.name

最终可能什么都没有。

因此调试模板时,一个很好用的办法是暂时减少 Quiet Reference 的使用,让未解析的变量尽早暴露出来。


#set:定义变量

Velocity 使用:

1
#set(...)

定义变量。

例如:

1
#set($entityName = $tableInfo.name)

之后:

1
$entityName

就表示当前实体名称。

代码生成中非常建议使用局部变量,因为这能显著降低模板复杂度。

相比反复写:

1
$tool.append($tableInfo.name, "Service")

可以先写:

1
2
#set($entityName = $tableInfo.name)
#set($serviceName = $tool.append($entityName, "Service"))

后面直接:

1
$serviceName

模板会更容易维护。


Velocity 1.7 中 #set 的一个隐藏坑

Velocity 1.7 官方文档特别说明:

如果 #set 右侧表达式计算结果是 null,旧版 Velocity 不一定会把原变量重新赋值成 null,它可能保留之前的值。

例如:

1
2
3
4
5
6
7
#foreach($column in $tableInfo.fullColumn)

#set($result = $tool.someMethod($column))

$result

#end

假设:

1
2
第一次 -> "String"
第二次 -> null

第二轮可能仍然残留第一次的值。

因此涉及可能返回空值的变量时,更稳妥的模板结构是:

1
2
3
4
5
6
7
8
9
#foreach($column in $tableInfo.fullColumn)

#set($result = "")

#set($result = $tool.someMethod($column))

$result

#end

这个问题尤其值得注意,因为它看起来不像模板语法错误,而更像“为什么某个字段莫名其妙拿到了上一个字段的数据”。


字符串:单引号和双引号

Velocity 中:

1
#set($name = "User")

双引号字符串可以进行变量插值。

例如:

1
#set($text = "Hello $name")

而单引号:

1
#set($text = 'Hello $name')

在 Velocity 1.7 默认语义下不会进行变量插值。

因此 EasyCode 模板里如果需要变量参与字符串拼接,通常应该使用:

1
"$variable"

或者直接通过:

1
$tool.append(...)

完成。


#if:条件判断

基本语法:

1
2
3
4
5
6
7
#if($condition)

#elseif($condition)

#else

#end

例如:

1
2
3
#if($column.comment)
/** $!{column.comment} */
#end

Velocity 支持:

1
2
3
&&
||
!

例如:

1
#if($column.comment && $column.name)

Velocity 1.7 对 Truth Value 的判断和现代 JavaScript、Python 等语言并不完全相同。官方文档的基本规则是 Boolean true 为真、null 和 Boolean false 为假,因此在模板里不要过分依赖“空字符串是不是 false”“空集合是不是 false”这种跨版本容易变化的隐式语义。

对于关键逻辑,更明确的判断通常更安全。


#foreach:字段生成的核心

实体类生成最常见的 Velocity 结构就是:

1
2
3
4
5
#foreach($column in $tableInfo.fullColumn)

private $!{tool.getClsNameByFullName($column.type)} $!{column.name};

#end

假设表结构为:

1
2
3
id          BIGINT
username VARCHAR
created_at TIMESTAMP

模板经过三次循环后,就可以形成类似:

1
2
3
4
5
private Long id;

private String username;

private LocalDateTime createdAt;

当然最终 Java 类型取决于 EasyCode 中配置的类型映射。

Apache Velocity 1.7 为 #foreach 提供:

1
2
3
4
5
$foreach.index
$foreach.count
$foreach.first
$foreach.last
$foreach.hasNext

等循环状态。

例如生成 SQL 字段列表:

1
2
3
#foreach($column in $tableInfo.fullColumn)
$!{column.obj.name}#if($foreach.hasNext),#end
#end

目的就是避免最后多一个逗号。


$velocityHasNext$foreach.hasNext

很多老 EasyCode 模板可以看到:

1
$velocityHasNext

这种写法来自更早期的 Velocity / 模板习惯。

当前更容易兼容 Apache Velocity 1.7 文档所描述循环模型的方式是:

1
$foreach.hasNext

官方 EasyCode 仓库后续提交信息也曾明确处理过旧 $velocityHasNext 写法失效的问题。Velocity 1.7 官方文档则明确提供 $foreach.hasNext

因此维护旧模板时,如果发现:

1
字段后总是出现逗号

或者:

1
$velocityHasNext

原样出现在生成文件里,应该优先检查这一处。


#macro:把公共模板逻辑抽出来

随着模板越来越复杂,复制代码的问题会从 Java 转移到 Velocity。

例如 Entity、DTO、VO 都需要生成字段注释。

与其复制三遍:

1
2
3
4
5
#if($column.comment)
/**
* $!{column.comment}
*/
#end

可以抽成 Macro:

1
2
3
4
5
#macro(fieldComment $column)
#if($column.comment)
/** $!{column.comment} */
#end
#end

使用:

1
#fieldComment($column)

Velocity 的 #macro 就是 Velocimacro,用于创建可复用模板片段。

这也是 EasyCode 全局变量真正有价值的场景之一。

官方 Wiki 对全局变量的定位就包括:

1
2
3
Velocity 宏定义
重复代码
公共模板片段

#include#parse

Velocity 同时提供:

1
#include(...)

和:

1
#parse(...)

它们的区别很容易混淆。

#include

更接近:

1
原样包含文件

被包含内容不会作为 Velocity 模板再次解释。

#parse

则会:

1
2
3
读取文件
+
继续执行其中的 VTL

因此如果公共文件里本身包含:

1
2
3
#if
#foreach
$variable

通常应该使用:

1
#parse(...)

Apache Velocity 官方文档明确区分了两种行为,并且说明 #parse 存在递归深度限制。


注释

Velocity 单行注释:

1
## 这里是模板注释

多行注释:

1
2
3
#*
这里是多行模板注释
*#

这些注释不会出现在最终 Java 文件里。

因此应该区分:

1
## Velocity 模板维护者看的说明

和:

1
2
3
/**
* JavaDoc
*/

前者属于模板源码。

后者属于模板输出。


一个最小 EasyCode 实体模板

理解前面的模型以后,可以构造一个非常精简的模板。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#set($entityName = $tableInfo.name)

$!callback.setFileName($tool.append($entityName, ".java"))

#if($tableInfo.savePackageName)
package $!{tableInfo.savePackageName}.entity;
#end

import java.io.Serializable;

public class $!{entityName} implements Serializable {

#foreach($column in $tableInfo.fullColumn)

#if($column.comment)
/**
* $!{column.comment}
*/
#end
private $!{tool.getClsNameByFullName($column.type)} $!{column.name};

#end
}

它实际上包含四个阶段。

1. 得到实体名称

1
#set($entityName = $tableInfo.name)

2. 确定输出文件

1
$!callback.setFileName(...)

3. 生成 package

1
package ...

4. 遍历数据库字段

1
#foreach(...)

最终 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
2
3
4
5
6
类型怎么映射?
主键怎么处理?
包名怎么算?
表前缀怎么移除?
不同框架生成什么注解?
DTO 和 PO 是否一样?

这些才是一套 EasyCode 配置最终能不能长期使用的关键。


类型映射:模板生成正确代码的第一道防线

数据库类型和 Java 类型并不是一一对应。

例如数据库可能存在:

1
2
3
4
5
6
7
BIGINT
VARCHAR
DECIMAL
TIMESTAMP
DATE
JSON
BOOLEAN

最终 Java 类型取决于:

  • 数据库类型;
  • JDBC Driver;
  • 项目编码规范;
  • ORM;
  • EasyCode 类型映射规则。

EasyCode 官方明确支持自定义数据库类型到 Java 类型的映射,而且支持正则匹配。官方 Wiki 也将类型映射单独列为入门配置步骤。

因此推荐把整个代码生成过程理解为:

1
2
3
4
5
6
7
8
9
Database Column Type

EasyCode Type Mapping

column.type

Velocity Template

Java Field Type

而不是直接在模板里写:

1
2
3
4
5
#if($databaseType == "BIGINT")
Long
#elseif(...)
...
#end

把几十种数据库类型全部硬编码进模板,会导致模板迅速失控。

正确的职责分离应该是:

1
2
3
4
5
类型映射配置
负责 DB Type -> Java Type

Velocity
负责 Java Type -> Java Code

表名和字段名:不要让命名规则散落在每一个模板中

数据库经常存在表前缀:

1
2
3
4
t_user
sys_user
biz_order
iam_role

最终 Java 类可能希望得到:

1
2
3
4
User
User
Order
Role

而不是:

1
2
3
4
TUser
SysUser
BizOrder
IamRole

老 EasyCode 实践中可以看到通过全局模板逻辑判断:

1
t_

前缀,再改变生成类名的方式。

这种方式可以工作,但团队环境中应该注意:

命名规则属于全局生成规则,不应该在 Entity、Mapper、Service、Controller 四个模板里分别实现一遍。

否则很容易发生:

1
2
3
4
Entity        -> User
Mapper -> SysUserMapper
Service -> UserService
Controller -> SysUserController

于是编译器开始替你主持一场命名规则辩论赛。

更好的设计是:

1
2
3
4
5
6
7
数据库表名

统一命名规则

EntityName

所有模板共享

如果确实通过修改 $tableInfo 来实现去前缀,还需要测试同一轮生成中的其他模板是否依赖原始名称,因为共享模板对象一旦被修改,就可能产生连锁影响。


原始数据库名称和 Java 名称要区分

代码生成器通常同时面对两个名字:

数据库:

1
created_time

Java:

1
createdTime

因此模板中不要默认:

1
数据库列名 == Java 字段名

旧 EasyCode 模板经常分别使用类似:

1
$column.obj.name

和:

1
$column.name

前者用于数据库原始列标识,后者用于 Java 属性名称。

例如生成 JPA:

1
2
@Column(name = "created_time")
private LocalDateTime createdTime;

模板需要同时知道:

1
2
created_time
createdTime

如果这两个概念混在一起,最终 SQL、XML 或 ORM 映射迟早会出现问题。


主键是最容易被模板“假装支持”的地方

很多简单代码生成模板直接写:

1
$tableInfo.pkColumn.get(0)

其潜台词其实是:

1
这张表只有一个主键字段。

如果数据库是:

1
2
order_id
product_id

组成联合主键,那么:

1
pkColumn.get(0)

只取第一个字段就已经改变了数据模型语义。

因此模板至少应该明确回答几个问题:

1
2
3
4
5
无主键表怎么办?
单主键怎么办?
联合主键怎么办?
数据库自增主键怎么办?
UUID / Snowflake 等应用生成主键怎么办?

如果当前项目明确规定:

1
所有业务表必须拥有单列 BIGINT 主键

那么模板假设单主键完全没有问题。

问题不是存在约束。

问题是:

模板偷偷假设存在约束,但项目规范里没人知道。

代码生成工具最危险的地方恰恰是它可以非常高效地批量生成错误代码。


不要把所有文件塞进一个 Velocity 模板

EasyCode 支持同时选择多个模板。

因此更合理的模板结构是:

1
2
3
4
5
6
7
8
9
10
Template Group

├── entity.java.vm
├── mapper.java.vm
├── mapper.xml.vm
├── service.java.vm
├── serviceImpl.java.vm
├── controller.java.vm
├── dto.java.vm
└── vo.java.vm

而不是:

1
everything.vm

里面连续几百行:

1
2
3
4
5
6
7
8
9
10
11
## Entity
...

## DAO
...

## Service
...

## Controller
...

分离以后,每个模板都只负责:

1
一个目标文件

例如:

1
2
3
4
5
6
7
8
entity.java.vm
-> UserPO.java

mapper.java.vm
-> UserMapper.java

service.java.vm
-> UserService.java

这样做的收益不仅是可读性。

还包括:

  • 某一层可以单独开启和关闭;
  • 修改 Mapper 不影响 Entity;
  • 不同项目可以复用部分模板;
  • Git Diff 更清楚;
  • 模板版本升级更容易;
  • 测试失败时更容易定位。

使用全局变量解决跨模板重复

当多个模板都需要相同内容:

1
2
3
4
5
6
7
作者
公共 JavaDoc
包名前缀
版权声明

通用 import
命名规则

不要在所有模板中复制。

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
2
3
4
projectAuthor
projectBasePackage
projectVersion
commonDefine

不要创建过于泛化的:

1
2
3
4
name
type
value
data

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 语言规定。尤其 DOVO 在不同团队中的含义并不完全一致:DO 可能表示 Data Object,也可能在 DDD 上下文中表示 Domain Object;VO 也可能分别表示 View Object 或 Value Object,因此项目必须先建立自己的术语表。


EasyCode 最适合生成哪一种对象

EasyCode 的信息来源是:

1
Database Schema

因此它天然最清楚的是:

1
2
3
4
5


类型
主键
数据库注释

所以最容易可靠生成的是与持久化结构接近的对象:

1
2
3
4
5
6
PO
Entity
DO(如果项目将 DO 定义为 Data Object)
Mapper
DAO
Repository

而 DTO:

1
2
3
4
5
6
7
8
9
class UserCreateDTO {

private String username;

private String password;

private String confirmPassword;

}

这里的:

1
confirmPassword

数据库很可能根本不存在。

VO:

1
2
3
4
5
6
7
8
9
10
11
class UserDetailVO {

private Long id;

private String username;

private List<String> roleNames;

private String organizationName;

}

其中:

1
2
roleNames
organizationName

可能来自关联查询,更不是一张 user 表可以推导出来的。

所以把数据库表:

1
user

机械复制成:

1
2
3
4
UserPO
UserDTO
UserVO
UserBO

虽然几秒钟就能生成四套对象,却很可能只是把未来的维护工作批量制造出来。

更合理的边界是:

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
2
3
4
业务接口设计
领域模型
用例模型
前端展示结构

而不能只看数据库 Schema。


EasyCode 也不负责运行时对象转换

EasyCode 做的是:

1
开发阶段 Source Code Generation

而不是:

1
程序运行期间 Object Mapping

例如:

1
2
3
UserPO

UserDTO

运行时可以使用手写转换器或者 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
2
3
4
5
6
7
8
9
10
11
改模板

Generate

编译

报错



继续改

更推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
查看模板上下文

确认 tableInfo

确认 column

确认方法返回值

Preview / Debug

生成单表

检查代码

批量生成

特别是当你不确定:

1
$tableInfo.xxx

到底有没有这个字段时,优先调试对象,而不是从十年前的博客里继续猜属性名。


给模板准备一组“测试表”

代码生成模板和普通代码一样,需要测试边界。

团队至少应该准备一些具有代表性的表。

普通业务表

1
user

覆盖:

1
2
3
BIGINT
VARCHAR
TIMESTAMP

没有字段 Comment 的表

测试:

1
$!column.comment

会不会产生异常注释。

特殊类型表

包含:

1
2
3
4
5
DECIMAL
DATE
JSON
TEXT
BOOLEAN

验证类型映射。

单主键表

验证:

1
pkColumn

联合主键表

用于暴露:

1
pkColumn.get(0)

这类隐藏假设。

带前缀的表

例如:

1
2
3
t_user
sys_role
biz_order

测试命名规则。

最终就形成一套简单的:

1
Template Regression Test

每次修改模板后,对这些表重新生成一次,通过 Git Diff 就可以发现模板是否意外影响其他类型的代码。


不要让 $! 把所有错误吃掉

假设模板写成:

1
$!tableInfo.userName

但实际上根本不存在:

1
userName

Quiet Reference 的结果可能只是:

1
空白

最终你看到:

1
2
public class  {
}

然后开始怀疑:

1
EasyCode 是不是坏了?

实际上是模板变量写错了。

因此推荐:

开发阶段

关键变量适当使用:

1
$tableInfo.name

让错误明显暴露。

模板稳定以后

对真正允许为空的字段:

1
$!tableInfo.comment

再使用 Quiet Reference。

Quiet Reference 应该表达:

这个值允许不存在。

而不应该表达:

不管有没有错误都别告诉我。


JPA 模板中的版本问题

较早的 EasyCode JPA 示例经常生成:

1
2
3
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Column;

以及 Hibernate 相关注解。

这类模板的价值在于展示 EasyCode 如何根据:

1
2
3
4

字段
主键
特殊字段

生成注解,但不应该直接视为适用于所有现代 Java 项目的最终模板。

代码生成模板实际上属于你的技术栈资产。

项目如果发生:

1
2
3
4
框架升级
ORM 升级
包名迁移
代码规范变化

模板也必须升级。

否则很容易出现:

1
2
项目代码已经升级
EasyCode 模板还活在几年前

然后每次生成代码都需要手工修正。

这种代码生成工具如果长期不维护,就会从:

1
效率工具

逐渐进化成:

1
高效率地产生技术债工具

EasyCode 本身的版本也要和旧教程区分

官方仓库当前可检索的 build.gradle 中版本信息为:

1
1.2.9-java.RELEASE

该版本信息对应 2024 年 11 月的更新;构建配置使用 Java 11,并依赖 IntelliJ Java 与 Database 插件。值得注意的是,仓库中仍能看到:

1
// compile 'org.apache.velocity:velocity:1.7'

这样的注释历史配置,但它已经是注释状态,因此不能仅凭这行代码断言当前运行时一定直接依赖该 Maven Artifact。

这解释了为什么阅读 EasyCode 历史资料时最好采用下面的思路:

1
2
3
4
5
6
7
Apache Velocity 1.7 文档

理解老模板的 VTL 语义

EasyCode 当前实际版本

通过插件 Debug 验证 Context 和运行行为

而不是:

1
2
3
十年前模板能跑
=
今天直接复制一定能跑

插件升级为什么可能没有修复你的模板

EasyCode 官方安装说明明确提到:

插件版本更新不会覆盖已有模板。

这其实是一个合理设计。

否则开发团队辛苦维护的模板:

1
2
3
4
Entity
Mapper
Service
Controller

升级插件以后突然全部恢复默认,恐怕比生成失败更刺激。

但它意味着:

1
Plugin Version

和:

1
Template Version

实际上是两个独立的版本。

例如:

1
EasyCode 已经升级

并不代表:

1
$velocityHasNext

这样的历史模板语法已经自动迁移。

因此团队最好明确记录:

1
2
3
EasyCode Plugin Version
Template Config Version
Project Framework Version

三者的关系。


把 EasyCode 模板当成真正的源代码

个人使用 EasyCode 时,经常是:

1
2
在 IDEA 里改几行模板
能跑就算完成

团队使用则完全不够。

模板实际上决定了:

1
2
3
几十张甚至几百张表
×
多个代码层

最终生成什么代码。

它的影响范围可能比普通 Java 类大几十倍。

因此模板应该具备与源代码相似的治理方式:

1
2
3
4
5
6
版本控制
Code Review
变更记录
测试
发布
回滚

而不是:

1
某个同事 IDEA 里的神秘配置

一个推荐的团队模板仓库结构

可以在项目或独立模板仓库中维护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
easycode/
├── README.md
├── CHANGELOG.md

├── config/
│ ├── type-mapping.md
│ ├── naming-rules.md
│ └── global-config.md

├── templates/
│ ├── entity.vm
│ ├── mapper.vm
│ ├── mapper-xml.vm
│ ├── service.vm
│ ├── service-impl.vm
│ ├── controller.vm
│ ├── dto.vm
│ └── vo.vm

├── macros/
│ └── common.vm

└── test-schema/
├── normal.sql
├── composite-key.sql
├── special-types.sql
└── prefix-table.sql

这里的重点不是目录名字,而是把:

1
2
3
4
5
模板
配置
命名规则
测试 Schema
版本记录

从个人 IDE 状态提升为:

1
Team Asset

EasyCode 官方 Wiki 本身也包含配置导出、多设备同步、统一配置以及版本控制同步表配置等主题,说明当模板从个人使用走向团队后,配置管理本身就会成为代码生成体系的一部分。


模板不要生成太多“有意见的代码”

代码生成应该优先生成:

1
2
3
结构稳定
规则明确
不会因为业务变化频繁修改

的内容。

例如:

1
2
public interface UserMapper {
}

非常稳定。

而下面这种业务逻辑:

1
2
3
if (user.getLevel() > 3 && order.getAmount().compareTo(...) > 0) {
...
}

显然不适合根据数据库自动生成。

一个健康的生成边界通常是:

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
2
public interface UserService {
}

或者基础 CRUD。

但模板越深入业务行为,后续重新生成的风险越大。


“重新生成”应该是模板设计时就考虑的问题

第一次生成代码最简单。

真正困难的是:

1
数据库表发生变化以后怎么办?

例如:

1
2
ALTER TABLE sys_user
ADD COLUMN nickname VARCHAR(64);

如果重新运行 EasyCode,而开发者已经在:

1
UserServiceImpl

写了 500 行业务代码,那么直接覆盖显然不可接受。

因此在决定模板结构之前,应该先确定生成策略。

策略一:一次性脚手架

生成:

1
2
3
4
Entity
Mapper
Service
Controller

然后以后不再重新生成。

优点是简单。

缺点是数据库变化无法自动同步。


策略二:只持续生成纯数据层

例如:

1
2
3
generated/
UserPO
UserMapperBase

人工代码:

1
2
3
domain/
service/
controller/

独立维护。

这样可以反复生成数据层,而不覆盖业务逻辑。


策略三:生成 Base 类型

例如:

1
2
3
public class UserServiceImpl
extends GeneratedUserServiceBase {
}

EasyCode 只维护:

1
GeneratedUserServiceBase

人工逻辑写在:

1
UserServiceImpl

这是一种典型的:

1
2
3
Generated Code
+
Handwritten Code

分离策略。

无论使用哪一种,原则都是:

不要让重新运行代码生成器拥有无条件覆盖手写业务逻辑的能力。


一个更完整的 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
Generate Code

而应该继续:

1
2
3
4
5
6
7
8
9
10
11
Generate

Format

Compile

Test

Review Diff

Commit

常见问题与排查

生成代码出现空类名或空变量

例如:

1
2
public class  {
}

优先检查模板中的:

1
$!xxx

Quiet Reference 是否隐藏了错误。

临时改成普通 Reference,再通过 Template Debug 查看对象。


生成 SQL 最后总有一个逗号

例如:

1
2
3
id,
username,
created_time,

应该检查循环:

1
#foreach(...)

以及:

1
$foreach.hasNext

老模板如果仍然使用:

1
$velocityHasNext

需要重点检查版本兼容。


Java 类型不正确

例如数据库:

1
BIGINT

生成:

1
Integer

不要先修改 Entity 模板。

先检查:

1
Type Mapping

因为 EasyCode 本身提供独立类型映射配置。


数据库字段名正确,Java 属性名错误

需要区分:

1
2
数据库原始列名
Java 属性名

并检查命名转换规则。

不要把:

1
created_time

直接当成 Java 属性名处理。


表前缀没有去掉

例如:

1
sys_user

生成:

1
SysUser

但项目期望:

1
User

应该统一处理表命名规则,不要分别修改 Entity、Mapper、Service 模板。


生成目录错误

例如希望:

1
application/service

却生成到:

1
domain/service

检查:

1
2
3
4
Module
savePath
package
callback.setSavePath(...)

的组合。

目录和 package 是两件不同的事情,不要只修改其中一个。


单主键项目正常,联合主键表生成异常

检查模板是否直接:

1
$tableInfo.pkColumn.get(0)

如果是,那么这个模板本身就只实现了单主键模型。

应该修改模板,或者把“只允许单主键”升级为数据库设计规范,而不是让代码生成器默默猜测。


修改模板后某些表正常、某些表失败

这通常不是 Velocity 本身的问题,而是不同表触发了不同元数据边界:

1
2
3
4
5
6
7
没有 Comment
没有主键
联合主键
特殊数据类型
保留字
表前缀
特殊字段名

用测试 Schema 做回归会比逐表猜问题快很多。


#set 中某个值莫名沿用了上一轮循环结果

如果运行环境遵循 Velocity 1.7 的相关语义,需要检查:

1
#set($value = ...)

右侧是否可能得到 null

旧版 Velocity 的 Null Assignment 行为可能保留旧值。

可以在每轮循环开始先设置明确默认值。


#include 中的变量没有执行

如果需要被包含的文件继续执行 Velocity:

1
2
3
#if
#foreach
$variable

应检查是否应该使用:

1
#parse(...)

而不是:

1
#include(...)

两者在 Velocity 中语义不同。


团队级 EasyCode 最佳实践

把前面的内容收敛到工程实践,可以形成几条比较稳定的原则。

模板就是代码

模板应该:

1
2
3
4
进入版本控制
经过 Review
能够回滚
拥有版本记录

而不是只存在某个人的 IDEA 配置目录中。


数据库类型映射和模板分离

让:

1
Type Mapping

负责:

1
Database Type -> Java Type

让模板负责:

1
Java Type -> Source Code

不要让 Entity 模板变成数据库类型判断大全。


公共逻辑使用 Macro

重复的:

1
2
3
4
JavaDoc
字段注释
命名规则
公共 Header

统一抽取。

避免维护十份几乎相同的模板。


一个模板负责一个文件

优先:

1
2
3
4
Entity Template
Mapper Template
Service Template
Controller Template

而不是一个几百行模板负责全部文件。

EasyCode 本身支持同时运行多个模板,没有必要在模板内部重新造一个模板系统。


把生成边界设计清楚

明确哪些文件:

1
可以反复覆盖

哪些文件:

1
只能第一次生成

哪些文件:

1
完全手写

这一条比模板写得多漂亮更重要。


DTO 和 VO 不要机械复制 Entity

数据库 Schema 能告诉代码生成器:

1
持久化结构

但不能完整告诉它:

1
2
3
4
API Contract
Use Case
Domain Model
View Model

因此 DTO、VO 应该根据业务边界决定是否生成。


模板升级必须做回归

至少准备:

1
2
3
4
5
6
普通表
单主键
联合主键
无 Comment
特殊类型
带前缀表

每次模板升级重新生成,比较 Diff。


插件升级和模板升级分别管理

因为 EasyCode 插件升级不会覆盖已有模板,所以:

1
Plugin Upgrade

不等于:

1
Template Migration

尤其遇到 Velocity 行为变化时,要主动检查模板兼容。


生成以后必须能够编译

一个模板最基本的质量标准不是:

1
成功生成文件

而是:

1
生成代码可以编译

更进一步应该达到:

1
2
3
4
5
6
7
8
9
Generate
+
Format
+
Compile
+
Static Check
+
Test

如果一个模板每次生成后都需要手工修三处 Import、两个泛型和一个路径,那么它其实并没有完成自动化,只是把手工劳动换了一个位置。


从“代码生成插件”重新理解 EasyCode

第一次接触 EasyCode,很容易把它理解为:

1
2
3
4
5
数据库右键

Generate

代码出来了

但真正有价值的部分其实在后面。

EasyCode 提供的是:

1
2
3
4
5
Database Metadata
+
Template Context
+
Generation Pipeline

Velocity 提供的是:

1
2
3
4
5
6
7
Variable
Property
Method
Condition
Loop
Macro
Template Composition

而团队真正需要补充的是:

1
2
3
4
5
6
Architecture Convention
Naming Convention
Type Convention
Generation Boundary
Template Governance
Regression Testing

三者组合以后,EasyCode 才不再只是一个“少写几个 Getter 和 Service 的 IDEA 插件”,而会变成项目级代码规范的一部分。


总结

EasyCode 的核心思想并不复杂:

1
2
3
4
5
6
7
8
9
读取数据库元数据

构造模板上下文

执行 Velocity

计算文件名称与路径

生成源码

真正值得投入时间学习的是这个流程背后的边界。

Velocity 中的:

1
2
3
4
5
6
7
8
9
10
$variable
${variable}
$!variable

#set
#if
#foreach
#macro
#parse
#include

解决模板控制问题;EasyCode 的:

1
2
3
4
5
6
7
tableInfo
column
tool
callback
type mapping
global config
additional attributes

解决代码生成上下文问题。Apache Velocity 1.7 官方文档对 Reference、Method、#set#foreach、Macro、#parse 等语义都有明确描述,而 EasyCode 则在这些基础上叠加自己的对象和工具。

如果只是个人开发,配置几个模板就已经可以节省大量重复工作;如果要进入团队项目,更重要的是把模板视为真正的软件资产:统一类型映射和命名规则,将模板、全局变量和配置纳入版本管理,明确 PO、DTO、VO 等对象边界,处理单主键与联合主键等特殊模型,通过测试 Schema 做回归,并确保生成结果能够直接编译和接受代码检查。

代码生成真正追求的并不是:

1
生成得越多越好

而是:

1
2
把确定性的重复劳动交给模板,
把真正需要设计和判断的部分留给开发者。

这也是 EasyCode 最适合在工程项目中承担的位置。


EasyCode 与 Velocity:从数据库表到可维护代码模板的工程化实践
https://allendericdalexander.github.io/2026/08/16/java/utils/EasyCode-Velocity/
作者
AtLuoFu
发布于
2026年8月16日
许可协议