Article
第一章:入门基础
1.1 Velocity 简介与核心特性
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Velocity 是什么 | Apache 开源的基于 Java 的模板引擎,用于将数据模型与模板文件分离,常用于 Web 页面渲染、邮件生成、代码生成等场景。 | 不适用于复杂逻辑处理,应保持模板轻量。 |
| 核心目标 | 实现”关注点分离”(Separation of Concerns),让设计师专注模板,开发者专注业务逻辑。 | 模板中不应包含业务逻辑,仅做展示控制。 |
| 模板语法特点 | 使用 # 开头的指令(如 #if, #foreach)、$ 引用变量(如 $name)、支持宏和包含。 | 语法简洁但功能有限,不支持函数定义。 |
| 安全性设计 | 默认禁止直接调用任意 Java 方法(需通过反射白名单或工具类暴露)。 | 避免在模板中直接调用敏感方法(如 System.exit())。 |
| 跨平台兼容 | 纯 Java 实现,可在任何支持 JVM 的环境中运行。 | 依赖 Java 8+(推荐),旧版本可能有兼容问题。 |
1.2 环境搭建与依赖引入
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| Maven 依赖引入 | 在 pom.xml 中添加:xml<br><dependency><br> <groupId>org.apache.velocity</groupId><br> <artifactId>velocity-engine-core</artifactId><br> <version>2.3</version><br></dependency><br> | 推荐使用 2.x 版本(Velocity Engine Core),1.x 已停止维护。 |
| Gradle 依赖引入 | 在 build.gradle 中添加:implementation 'org.apache.velocity:velocity-engine-core:2.3' | 注意与项目 JDK 版本兼容(2.3 支持 Java 8+)。 |
| 手动下载 JAR | 从 https://velocity.apache.org/download.cgi 下载 velocity-engine-core-x.x.jar 并加入 classpath。 | 需同时引入依赖项(如 commons-lang3、slf4j-api)。 |
| 初始化引擎 | 使用 VelocityEngine 实例:java<br>VelocityEngine ve = new VelocityEngine();<br>ve.init();<br> | 默认配置即可运行,高级功能需配置 properties。 |
| 编码设置 | 建议在初始化时指定输入/输出编码:java<br>ve.setProperty("input.encoding", "UTF-8");<br>ve.setProperty("output.encoding", "UTF-8");<br> | 避免中文乱码,尤其在 Windows 环境下。 |
1.3 第一个 Velocity 模板示例
| 方法/组件 | 语法/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 模板文件(hello.vm) | <p>Hello, $name!</p> | 定义带变量占位符的 HTML/文本模板 | 文件扩展名通常为 .vm,但非强制 |
| Java 渲染代码 | java<br>VelocityEngine ve = new VelocityEngine();<br>ve.init();<br>VelocityContext ctx = new VelocityContext();<br>ctx.put("name", "World");<br>StringWriter writer = new StringWriter();<br>ve.evaluate(ctx, writer, "logTag", "<p>Hello, $name!</p>");<br>System.out.println(writer.toString());<br> | 将上下文数据合并到模板并输出 | evaluate() 可直接传字符串模板;也可用 getTemplate() 加载文件 |
| 使用文件模板 | java<br>Template t = ve.getTemplate("templates/hello.vm", "UTF-8");<br>t.merge(ctx, writer);<br> | 从文件系统加载模板 | 需配置资源加载器(默认从 classpath 或 file 加载) |
| 输出结果 | <p>Hello, World!</p> | 渲染后的最终文本 | 若 name 未定义,默认输出 $name 字面量(可配置静默模式) |
| 静默引用(可选) | 使用 $!name 替代 $name,当 name 为 null 时输出空字符串而非 $name | 避免未定义变量显示占位符 | 推荐在生产环境中使用 $!{} 形式提升健壮性 |
第二章:模板语法详解
2.1 变量引用与赋值(#set)
| 方法/语法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 变量引用 | $variable 或 ${variable} | 引用上下文中的变量 | $name 或 ${user.name} | 若变量未定义,默认输出字面量 $name;推荐使用 ${} 避免歧义 |
| 安全引用 | $!variable 或 $!{variable} | 当变量为 null 时输出空字符串而非字面量 | $!{email} | 生产环境推荐使用,避免模板暴露未定义变量 |
| 赋值指令 | #set( $var = value ) | 在模板内定义或修改变量 | #set( $count = 10 )#set( $title = "Hello" ) | 右侧可为字面量、变量、表达式(如 $a + $b) |
| 表达式赋值 | 支持算术、逻辑、字符串拼接 | 在模板中进行简单计算 | #set( $total = $price * $qty )#set( $msg = "Hi " + $name ) | 不支持复杂函数调用;仅限基本操作 |
| 引用对象属性 | $obj.property | 访问 Java 对象的 getter 方法 | $user.email → 调用 user.getEmail() | 仅能调用无参 public 方法;不能调用任意方法(受 introspection 限制) |
2.2 条件语句(#if / #elseif / #else)
| 方法/语法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 基本 if | #if( condition ) ... #end | 根据条件决定是否渲染内容 | #if( $age >= 18 ) Adult #end | condition 支持比较(==, !=, <, >)、逻辑(&&, ||, !) |
| else 分支 | #if(...) ... #else ... #end | 提供默认分支 | #if( $loggedIn ) Welcome #else Please login #end | 必须在 #if 内部使用 |
| elseif 分支 | #elseif( condition ) | 多条件判断 | #if( $score >= 90 ) A #elseif( $score >= 80 ) B #else C #end | 可多个 #elseif,顺序执行 |
| 真值判断 | 非 null 且非 false 视为 true | 判断变量是否存在或为真 | #if( $user ) Hello $user.name #end | 空字符串、0、null 均视为 false |
| 括号与优先级 | 支持括号分组 | 控制逻辑优先级 | #if( ($a > 0) && ($b < 10) ) ... #end | 推荐使用括号明确逻辑,避免歧义 |
2.3 循环结构(#foreach)
| 方法/语法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 基本 foreach | #foreach( $item in $list ) ... #end | 遍历集合或数组 | velocity<br>#foreach( $product in $products )<br> - $product.name<br>#end | $list 可为 List、Array、Map(遍历 keySet)等 |
| 循环计数器 | $velocityCount | 获取当前循环索引(从 1 开始) | #foreach( $i in [1..3] ) Item $velocityCount: $i #end | 旧版 Velocity 使用 $velocityCount,2.x 仍支持 |
| 循环状态对象 | $foreach.count, $foreach.index, $foreach.hasNext, $foreach.first, $foreach.last | 获取更丰富的循环信息 | #if( $foreach.last ) Last item #end | 需启用 directive.foreach.counter.name = foreach(默认已启用) |
| 遍历 Map | #foreach( $key in $map.keySet() ) 或直接 #foreach( $entry in $map.entrySet() ) | 遍历键值对 | velocity<br>#foreach( $e in $userMap.entrySet() )<br> $e.key: $e.value<br>#end | 需确保 map 方法可被 introspector 调用 |
| 中断循环 | Velocity 不支持 break/continue | 无法提前退出循环 | — | 需在 Java 层过滤数据,或用 #if 控制输出 |
2.4 宏定义与调用(#macro / #end)
| 方法/语法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 宏定义 | #macro( name $arg1 $arg2 ... ) ... #end | 定义可复用模板片段 | #macro( bold $text )<strong>$text</strong>#end | 宏名和参数名不能含特殊字符;作用域为当前模板或全局(若预加载) |
| 宏调用 | #name( arg1 arg2 ... ) 或 #@name()...#end(带 body) | 调用已定义宏 | #bold( "Hello" ) | 参数按位置传递,不支持命名参数 |
| 带 body 的宏 | #macro( alert )<div>#bodyContent#</div>#end | 宏内部可嵌入调用者提供的内容 | 定义:#macro( alert )...调用: #@alert() Warning! #end | 使用 #bodyContent# 插入调用时传入的 body |
| 宏作用域 | 默认在定义模板内可见;可通过 velocimacro.library 全局共享 | 实现跨模板复用 | 在 velocity.properties 中配置:velocimacro.library = common.vm | 全局宏需提前加载,避免运行时找不到 |
| 递归宏 | 宏可调用自身 | 实现递归逻辑(如树形结构) | velocity<br>#macro( tree $node )<br> <li>$node.name<br> #if( $node.children )<br> <ul>#tree( $node.children )</ul><br> #end</li><br>#end | 需注意栈溢出风险;Velocity 默认不限制递归深度 |
2.5 注释与转义字符
| 方法/语法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 单行注释 | ## This is a comment | 模板内添加说明,不输出 | velocity<br>## 用户欢迎信息<br>Hello $name | 注释整行,从 ## 开始到行尾 |
| 多行注释 | #* ... *# | 跨多行注释 | velocity<br>#*<br> 这是一个<br> 多行注释<br>*# | 可嵌套在模板任意位置,包括行内 |
转义 $ | \$variable | 输出字面量 $variable 而非引用 | Price: \$10 → 输出 “Price: $10” | 用于展示模板语法本身 |
转义 # | \#set | 输出字面量 #set | \#if( true ) → 输出 “#if( true )“ | 避免被解析为指令 |
| 转义反斜杠 | \\ | 输出单个反斜杠 | Path: C:\\temp → 输出 “C:\temp” | 需双写反斜杠 |
| 禁用指令解析 | 将指令放入字符串或注释中 | 防止意外执行 | Code: '#set( $x = 1 )' | 字符串中的 # 不会被解析 |
第三章:上下文与数据绑定
3.1 VelocityContext 的使用
| 方法/组件 | 语法/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 创建上下文 | VelocityContext ctx = new VelocityContext(); | 存储模板中可访问的变量 | 是线程不安全的,每个请求应创建新实例 |
| 设置变量 | ctx.put("name", "Alice"); | 向上下文绑定键值对 | 键为字符串,值可为任意 Java 对象 |
| 获取变量 | Object val = ctx.get("name"); | 从上下文读取值(通常在调试时使用) | 模板渲染时由引擎自动调用,一般无需手动 get |
| 嵌套上下文 | java<br>VelocityContext parent = new VelocityContext();<br>VelocityContext child = new VelocityContext(parent);<br> | 实现变量继承(子上下文可访问父变量) | 子上下文 put 的变量不会影响父上下文 |
| 清空上下文 | ctx = new VelocityContext(); | 重置上下文(无 clear() 方法) | 无法清空已有实例,需新建 |
| 与渲染结合 | ve.evaluate(ctx, writer, "tag", templateStr); | 将上下文传入模板引擎进行合并 | 必须在调用 evaluate() 或 merge() 前完成 put |
3.2 向模板传入 Java 对象
| 方法/组件 | 语法/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 传入 POJO | ctx.put("user", new User("Bob", 25)); | 将自定义对象暴露给模板 | 对象需有 public getter 方法(如 getName()) |
| 传入集合 | ctx.put("items", Arrays.asList("A", "B")); | 传递 List、Set、Array 等用于 #foreach | Map 也可直接传入,模板中可遍历 keySet 或 entrySet |
| 传入 Map | java<br>Map<String, Object> data = new HashMap<>();<br>data.put("title", "News");<br>ctx.put("page", data);<br> | 以 Map 形式组织数据 | 模板中通过 $page.title 访问 |
| 传入基本类型 | ctx.put("count", 100);ctx.put("active", true); | 传递 int、boolean、String 等 | 自动装箱为对应包装类(Integer、Boolean) |
| 传入 null | ctx.put("optional", null); | 表示可选字段 | 模板中 $optional 默认输出 $optional,建议用 $!{optional} |
| 多对象传入 | ctx.put("user", user);ctx.put("config", config); | 同时传入多个独立对象 | 每个对象独立命名,互不影响 |
3.3 访问对象属性与方法调用限制
| 概念/规则 | 说明 | 注意事项 |
|---|---|---|
| 属性访问机制 | $user.name → 调用 user.getName() 或 user.isName()(boolean) | 必须是 public 无参方法;不支持字段直接访问 |
| 方法调用限制 | 默认仅允许调用 getter、size()、length()、iterator() 等安全方法 | 不能调用任意方法(如 user.deleteAccount()),防止安全风险 |
| Introspector 机制 | Velocity 使用 Apache Commons BeanUtils 的 introspection 查找方法 | 若 getter 不存在或非 public,则属性不可访问 |
| 链式调用 | $user.address.city → 调用 user.getAddress().getCity() | 中间任一环节为 null 则整个表达式返回 null(静默失败) |
| 数组/列表索引 | $list[0] → 调用 list.get(0)(List)或 array[0](Array) | 支持整数索引;不支持负数或越界检查(越界返回 null) |
| Map 键访问 | $map.key 或 $map["key"] → 调用 map.get("key") | 推荐使用 $map["key with space"] 处理含空格的 key |
| 禁止的操作 | 不能 new 对象、不能调用 static 方法、不能执行 setter | 模板应只读,不修改数据状态 |
3.4 工具类(Tools)集成(如 DateTool、NumberTool)
| 工具类 | 用途 | 代码示例(Java 端) | 模板调用示例 | 注意事项 |
|---|---|---|---|---|
| DateTool | 格式化日期、计算时间 | ctx.put("date", new DateTool()); | $date.format('yyyy-MM-dd', $now) | 需添加 velocity-tools 依赖 |
| NumberTool | 格式化数字、货币、百分比 | ctx.put("number", new NumberTool()); | $number.format('#,##0.00', $price) | 支持 Locale,但需显式配置 |
| EscapeTool | HTML/JavaScript/URL 转义 | ctx.put("esc", new EscapeTool()); | $esc.html($userInput) | 防 XSS 攻击必备 |
| DisplayTool | 处理 null 显示、截断文本 | ctx.put("display", new DisplayTool()); | $display.alt($description "N/A") | 替代 $!{} 的更强大方案 |
| MathTool | 基础数学运算(abs、round 等) | ctx.put("math", new MathTool()); | $math.round($value) | 不支持复杂函数(如 sin、log) |
| 初始化方式 | 手动 put 或使用 ToolManager | java<br>ToolManager tm = new ToolManager();<br>Context ctx = tm.createContext(); | 自动注入所有注册工具 | 推荐使用 ToolManager 简化管理 |
| 依赖引入 | Maven:提供常用工具类实现 | xml<br><dependency><br> <groupId>org.apache.velocity.tools</groupId><br> <artifactId>velocity-tools-generic</artifactId><br> <version>3.1</version><br></dependency><br> | — | velocity-tools 3.x 需 Java 8+,与 velocity-engine-core 2.x 兼容 |
第四章:高级功能
4.1 模板包含(#parse 与 #include)
| 指令 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
#parse | #parse( "header.vm" ) | 将指定模板内容解析并执行(支持变量、指令) | #parse( "common/nav.vm" ) | 被包含模板可访问当前上下文;可嵌套调用(但有深度限制,默认 20 层) |
#include | #include( "footer.html" ) | 原样插入文件内容(不解析 Velocity 语法) | #include( "static/disclaimer.txt" ) | 适用于纯文本/HTML 片段,性能略高于 #parse |
| 文件路径 | 相对路径基于资源加载器根目录 | 定位被包含模板 | #parse( "templates/partials/sidebar.vm" ) | 路径分隔符统一用 /,即使 Windows 系统 |
| 动态包含 | 支持变量作为参数 | 根据条件包含不同模板 | velocity<br>#set( $tpl = "mobile.vm" )<br>#parse( $tpl ) | 变量值必须是字符串且指向有效模板 |
| 循环包含风险 | 在 #foreach 中使用 #parse | 可能导致性能问题或栈溢出 | — | 避免在循环内频繁 #parse;可预合并模板 |
| 错误处理 | 若模板不存在,默认抛出 ResourceNotFoundException | 需确保模板路径正确 | — | 可通过自定义 ResourceLoader 实现静默失败 |
4.2 自定义指令开发
| 组件 | 说明 | 代码示例(Java) | 注册方式 | 注意事项 |
|---|---|---|---|---|
| 指令基类 | 继承 org.apache.velocity.runtime.directive.Directive | public class AlertDirective extends Directive { ... } | 在 velocity.properties 中注册:userdirective = com.example.AlertDirective | 必须提供无参构造函数 |
| 指令名称 | 重写 getName() 方法 | @Override public String getName() { return "alert"; } | 模板中通过 #alert() 调用 | 名称不能与内置指令冲突(如 if、set) |
| 指令类型 | 重写 getType() 返回 LINE 或 BLOCK | @Override public int getType() { return BLOCK; } | BLOCK 型需配对 #end,如 #alert()...#end | LINE 型单行结束,如 #set() |
| 渲染逻辑 | 重写 render(InternalContextAdapter context, Writer writer, Node node) | 解析参数并写入 writer | 可通过 node.jjtGetChild(i) 获取参数节点 | 需处理异常并记录日志 |
| 参数解析 | 使用 NodeUtils.tokenLiteral(node.jjtGetChild(0)) 获取字符串参数 | 支持字面量、变量引用 | String msg = (String) node.jjtGetChild(0).value(context); | 复杂参数建议用 AST 分析 |
| 线程安全 | 指令实例由引擎全局共享 | 所有字段应为 final 或线程安全 | — | 避免在指令中保存请求级状态 |
4.3 事件处理与拦截器(EventCartridge)
| 事件类型 | 触发时机 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| ReferenceInsertionEventHandler | 当 $var 被替换为实际值时 | 修改输出值(如脱敏、高亮) | public Object referenceInsert(String ref, Object val) { return "***"; } | 返回 null 表示跳过该引用 |
| NullSetEventHandler | 当 #set 赋值为 null 时 | 拦截 null 赋值行为 | public boolean shouldLogOnNullSet(String lhs, String rhs) { return false; } | 可用于调试或安全控制 |
| MethodExceptionEventHandler | 当方法调用抛出异常时 | 自定义异常处理(如返回默认值) | public Object methodException(Class clz, String method, Exception e) { return "[ERROR]"; } | 默认会抛出异常中断渲染 |
| IncludeEventHandler | 当 #include / #parse 被调用时 | 动态修改包含路径或阻止包含 | public String includeEvent(String includeResource, String currentResource, String directiveName) | 可实现权限控制或路径重定向 |
| 注册方式 | 通过 EventCartridge 绑定到上下文 | java<br>EventCartridge ec = new EventCartridge();<br>ec.addEventHandler(myHandler);<br>ctx.attachEventCartridge(ec);<br> | 必须在渲染前 attach 到 VelocityContext | 事件处理器可叠加多个 |
| 性能影响 | 每次引用/方法调用均触发回调 | 避免复杂逻辑 | — | 仅在必要时使用,如审计、安全过滤 |
4.4 资源加载器(ResourceLoader)配置
| 加载器类型 | 用途 | 配置方式(velocity.properties) | 说明 | 注意事项 |
|---|---|---|---|---|
| file | 从文件系统加载模板 | resource.loader = filefile.resource.loader.class = org.apache.velocity.runtime.resource.loader.FileResourceLoaderfile.resource.loader.path = /opt/templates | 开发阶段常用 | 路径为绝对或相对(相对于启动目录) |
| classpath | 从 classpath 加载(JAR 内) | resource.loader = classclass.resource.loader.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader | 生产环境推荐 | 模板需打包进 JAR/WAR 的 resources 目录 |
| jar | 从指定 JAR 文件加载 | jar.resource.loader.class = org.apache.velocity.runtime.resource.loader.JarResourceLoaderjar.resource.loader.path = jar:file:/lib/tpls.jar | 适用于插件化模板 | 路径格式必须为 jar:file:... |
| url | 从 HTTP/HTTPS 加载(不推荐) | url.resource.loader.class = org.apache.velocity.runtime.resource.loader.URLResourceLoaderurl.resource.loader.root = http://example.com/templates/ | 远程模板(高延迟、不可靠) | 仅用于特殊场景,需处理超时和缓存 |
| 多加载器 | 按顺序尝试多个加载器 | resource.loader = file, class(先 file 后 class) | 实现 fallback 机制 | 加载器名称(如 file)需与 .resource.loader.class 前缀一致 |
| 缓存控制 | 是否缓存模板内容 | file.resource.loader.cache = truefile.resource.loader.modificationCheckInterval = 2 | 开发时设 cache=false,生产设 true | modificationCheckInterval 单位为秒(仅当 cache=true 时生效) |
| 自定义加载器 | 实现 ResourceLoader 接口 | myloader.resource.loader.class = com.example.DBResourceLoader | 从数据库、Redis 等加载模板 | 需重写 getResourceStream() 和 isSourceModified() |
第五章:配置与性能优化
5.1 velocity.properties 配置详解
| 配置项 | 默认值 | 说明 | 示例值 | 注意事项 |
|---|---|---|---|---|
resource.loader | 无(必须设置) | 指定使用的资源加载器名称(如 file, class) | resource.loader = file | 可指定多个,用逗号分隔,按顺序查找 |
file.resource.loader.path | 当前目录 | 文件加载器的根路径 | file.resource.loader.path = /templates | 多路径可用逗号分隔 |
class.resource.loader.cache | true | 是否缓存从 classpath 加载的模板 | class.resource.loader.cache = false | 开发时建议关闭缓存以便热更新 |
input.encoding | ISO-8859-1 | 模板文件读取编码 | input.encoding = UTF-8 | 必须与模板文件实际编码一致 |
output.encoding | ISO-8859-1 | 模板输出编码 | output.encoding = UTF-8 | Web 应用中应设为 UTF-8 |
runtime.log | velocity.log | 日志文件路径 | runtime.log = /logs/velocity.log | 可设为 null 禁用日志 |
velocimacro.library | 无 | 全局宏库文件路径(可多个) | velocimacro.library = macros.vm,common.vm | 路径相对于资源加载器根目录 |
velocimacro.permissions.allow.inline | true | 是否允许在模板中定义宏 | velocimacro.permissions.allow.inline = false | 生产环境可禁用以提升安全 |
parser.pool.size | 20 | AST 解析器池大小 | parser.pool.size = 50 | 高并发场景可适当增大 |
directive.foreach.counter.name | velocityCount | #foreach 计数器变量名 | directive.foreach.counter.name = loopIndex | 影响 $velocityCount 的名称 |
introspector.restrict.packages | 无 | 限制可访问的 Java 包(安全沙箱) | introspector.restrict.packages = java.lang | 防止模板调用危险类 |
introspector.restrict.classes | 无 | 限制可访问的具体类 | introspector.restrict.classes = java.lang.System | 常用于屏蔽 System、Runtime 等 |
5.2 缓存机制与模板预编译
| 机制/操作 | 说明 | 代码/配置示例 | 用途 | 注意事项 |
|---|---|---|---|---|
| 模板缓存 | Velocity 默认缓存已加载的 Template 对象 | file.resource.loader.cache = true(默认) | 避免重复 I/O 和解析,提升性能 | 修改模板后需重启或等待 modificationCheckInterval |
| 缓存检查间隔 | 定期检查模板文件是否被修改 | file.resource.loader.modificationCheckInterval = 5 | 开发时自动重载模板 | 仅当 cache=true 时生效;单位为秒 |
| 手动预编译 | 启动时加载所有模板到内存 | Template t = ve.getTemplate("home.vm");(提前调用) | 避免首次访问延迟 | 可遍历模板目录批量加载 |
| 清除缓存 | 不支持运行时清除单个模板缓存 | 需重启应用或自定义 ResourceLoader 实现 | — | 官方未提供 clearCache() API |
| 内存占用 | 每个 Template 对象常驻内存 | — | 高模板数量场景需监控内存 | 建议对不常用模板按需加载 |
| 预编译优势 | 模板语法错误在启动时报出 | ve.init(); ve.getTemplate("error.vm"); | 提前暴露模板错误 | 比运行时错误更易排查 |
5.3 多语言与编码支持
| 配置/操作 | 说明 | 示例 | 注意事项 |
|---|---|---|---|
| 输入编码 | 模板文件的字符编码 | input.encoding = UTF-8 | 必须与模板文件保存编码一致,否则中文乱码 |
| 输出编码 | 渲染结果的字符编码 | output.encoding = UTF-8 | Web 场景需与 HTTP 响应头 Content-Type: text/html; charset=UTF-8 一致 |
| Java 源码编码 | Java 字符串字面量编码 | ctx.put("msg", "你好"); | 若 Java 文件非 UTF-8 编译,字符串可能乱码 |
| 模板内 Unicode | 直接写 Unicode 字符或转义 | 欢迎 $name! 或 \u4f60\u597d | 推荐直接使用 UTF-8 编辑模板,避免转义 |
| 国际化(i18n) | Velocity 本身不提供 i18n,需结合 Java ResourceBundle | ctx.put("i18n", bundle);模板中: $i18n.welcome | 需自行实现多语言键值管理 |
| 多语言模板 | 为不同语言维护独立模板文件 | home_en.vm, home_zh.vm | 通过逻辑选择加载对应模板 |
5.4 安全性与沙箱限制
| 安全机制 | 说明 | 配置/代码示例 | 用途 | 注意事项 |
|---|---|---|---|---|
| 方法调用限制 | 默认仅允许”安全”方法(getter、size 等) | 无需配置,默认启用 | 防止模板执行任意 Java 方法 | 不能调用 System.exit()、File.delete() 等 |
| 包限制 | 禁止访问指定包下的类 | introspector.restrict.packages = java.lang,java.io | 屏蔽危险包 | 即使有 getter 也无法访问被禁包中的对象 |
| 类限制 | 禁止访问特定类 | introspector.restrict.classes = java.lang.Runtime,java.lang.System | 精准屏蔽高危类 | 优先级高于包限制 |
| 宏安全 | 禁止模板内定义宏 | velocimacro.permissions.allow.inline = false | 防止宏注入或覆盖 | 全局宏仍可通过 velocimacro.library 使用 |
| 上下文隔离 | 每个请求使用独立 VelocityContext | new VelocityContext() per request | 防止数据泄露 | VelocityContext 非线程安全 |
| 表达式深度限制 | 限制嵌套表达式深度(防 DoS) | 无直接配置,依赖 AST 解析器内部限制 | 防止恶意模板耗尽栈空间 | 极端复杂表达式可能被拒绝 |
| 沙箱实践 | 结合 SecurityManager(不推荐) | — | 更强隔离(但复杂) | Velocity 官方不推荐依赖 SecurityManager,建议用 restrict 配置 |
第六章:与主流框架集成
6.1 在 Spring Boot 中集成 Velocity
| 配置项 / 组件 | 说明 | 示例代码 / 配置 | 注意事项 |
|---|---|---|---|
| 依赖引入(Maven) | Spring Boot 2.0+ 已移除 Velocity 官方支持,需手动添加引擎和视图解析器 | xml<br><dependency><br> <groupId>org.apache.velocity</groupId><br> <artifactId>velocity-engine-core</artifactId><br> <version>2.3</version><br></dependency><br><dependency><br> <groupId>org.springframework</groupId><br> <artifactId>spring-context-support</artifactId><br></dependency> | 不要使用已废弃的 spring-boot-starter-velocity |
| 视图解析器配置 | 注册 VelocityViewResolver 并配置前缀/后缀 | java<br>@Bean<br>public ViewResolver velocityViewResolver() {<br> VelocityViewResolver resolver = new VelocityViewResolver();<br> resolver.setPrefix("templates/");<br> resolver.setSuffix(".vm");<br> resolver.setContentType("text/html;charset=UTF-8");<br> return resolver;<br>}<br><br>@Bean<br>public VelocityEngine velocityEngine() {<br> VelocityEngine ve = new VelocityEngine();<br> ve.setProperty("input.encoding", "UTF-8");<br> ve.setProperty("output.encoding", "UTF-8");<br> ve.setProperty("resource.loader", "class");<br> ve.setProperty("class.resource.loader.class",<br> "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");<br> ve.init();<br> return ve;<br>} | 模板需放在 src/main/resources/templates/ 目录下 |
| Controller 返回视图 | 返回逻辑视图名,由解析器匹配模板 | java<br>@Controller<br>public class HomeController {<br> @GetMapping("/hello")<br> public String hello(Model model) {<br> model.addAttribute("name", "Spring");<br> return "hello"; // → templates/hello.vm<br> }<br>} | 与 Thymeleaf/FreeMarker 用法一致 |
| 自动配置替代方案 | 可通过 application.properties 配置(需自定义) | spring.velocity.enabled=true(无效,仅旧版支持) | Spring Boot 2.0+ 无自动配置,必须手动注册 Bean |
| 静态资源处理 | Velocity 不影响 /static、/public 下的静态文件 | — | 确保 WebMvcConfigurer 未覆盖静态资源路径 |
6.2 与 Web 框架(如 Servlet、Struts)结合
| 框架 | 集成方式 | 示例代码 / 配置 | 注意事项 |
|---|---|---|---|
| 原生 Servlet | 在 HttpServlet 中手动渲染模板 | java<br>protected void doGet(HttpServletRequest req, HttpServletResponse resp) {<br> VelocityEngine ve = (VelocityEngine) getServletContext()<br> .getAttribute("velocityEngine");<br> VelocityContext ctx = new VelocityContext();<br> ctx.put("user", req.getParameter("name"));<br> Template t = ve.getTemplate("web/hello.vm");<br> resp.setContentType("text/html;charset=UTF-8");<br> t.merge(ctx, resp.getWriter());<br>} | 建议在 ServletContextListener 中初始化 VelocityEngine 并存入 context |
| Struts 1.x | 使用 VelocityLayoutServlet 或自定义 Action | 在 struts-config.xml 中配置:<controller processorClass="org.apache.struts.action.RequestProcessor"/>并设置 velocity.properties | 需引入 velocity-tools-view,官方已停止维护 |
| Struts 2 | 通过 result-type 集成 | 在 struts.xml 中:xml<br><result-types><br> <result-type name="velocity"<br> class="org.apache.struts2.views.velocity.VelocityResult"/><br></result-types><br><result name="success" type="velocity"><br> /templates/success.vm<br></result> | 需添加 struts2-velocity-plugin 依赖 |
| 初始化位置 | Web 应用启动时初始化 VelocityEngine | java<br>public class VelocityInitListener<br> implements ServletContextListener {<br> public void contextInitialized(ServletContextEvent sce) {<br> VelocityEngine ve = new VelocityEngine();<br> ve.init();<br> sce.getServletContext().setAttribute("ve", ve);<br> }<br>} | 避免每次请求重复初始化 |
| 路径映射 | 模板路径相对于 Web 应用根目录或 classpath | ve.setProperty("file.resource.loader.path", "/WEB-INF/templates"); | 生产环境推荐使用 classpath 加载(更安全) |
6.3 用于邮件模板或代码生成场景
| 场景 | 实现方式 | 示例代码 | 注意事项 |
|---|---|---|---|
| 邮件模板 | 将 Velocity 渲染结果作为邮件正文 | java<br>VelocityEngine ve = new VelocityEngine();<br>ve.init();<br>VelocityContext ctx = new VelocityContext();<br>ctx.put("username", "Alice");<br>ctx.put("resetLink",<br> "https://example.com/reset?token=123");<br>StringWriter writer = new StringWriter();<br>ve.getTemplate("email/password_reset.vm")<br> .merge(ctx, writer);<br>String htmlBody = writer.toString();<br>// 通过 JavaMailSender 发送 htmlBody | 模板应使用内联 CSS(部分邮箱不支持外部样式) |
| 代码生成 | 根据模型数据生成 Java/SQL/XML 等源码 | java<br>ctx.put("tableName", "User");<br>ctx.put("columns", Arrays.asList(<br> new Column("id", "Long"),<br> new Column("name", "String")));<br>ve.getTemplate("codegen/entity.vm")<br> .merge(ctx, new FileWriter("User.java")); | 适用于 ORM 实体、DAO、DTO 等重复性代码 |
| 批量生成 | 循环遍历元数据生成多个文件 | java<br>for (Table table : schema.getTables()) {<br> ctx.put("table", table);<br> ve.getTemplate("dao.vm")<br> .merge(ctx, new FileWriter(<br> table.getName() + "Dao.java"));<br>} | 可集成到 Maven 插件或 Gradle task 中 |
| 模板组织 | 按功能拆分模板(如 email/header.vm, email/body.vm) | 使用 #parse("email/header.vm") 复用头部 | 提升可维护性,避免大模板 |
| 输出控制 | 生成纯文本(非 HTML)时注意换行与缩进 | 模板中直接写代码结构:public class ${className} { private ${type} ${name};} | Velocity 保留模板中的空白符,适合格式化输出 |
第七章:底层原理与扩展机制
7.1 模板解析流程概述
| 步骤名称 | 操作细节 | 涉及核心类 | 注意事项 |
|---|---|---|---|
| 模板加载 | 通过 ResourceLoader 从文件、classpath 等读取模板内容为字符串 | ResourceLoader, ResourceManager | 若启用缓存,已加载模板直接返回 Template 对象 |
| 词法分析 | 将模板字符串拆分为 Token 流(如 #if, $var, 文本等) | Parser(基于 JavaCC 生成) | 语法错误在此阶段抛出 ParseException |
| 语法分析 | 构建抽象语法树(AST),表示模板结构 | AST* 节点类(如 ASTIfStatement, ASTReference) | AST 是后续执行的中间表示 |
| 指令注册绑定 | 将 AST 中的指令节点(如 #set)关联到对应的 Directive 实例 | RuntimeServices.getDirective(String name) | 内置指令(#if, #foreach)和用户自定义指令均在此绑定 |
| 上下文合并 | 遍历 AST 节点,结合 VelocityContext 执行指令并输出文本 | SimpleNode.render() | 渲染过程是递归遍历 AST 并写入 Writer |
| 输出生成 | 最终文本写入目标 Writer(如 StringWriter、ServletOutputStream) | Writer 接口 | 编码由 output.encoding 控制 |
7.2 AST(抽象语法树)结构简介
| AST 节点类型 | 对应模板语法 | 说明 | 示例 |
|---|---|---|---|
ASTReference | $name, $user.email | 表示变量引用 | $product.price → ASTReference 包含 identifier “product” 和 property “price” |
ASTSetDirective | #set( $x = 1 ) | 表示赋值指令 | 子节点包含左值($x)和右值(1) |
ASTIfStatement | #if( $cond ) ... #end | 条件语句节点 | 包含 condition 子节点和 body 子节点 |
ASTElseIfStatement / ASTElseStatement | #elseif, #else | if 的分支节点 | 作为 ASTIfStatement 的兄弟节点 |
ASTForeachStatement | #foreach( $i in $list ) | 循环节点 | 包含 element 变量名、collection 表达式、循环体 |
ASTText | 普通文本(非指令/引用) | 字面量文本块 | <h1>Welcome</h1> → 单个 ASTText 节点 |
ASTComment | ## comment 或 #* ... *# | 注释节点(渲染时跳过) | 不参与输出 |
ASTMacro | #macro( hello $name ) | 宏定义节点 | 存储宏名、参数列表、宏体 |
ASTDirective(通用) | 自定义指令 | 所有指令的基类 | 用户自定义指令也生成此类子节点 |
| 节点关系 | 树形结构,父子/兄弟关系 | 通过 jjtGetChild(i) 访问子节点 | AST 是 Visitor 模式的基础 |
7.3 指令(Directive)执行机制
| 机制环节 | 说明 | 核心方法 / 类 | 注意事项 |
|---|---|---|---|
| 指令注册 | 启动时加载内置指令 + 用户配置的 userdirective | DirectiveFactoryRuntimeInitializer.initializeDirectives() | 指令名必须全局唯一 |
| 指令查找 | 渲染时根据 AST 节点名查找对应 Directive 实例 | RuntimeServices.getDirective(String name) | 返回单例实例(线程安全) |
| 初始化 | 首次使用时调用 init(RuntimeServices rs, InternalContextAdapter context, Node node) | Directive.init() | 可在此解析参数结构 |
| 渲染执行 | 调用 render(InternalContextAdapter context, Writer writer, Node node) | Directive.render() | 开发者需在此实现核心逻辑 |
| BLOCK vs LINE | BLOCK 型指令需处理 #end 及内部 body | getType() == BLOCK | BLOCK 指令的 node 包含子节点(body) |
| 参数获取 | 通过 node.jjtGetChild(i) 获取 AST 子节点 | NodeUtils.tokenLiteral(node.jjtGetChild(0)) | 字面量参数需转义处理 |
| 上下文操作 | 可通过 context 修改变量(如 #set) | context.put("key", value) | 修改仅在当前上下文有效 |
| 异常处理 | 抛出 IOException 或 MethodInvocationException | — | 异常会中断渲染并向上抛出 |
| 执行顺序 | 按 AST 深度优先遍历顺序执行 | SimpleNode.render() 递归调用 | 指令执行是串行的 |
7.4 自定义 Uberspect 实现
| 概念/组件 | 说明 | 示例代码 | 注意事项 |
|---|---|---|---|
| Uberspect 作用 | 控制 Velocity 如何”观察”Java 对象(即如何查找方法/属性) | 替代默认的 SecureUberspector | 用于实现自定义属性访问策略 |
| 默认实现 | SecureUberspector:仅允许 public getter/setter/size/length/is* | — | 屏蔽私有方法和危险方法 |
| 自定义步骤 | 继承 AbstractUberspector 或实现 Uberspect 接口 | java<br>public class MyUberspector<br> extends AbstractUberspector {<br> @Override<br> public Iterator getIterator(<br> Object obj, Info info) {<br> // 自定义迭代逻辑<br> }<br> @Override<br> public VelPropertyGet getPropertyGet(<br> Object obj, String methodName, Info i) {<br> return new MyVelGetter(obj, methodName);<br> }<br>} | 需同时实现 VelPropertyGet 等辅助接口 |
| 注册方式 | 在 velocity.properties 中指定 | runtime.introspector.uberspect = com.example.MyUberspector | 必须提供无参构造函数 |
| 应用场景 | 支持字段直接访问(而非仅 getter) 允许调用特定业务方法 集成 Groovy/动态对象 | java<br>// 允许 $obj.field 直接访问 public field<br>Field f = obj.getClass()<br> .getField(methodName);<br>return f.get(obj); | 需谨慎评估安全风险 |
| 性能影响 | Uberspect 被频繁调用(每次属性访问) | 建议缓存 Method/Field 对象 | 可结合 Introspector 缓存机制 |
| 与沙箱协同 | 可在 Uberspect 中集成包/类白名单 | java<br>if (isRestrictedClass(obj.getClass())) {<br> throw new SecurityException(<br> "Access denied");<br>} | 比 restrict.packages 更灵活 |
第八章:代码生成
8.1 配置依赖
| 方法/配置项名称 | 语法/配置方式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 引入 Velocity Engine | Maven / Gradle 依赖声明 | 在项目中集成 Apache Velocity 模板引擎 | xml<br><dependency><br> <groupId>org.apache.velocity</groupId><br> <artifactId>velocity-engine-core</artifactId><br> <version>2.3</version><br></dependency> | 确保版本兼容 JDK;若用于 Web 项目,可考虑 velocity-tools |
| 初始化 VelocityEngine | Java API 调用 | 创建并配置模板引擎实例 | java<br>VelocityEngine ve = new VelocityEngine();<br>ve.setProperty(<br> VelocityEngine.FILE_RESOURCE_LOADER_PATH,<br> "templates");<br>ve.init(); | FILE_RESOURCE_LOADER_PATH 应指向模板所在目录;多线程环境下建议单例使用 |
| 设置字符编码 | setProperty("input.encoding", "UTF-8") | 避免中文乱码 | java<br>ve.setProperty("input.encoding", "UTF-8");<br>ve.setProperty("output.encoding", "UTF-8"); | 输入输出编码需一致,推荐统一使用 UTF-8 |
8.2 生成后端代码
| 方法/配置项名称 | 语法/配置方式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 加载后端模板(如 Entity.vm) | ve.getTemplate("Entity.vm", "UTF-8") | 获取预定义的后端实体类模板 | java<br>Template template = ve.getTemplate(<br> "Entity.vm", "UTF-8"); | 模板文件需存在于 resource loader 路径下 |
| 填充上下文数据 | VelocityContext context = new VelocityContext();context.put("className", "User"); | 向模板传入动态变量 | java<br>VelocityContext ctx = new VelocityContext();<br>ctx.put("packageName", "com.example.model");<br>ctx.put("fields", fieldList); | 字段名需与模板中 $!{xxx} 或 ${xxx} 变量一致;支持 List、Map 等复杂类型 |
| 渲染输出到文件 | FileWriter + template.merge(context, writer) | 将生成的 Java 代码写入 .java 文件 | java<br>FileWriter writer = new FileWriter("User.java");<br>template.merge(ctx, writer);<br>writer.close(); | 注意关闭流;路径需提前创建父目录;建议使用 try-with-resources |
8.3 生成前端代码
| 方法/配置项名称 | 语法/配置方式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 使用 Vue/React 模板 | 编写如 view.vue.vm 或 component.jsx.vm | 生成前端组件代码 | 模板内容示例:html<br><template><br> <div>{{ className }}</div><br></template> | 模板语法需符合目标框架规范;避免在 .vm 中硬编码业务逻辑 |
| 注入 API 路径和字段映射 | context.put("apiUrl", "/api/user");context.put("columns", columnDefs); | 动态生成表格列、表单字段等 | java<br>ctx.put("apiUrl", "/api/" + tableName);<br>ctx.put("formItems", formFields); | 前端字段名应与后端保持一致,便于联调 |
| 输出到 src/views/ 目录 | 指定输出路径如 src/views/UserView.vue | 按项目结构组织生成文件 | java<br>File output = new File(<br> "src/views/" + className + "View.vue");<br>FileWriter fw = new FileWriter(output); | 需确保前端工程目录存在;建议按模块分目录生成 |
8.4 生成 SQL 脚本
| 方法/配置项名称 | 语法/配置方式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 定义建表模板(create_table.vm) | 编写 DDL 模板 | 自动生成 CREATE TABLE 语句 | sql<br>CREATE TABLE ${tableName} (<br>#foreach($field in $fields)<br> ${field.name} ${field.type}<br> #if($field.comment)<br> COMMENT '${field.comment}'<br> #end<br> #if($velocityHasNext),#end<br>#end<br>); | 注意数据库方言差异(MySQL vs PostgreSQL);字段类型需映射正确 |
| 注入主键、索引信息 | context.put("primaryKey", "id");context.put("indexes", indexList); | 支持生成主键和索引 | java<br>ctx.put("primaryKey", "user_id");<br>ctx.put("uniqueIndex",<br> Arrays.asList("email")); | 索引名避免重复;复合索引需特殊处理 |
| 输出为 .sql 文件 | 写入如 init_user.sql | 供 DBA 或初始化脚本使用 | java<br>FileWriter fw = new FileWriter(<br> "sql/init_" + tableName + ".sql");<br>template.merge(ctx, fw); | 文件名建议带时间戳或版本号;可用于 flyway/liquibase 集成 |
8.5 生成配置文件
| 方法/配置项名称 | 语法/配置方式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| application.yml.vm 模板 | 编写 YAML 格式模板 | 生成 Spring Boot 配置 | yaml<br>server:<br> port: ${serverPort}<br>spring:<br> datasource:<br> url: jdbc:mysql://localhost:3306/${dbName} | 注意 YAML 缩进必须严格;变量值需转义特殊字符 |
| 注入环境参数 | context.put("serverPort", 8080);context.put("dbName", "myapp_dev"); | 动态适配不同环境 | java<br>ctx.put("profile", "dev");<br>ctx.put("logLevel", "DEBUG"); | 敏感信息(如密码)不应硬编码在模板中,建议占位符+外部注入 |
| 生成多环境配置 | 循环生成 dev/test/prod 配置 | 一键生成整套配置 | java<br>for (String env :<br> Arrays.asList("dev", "test", "prod")) {<br> ctx.put("env", env);<br> // merge to<br> // application-${env}.yml<br>} | 文件命名需符合 Spring Profile 规范 |
8.6 生成 API 文档
| 方法/配置项名称 | 语法/配置方式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Swagger/OpenAPI 模板 | 编写 openapi.json.vm 或 swagger.yaml.vm | 自动生成 API 描述文档 | json<br>{<br> "paths": {<br> "/api/${entity}": {<br> "get": {<br> "summary": "List ${entity}"<br> }<br> }<br> }<br>} | 需遵循 OpenAPI 3.0 规范;路径、方法、参数结构要完整 |
| 注入接口元数据 | context.put("apis", apiList);包含 method, path, params, resp | 提供接口描述信息 | java<br>ApiInfo api = new ApiInfo();<br>api.setMethod("POST");<br>api.setPath("/users");<br>api.setRequestBody(userDtoClass);<br>ctx.put("apis", Arrays.asList(api)); | 参数类型、是否必填、示例值等需准确提供 |
| 输出为 markdown 或 HTML | 使用额外模板渲染文档 | 便于阅读或集成到 Wiki | java<br>Template mdTemplate =<br> ve.getTemplate("api_doc.md.vm");<br>mdTemplate.merge(ctx,<br> new FileWriter("docs/api.md")); | 可结合 GitBook、Docusaurus 等静态站点工具发布 |