Apache Velocity 配置参数详解
解析 Apache Velocity 2.4.1 核心配置参数:覆盖资源加载器、编码、指令、宏、严格模式与日志六类配置,给出 Properties 与代码两种修改方式、新旧键名映射、逐项实测回显与可落地的生产配置模板。
Apache Velocity 配置参数详解
一、环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS(CentOS 8 实测行为一致) |
| 登录账号 | root |
| JDK | OpenJDK 17.0.20.1 |
| Maven | 3.6.3 |
| 组件 | velocity-engine-core 2.4.1 |
| 辅助依赖 | slf4j-simple 1.7.36、velocity-tools-generic 3.1 |
| 测试工程 | /root/velocity-demo(Maven 工程) |
二、前置条件
Velocity 无独立配置文件服务,配置通过 VelocityEngine 的 Properties 或 setProperty 传入。本篇基于以下测试工程展开。
工程 /root/velocity-demo/pom.xml(关键字段):
<dependencies>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-generic</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
测试模板与配置文件:
mkdir -p /root/velocity-demo/src/main/resources/templates
mkdir -p /etc/velocity-demo /opt/velocity-tpl /var/log/velocity
依次创建模板文件。/root/velocity-demo/src/main/resources/templates/macros.vm:
#macro(greet $who)
你好,$who!欢迎使用 Apache Velocity。
#end
/root/velocity-demo/src/main/resources/templates/strict-if.vm:
#if($undefinedRef)
条件为真
#else
未定义引用在条件指令中按 false 处理
#end
/root/velocity-demo/src/main/resources/templates/strict-ref.vm:
直接输出未定义引用:[$undefinedRef]
静默引用:[$!undefinedQuiet]
/root/velocity-demo/src/main/resources/templates/strict-quiet.vm:
静默引用(严格模式下):[$!undefinedQuiet]
/root/velocity-demo/src/main/resources/templates/items.vm:
#foreach($item in $items)
第 $foreach.count 件:$item.name
#end
/opt/velocity-tpl/hello-file.vm(FileResourceLoader 测试用,磁盘目录模板):
你好,张三!这是来自磁盘模板目录 /opt/velocity-tpl 的中文渲染。
三、参数查看与修改方式
3.1 代码方式(setProperty)
引擎初始化前逐项设置,适合配置项少的场景:
VelocityEngine engine = new VelocityEngine();
engine.setProperty("resource.loaders", "classpath");
engine.setProperty("resource.loader.classpath.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
engine.setProperty("resource.default_encoding", "UTF-8");
engine.init();
3.2 文件方式(Properties)
将配置写入独立文件,运行时加载。本篇测试程序从 /etc/velocity-demo/velocity-demo.properties 读取:
Properties props = new Properties();
try (FileInputStream in = new FileInputStream("/etc/velocity-demo/velocity-demo.properties")) {
props.load(in);
}
VelocityEngine engine = new VelocityEngine();
engine.setProperties(props);
engine.init();
3.3 配置验证程序
完整验证程序 /root/velocity-demo/src/main/java/com/example/ConfigDemo.java,依次验证严格模式、循环上限、文件加载器、错误路径与日志行为:
package com.example;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.VelocityException;
import java.io.File;
import java.io.FileInputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
public class ConfigDemo {
private static VelocityEngine classpathEngine(Properties props) {
VelocityEngine engine = new VelocityEngine();
engine.setProperties(props);
engine.init();
return engine;
}
private static Map<String, Object> item(String name) {
Map<String, Object> item = new HashMap<>();
item.put("name", name);
return item;
}
public static void main(String[] args) throws Exception {
String propsPath = args.length > 0 ? args[0] : "/etc/velocity-demo/velocity-demo.properties";
Properties props = new Properties();
try (FileInputStream in = new FileInputStream(propsPath)) {
props.load(in);
}
System.out.println("已加载配置(" + propsPath + "):directive.foreach.max_loops="
+ props.getProperty("directive.foreach.max_loops", props.getProperty("directive.foreach.maxloops"))
+ ",runtime.strict_mode.enable=" + props.getProperty("runtime.strict_mode.enable",
props.getProperty("runtime.references.strict")));
VelocityContext ctx = new VelocityContext();
render(classpathEngine(props), ctx, "templates/strict-if.vm");
try {
render(classpathEngine(props), ctx, "templates/strict-ref.vm");
} catch (VelocityException e) {
System.out.println("[捕获异常] " + e.getClass().getName());
System.out.println("[异常消息] " + e.getMessage().split("\n")[0]);
}
try {
render(classpathEngine(props), ctx, "templates/strict-quiet.vm");
} catch (VelocityException e) {
System.out.println("[捕获异常] " + e.getClass().getName());
}
Properties noStrict = new Properties();
noStrict.putAll(props);
noStrict.setProperty("runtime.strict_mode.enable", "false");
VelocityContext listCtx = new VelocityContext();
List<Map<String, Object>> items = new ArrayList<>();
items.add(item("商品1"));
items.add(item("商品2"));
items.add(item("商品3"));
listCtx.put("items", items);
render(classpathEngine(noStrict), listCtx, "templates/items.vm");
VelocityEngine fileEngine = new VelocityEngine();
fileEngine.setProperty("resource.loaders", "file");
fileEngine.setProperty("resource.loader.file.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
fileEngine.setProperty("resource.loader.file.path", "/opt/velocity-tpl");
fileEngine.setProperty("resource.default_encoding", "UTF-8");
fileEngine.init();
render(fileEngine, ctx, "hello-file.vm");
VelocityEngine badEngine = new VelocityEngine();
badEngine.setProperty("resource.loaders", "file");
badEngine.setProperty("resource.loader.file.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
badEngine.setProperty("resource.loader.file.path", "/no/such/dir");
badEngine.init();
try {
render(badEngine, ctx, "hello-file.vm");
} catch (VelocityException e) {
System.out.println("[捕获异常] " + e.getClass().getName());
System.out.println("[异常消息] " + e.getMessage().split("\n")[0]);
}
File logFile = new File("/var/log/velocity/velocity-demo.log");
if (logFile.exists()) {
System.out.println("---- /var/log/velocity/velocity-demo.log 已生成,前 5 行 ----");
Files.readAllLines(logFile.toPath(), StandardCharsets.UTF_8)
.stream().limit(5).forEach(System.out::println);
} else {
System.out.println("runtime.log 文件未生成:classpath 存在 SLF4J 绑定时,"
+ "velocity-engine-core 2.4.1 日志由 SLF4J 输出,runtime.log 被忽略");
}
}
private static void render(VelocityEngine engine, VelocityContext ctx, String path) {
Template template = engine.getTemplate(path, "UTF-8");
StringWriter writer = new StringWriter();
template.merge(ctx, writer);
System.out.println("---- " + path + " 渲染结果 ----");
System.out.println(writer);
}
}
编译并运行:
cd /root/velocity-demo && mvn -q clean compile
cd /root/velocity-demo && LANG=C.UTF-8 mvn -q exec:java -Dexec.mainClass=com.example.ConfigDemo -Dexec.args=/etc/velocity-demo/velocity-demo-new2.properties
四、参数总览
| 参数(2.4.1 现行名) | 默认值 | 作用 |
|---|---|---|
| resource.loaders | file | 启用的资源加载器列表,可多值逗号分隔 |
| resource.loader.classpath.class | 无 | 指定加载器实现类,classpath 名可自定 |
| resource.loader.file.path | 当前目录 | FileResourceLoader 的模板根目录 |
| resource.default_encoding | UTF-8 依赖平台 | 模板读取与输出统一编码 |
| velocimacro.library.path | VM_global_library.vm | 启动时加载的宏库路径 |
| directive.foreach.max_loops | 无限制 | foreach 单次渲染最大循环次数 |
| runtime.strict_mode.enable | false | 严格引用模式开关 |
| runtime.log | 已废弃 | 2.x 日志由 SLF4J 接管,该键无效 |
| input.encoding、output.encoding | 已废弃 | 由 resource.default_encoding 取代 |
| velocimacro.library | 已废弃 | 由 velocimacro.library.path 取代 |
| directive.foreach.maxloops | 已废弃 | 由 directive.foreach.max_loops 取代 |
| runtime.references.strict | 已废弃 | 由 runtime.strict_mode.enable 取代 |
五、资源加载器参数详解
5.1 classpath 加载器
从应用 classpath 读取模板,模板文件放在 src/main/resources 下随代码一起编译打包:
engine.setProperty("resource.loaders", "classpath");
engine.setProperty("resource.loader.classpath.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
要点:getTemplate 的路径相对 classpath 根,子目录要写全,例如模板位于 resources/templates/main.vm 时应写 templates/main.vm;模板内部的 #include 与 #parse 同样按全路径引用。
5.2 file 加载器
从磁盘目录读取模板,适合模板需要热更新或不打包进应用的场景:
engine.setProperty("resource.loaders", "file");
engine.setProperty("resource.loader.file.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
engine.setProperty("resource.loader.file.path", "/opt/velocity-tpl");
实测渲染 /opt/velocity-tpl/hello-file.vm 回显:
---- hello-file.vm 渲染结果 ----
你好,张三!这是来自磁盘模板目录 /opt/velocity-tpl 的中文渲染。
5.3 路径错误行为
file.path 指向不存在目录时,引擎启动不报错,渲染时抛资源未找到异常。实测 file.path 配为 /no/such/dir 的回显:
[main] ERROR org.apache.velocity.loader - ResourceManager: unable to find resource 'hello-file.vm' in any resource loader.
[捕获异常] org.apache.velocity.exception.ResourceNotFoundException
[异常消息] Unable to find resource 'hello-file.vm'
六、编码参数详解
6.1 resource.default_encoding
统一控制模板读取与输出编码,取代 1.x 与 2.x 早期的 input.encoding、output.encoding:
resource.default_encoding = UTF-8
6.2 与系统 locale 的关系
resource.default_encoding 只管模板内容,程序自身打印中文还取决于 JVM 的 file.encoding。POSIX locale 环境实测 file.encoding 为 ANSI_X3.4-1968,控制台中文会变问号,需将系统 locale 设为 C.UTF-8:
LANG 未设置:file.encoding = ANSI_X3.4-1968
LANG=C.UTF-8:file.encoding = UTF-8
七、指令参数详解
7.1 directive.foreach.max_loops
限制单次 foreach 渲染的最大循环次数,防御脏数据导致的超长循环。配置文件中设为 2,数据模型放 3 条记录,实测渲染只输出前 2 条:
---- templates/items.vm 渲染结果 ----
第 1 件:商品1
第 2 件:商品2
第 3 条被静默截断,不抛异常,符合上限保护的设计预期。
八、宏参数详解
8.1 velocimacro.library.path
指定启动时加载的宏库文件,多个文件用逗号分隔。宏库放在 classpath 加载器根路径下时按相对路径写:
velocimacro.library.path = templates/macros.vm
配置生效后,模板内直接调用宏,实测输出:
你好,张三!欢迎使用 Apache Velocity。
你好,李四!欢迎使用 Apache Velocity。
不配置该键时,调用未定义宏不会报错,宏调用语句按原样输出到渲染结果,这是排查“模板出现未执行指令”时的高频原因。
九、严格模式参数详解
9.1 runtime.strict_mode.enable
开启后引用未定义变量将抛异常,用于上线前暴露模板拼写错误:
runtime.strict_mode.enable = true
9.2 条件指令中的未定义引用
#if 条件中的未定义引用不抛异常,按 false 处理,走 #else 分支,实测回显:
---- templates/strict-if.vm 渲染结果 ----
未定义引用在条件指令中按 false 处理
9.3 输出位置的未定义引用
直接输出未定义引用抛 MethodInvocationException,实测回显:
[main] ERROR org.apache.velocity.rendering - Variable $undefinedRef has not been set at templates/strict-ref.vm[line 1, column 12]
[捕获异常] org.apache.velocity.exception.MethodInvocationException
[异常消息] Variable $undefinedRef has not been set at templates/strict-ref.vm[line 1, column 12]
9.4 静默引用的行为
注意 2.4.1 实测行为:即使写成静默引用形式,严格模式下同样抛异常,并不会静默返回空:
[main] ERROR org.apache.velocity.rendering - Variable $undefinedQuiet has not been set at templates/strict-quiet.vm[line 1, column 14]
[捕获异常] org.apache.velocity.exception.MethodInvocationException
因此严格模式适合在测试环境开启,与脏数据共存的线上模板慎用。
十、日志参数详解
10.1 runtime.log 已废弃
2.4.1 的日志强制走 SLF4J:classpath 缺少 slf4j-api 时引擎初始化直接失败,实测异常:
Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
at org.apache.velocity.runtime.RuntimeInstance.<init>(RuntimeInstance.java:121)
classpath 只有 slf4j-api 没有绑定时,日志进入 SLF4J 空实现并被吞掉,配置 runtime.log 也不会生成文件,实测日志目录始终为空。
10.2 用 SLF4J 绑定落盘日志
推荐引入 slf4j-simple,并用系统属性指定日志文件:
cd /root/velocity-demo && LANG=C.UTF-8 java -Dorg.slf4j.simpleLogger.logFile=/var/log/velocity/velocity-demo.log -cp 'target/classes:libs/*' com.example.ConfigDemo
实测日志文件生成(1718 字节),内容节选:
[main] WARN org.apache.velocity.deprecation - configuration key 'runtime.references.strict' has been deprecated in favor of 'runtime.strict_mode.enable'
[main] ERROR org.apache.velocity.rendering - Variable $undefinedRef has not been set at templates/strict-ref.vm[line 1, column 12]
生产环境也可换用 logback 等绑定,Velocity 日志会自动并入应用统一日志体系。
十一、弃用键名新旧映射
2.4.1 对旧键名逐项打印 WARN 且功能保持兼容。旧式配置文件实测回显:
[main] WARN org.apache.velocity.deprecation - configuration key 'runtime.references.strict' has been deprecated in favor of 'runtime.strict_mode.enable'
[main] WARN org.apache.velocity.deprecation - configuration key 'input.encoding' has been deprecated in favor of 'resource.default_encoding'
[main] WARN org.apache.velocity.deprecation - configuration key 'directive.foreach.maxloops' has been deprecated in favor of 'directive.foreach.max_loops'
[main] WARN org.apache.velocity.deprecation - configuration key 'velocimacro.library' has been deprecated in favor of 'velocimacro.library.path'
映射汇总:
| 旧键名 | 新键名 |
|---|---|
| input.encoding | resource.default_encoding |
| output.encoding | resource.default_encoding |
| velocimacro.library | velocimacro.library.path |
| runtime.references.strict | runtime.strict_mode.enable |
| directive.foreach.maxloops | directive.foreach.max_loops |
| runtime.log | 无对应,日志由 SLF4J 接管 |
将配置全部换成新键名后重新运行,实测弃用告警数量为 0。
十二、生产配置模板
以下模板适用于以 jar 包部署、模板随应用打包的渲染服务,内存按 256 至 512MB 起步(堆参数在启动命令 -Xms 与 -Xmx 上设置),磁盘仅需容纳模板文件与日志:
# 资源加载器:classpath 为主
resource.loaders = classpath
resource.loader.classpath.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
# 编码
resource.default_encoding = UTF-8
# 宏库
velocimacro.library.path = templates/macros.vm
# 循环保护:按业务最大清单量调整
directive.foreach.max_loops = 1000
# 严格模式:测试环境 true,按模板成熟度灰度
runtime.strict_mode.enable = false
模板需要热更新时增加 file 加载器:
resource.loaders = classpath,file
resource.loader.classpath.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
resource.loader.file.class = org.apache.velocity.runtime.resource.loader.FileResourceLoader
resource.loader.file.path = /opt/velocity-tpl
十三、常见问题
Q1:配置了 runtime.log 为什么始终没有日志文件? A:2.4.1 日志强制走 SLF4J,runtime.log 已废弃。classpath 有绑定时日志按绑定配置输出,可用 -Dorg.slf4j.simpleLogger.logFile 指定文件;完全没有 slf4j-api 时引擎初始化抛 NoClassDefFoundError。
Q2:模板里的宏调用被原样输出怎么办? A:宏库没有加载。检查 velocimacro.library.path 是否指向实际存在的宏库文件、路径是否相对正确的加载器根目录,修改后重新编译打包再验证。
Q3:foreach 数据量超过配置上限会报错吗? A:不会。实测 max_loops 设为 2 时 3 条数据只渲染前 2 条,超出部分被静默截断,业务上需确认截断是否可接受或调大上限。
Q4:出现 Lexical error, Encountered 之类解析异常是什么原因? A:模板正文里出现了形如 #if、#end 的裸指令词片段(例如在说明文字里写了指令名),词法分析器将其当作未闭合指令。修改正文措辞或用 #* 与 *# 注释包裹该字样即可。
十四、参考链接
- Velocity 引擎开发者文档(配置章节):https://velocity.apache.org/engine/2.4/developer-guide.html
- 2.x 默认配置键说明:https://velocity.apache.org/engine/2.4/configuration.html
- VTL 参考指南:https://velocity.apache.org/engine/2.4/vtl-reference.html
- velocity-engine-core 2.4.1(Maven Central):https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core