Article

模板引擎 Velocity

更新于:2026-07-15

第一章:入门基础

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>&lt;dependency&gt;<br> &lt;groupId&gt;org.apache.velocity&lt;/groupId&gt;<br> &lt;artifactId&gt;velocity-engine-core&lt;/artifactId&gt;<br> &lt;version&gt;2.3&lt;/version&gt;<br>&lt;/dependency&gt;<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+)。
手动下载 JARhttps://velocity.apache.org/download.cgi 下载 velocity-engine-core-x.x.jar 并加入 classpath。需同时引入依赖项(如 commons-lang3slf4j-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", "&lt;p&gt;Hello, $name!&lt;/p&gt;");<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,当 namenull 时输出空字符串而非 $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 #endcondition 支持比较(==, !=, <, >)、逻辑(&&, ||, !
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空字符串、0null 均视为 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 对象

方法/组件语法/代码示例用途注意事项
传入 POJOctx.put("user", new User("Bob", 25));将自定义对象暴露给模板对象需有 public getter 方法(如 getName()
传入集合ctx.put("items", Arrays.asList("A", "B"));传递 List、Set、Array 等用于 #foreachMap 也可直接传入,模板中可遍历 keySet 或 entrySet
传入 Mapjava<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)
传入 nullctx.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,但需显式配置
EscapeToolHTML/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 或使用 ToolManagerjava<br>ToolManager tm = new ToolManager();<br>Context ctx = tm.createContext();自动注入所有注册工具推荐使用 ToolManager 简化管理
依赖引入Maven:提供常用工具类实现xml<br>&lt;dependency&gt;<br> &lt;groupId&gt;org.apache.velocity.tools&lt;/groupId&gt;<br> &lt;artifactId&gt;velocity-tools-generic&lt;/artifactId&gt;<br> &lt;version&gt;3.1&lt;/version&gt;<br>&lt;/dependency&gt;<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.Directivepublic class AlertDirective extends Directive { ... }velocity.properties 中注册:
userdirective = com.example.AlertDirective
必须提供无参构造函数
指令名称重写 getName() 方法@Override public String getName() { return "alert"; }模板中通过 #alert() 调用名称不能与内置指令冲突(如 if、set)
指令类型重写 getType() 返回 LINEBLOCK@Override public int getType() { return BLOCK; }BLOCK 型需配对 #end,如 #alert()...#endLINE 型单行结束,如 #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 = file
file.resource.loader.class = org.apache.velocity.runtime.resource.loader.FileResourceLoader
file.resource.loader.path = /opt/templates
开发阶段常用路径为绝对或相对(相对于启动目录)
classpath从 classpath 加载(JAR 内)resource.loader = class
class.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.JarResourceLoader
jar.resource.loader.path = jar:file:/lib/tpls.jar
适用于插件化模板路径格式必须为 jar:file:...
url从 HTTP/HTTPS 加载(不推荐)url.resource.loader.class = org.apache.velocity.runtime.resource.loader.URLResourceLoader
url.resource.loader.root = http://example.com/templates/
远程模板(高延迟、不可靠)仅用于特殊场景,需处理超时和缓存
多加载器按顺序尝试多个加载器resource.loader = file, class(先 file 后 class)实现 fallback 机制加载器名称(如 file)需与 .resource.loader.class 前缀一致
缓存控制是否缓存模板内容file.resource.loader.cache = true
file.resource.loader.modificationCheckInterval = 2
开发时设 cache=false,生产设 truemodificationCheckInterval 单位为秒(仅当 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.cachetrue是否缓存从 classpath 加载的模板class.resource.loader.cache = false开发时建议关闭缓存以便热更新
input.encodingISO-8859-1模板文件读取编码input.encoding = UTF-8必须与模板文件实际编码一致
output.encodingISO-8859-1模板输出编码output.encoding = UTF-8Web 应用中应设为 UTF-8
runtime.logvelocity.log日志文件路径runtime.log = /logs/velocity.log可设为 null 禁用日志
velocimacro.library全局宏库文件路径(可多个)velocimacro.library = macros.vm,common.vm路径相对于资源加载器根目录
velocimacro.permissions.allow.inlinetrue是否允许在模板中定义宏velocimacro.permissions.allow.inline = false生产环境可禁用以提升安全
parser.pool.size20AST 解析器池大小parser.pool.size = 50高并发场景可适当增大
directive.foreach.counter.namevelocityCount#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-8Web 场景需与 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 ResourceBundlectx.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 使用
上下文隔离每个请求使用独立 VelocityContextnew 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 或自定义 Actionstruts-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 应用启动时初始化 VelocityEnginejava<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 应用根目录或 classpathve.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, ASTReferenceAST 是后续执行的中间表示
指令注册绑定将 AST 中的指令节点(如 #set)关联到对应的 Directive 实例RuntimeServices.getDirective(String name)内置指令(#if, #foreach)和用户自定义指令均在此绑定
上下文合并遍历 AST 节点,结合 VelocityContext 执行指令并输出文本SimpleNode.render()渲染过程是递归遍历 AST 并写入 Writer
输出生成最终文本写入目标 Writer(如 StringWriterServletOutputStreamWriter 接口编码由 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, #elseif 的分支节点作为 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)执行机制

机制环节说明核心方法 / 类注意事项
指令注册启动时加载内置指令 + 用户配置的 userdirectiveDirectiveFactory
RuntimeInitializer.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 LINEBLOCK 型指令需处理 #end 及内部 bodygetType() == BLOCKBLOCK 指令的 node 包含子节点(body)
参数获取通过 node.jjtGetChild(i) 获取 AST 子节点NodeUtils.tokenLiteral(node.jjtGetChild(0))字面量参数需转义处理
上下文操作可通过 context 修改变量(如 #setcontext.put("key", value)修改仅在当前上下文有效
异常处理抛出 IOExceptionMethodInvocationException异常会中断渲染并向上抛出
执行顺序按 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 EngineMaven / 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
初始化 VelocityEngineJava 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.vmcomponent.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.vmswagger.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使用额外模板渲染文档便于阅读或集成到 Wikijava<br>Template mdTemplate =<br> ve.getTemplate("api_doc.md.vm");<br>mdTemplate.merge(ctx,<br> new FileWriter("docs/api.md"));可结合 GitBook、Docusaurus 等静态站点工具发布