Velocity 代码生成
学习目标
- 能配置 Velocity 依赖、模板目录和 UTF-8 编码。
- 能把
VelocityEngine、VelocityContext、Template与merge串成完整渲染流程。 - 能限制模板来源、上下文能力和输出路径,避免模板注入与任意文件覆盖。
核心知识点
Velocity 把稳定文本结构放进模板,把变化数据放进上下文,最后由模板引擎合并为字符串或文件。本文使用 org.apache.velocity:velocity-engine-core:2.4.1;Maven 坐标如下:
xml
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.4.1</version>
</dependency>Gradle(Groovy DSL)使用同一版本:
groovy
dependencies {
implementation 'org.apache.velocity:velocity-engine-core:2.4.1'
}下文约定模板根目录是应用随包发布且经过代码审查的 templates/,模板名由代码选择而不是由请求参数决定。上下文只放字符串、数字、DTO 和只读集合,不放数据库连接、Spring 容器、类加载器或反射入口。
常用用法
Properties():创建引擎配置集
Properties 的空构造器创建可变的字符串键值集;这里的 config 专门承载 Velocity 资源加载器和编码选项,然后整体传给 VelocityEngine。
java
// 说明:Properties():创建引擎配置集。
import java.util.Properties;
Properties config = new Properties();
// 初始状态:config = new Properties()。
// 作用:Properties config = new Properties();;创建引擎配置集。
config.setProperty("resource.loader.file.path", "templates");
// 初始状态:config.setProperty("resource.loader.file.path", "templates")。
// 作用:config.setProperty("resource.loader.file.path", "templates");;创建引擎配置集。
// config 中的 templates 是引擎随后查找 .vm 文件的受信任根目录。
System.out.println(config.getProperty("resource.loader.file.path"));
// 输出:templatesVelocityEngine.init:初始化文件资源加载器
用途:用于显式指定受信任模板目录、UTF-8 编码并完成引擎初始化。
java
// 说明:VelocityEngine.init:初始化文件资源加载器。
import java.util.Properties;
import org.apache.velocity.app.VelocityEngine;
public class VelocityInitDemo {
public static void main(String[] args) {
Properties config = new Properties();
// 作用:Properties config = new Properties();;初始化文件资源加载器。
// 初始状态:config = new Properties()。
// config 限定 engine 只从 templates 目录按 UTF-8 解码模板。
config.setProperty("resource.loaders", "file");
// 作用:config.setProperty("resource.loaders", "file");;初始化文件资源加载器。
config.setProperty("resource.loader.file.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
// 作用:config.setProperty("resource.loader.file.class",;初始化文件资源加载器。
config.setProperty("resource.loader.file.path", "templates");
// 作用:config.setProperty("resource.loader.file.path", "templates");;初始化文件资源加载器。
config.setProperty("resource.default_encoding", "UTF-8");
// 作用:config.setProperty("resource.default_encoding", "UTF-8");;初始化文件资源加载器。
VelocityEngine engine = new VelocityEngine(config);
// 作用:VelocityEngine engine = new VelocityEngine(config);;初始化文件资源加载器。
// 初始状态:engine = new VelocityEngine(config)。
// init() 根据 config 创建文件资源加载器,之后 engine 才能查找模板。
engine.init();
// 初始状态:engine.init()。
// 作用:engine.init();;初始化文件资源加载器。
System.out.println("Velocity ready");
// 输出:Velocity ready
}
}VelocityContext.put:提供最小模板数据
用途:用于按稳定键名写入模板真正需要的数据,形成可测试的输入契约。
java
// 说明:VelocityContext.put:提供最小模板数据。
import java.util.List;
import org.apache.velocity.VelocityContext;
public class VelocityContextDemo {
public static void main(String[] args) {
List<String> fields = List.of("id", "name");
// 作用:List<String> fields = List.of("id", "name");;提供最小模板数据。
// 初始状态:fields = List.of("id", "name")。
// fields 保留字段顺序,模板中的 #foreach 会按 id、name 的顺序迭代。
VelocityContext context = new VelocityContext();
// 作用:VelocityContext context = new VelocityContext();;提供最小模板数据。
// 初始状态:context = new VelocityContext()。
context.put("packageName", "example.user");
// 初始状态:context.put("packageName", "example.user")。
// 作用:context.put("packageName", "example.user");;提供最小模板数据。
context.put("className", "UserView");
// 初始状态:context.put("className", "UserView")。
// 作用:context.put("className", "UserView");;提供最小模板数据。
context.put("fields", fields);
// 初始状态:context.put("fields", fields)。
// 作用:context.put("fields", fields);;提供最小模板数据。
// context 只暴露模板使用的三个键,不向模板传递文件系统或反射能力。
System.out.println(context.get("className"));
// 输出:UserView
}
}Template.merge:把上下文渲染为文本
用途:用于将已加载模板与上下文合并到 writer,并在落盘前检查生成结果。
java
// 说明:Template.merge:把上下文渲染为文本。
import java.io.StringWriter;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
public class VelocityMergeDemo {
static String render(Template template) {
VelocityContext context = new VelocityContext();
// 作用:VelocityContext context = new VelocityContext();;把上下文渲染为文本。
// 初始状态:context = new VelocityContext()。
context.put("className", "UserService");
// 作用:context.put("className", "UserService");;把上下文渲染为文本。
// 模板中的 $className 会从 context 取得 UserService。
StringWriter writer = new StringWriter();
// 作用:StringWriter writer = new StringWriter();;把上下文渲染为文本。
// 初始状态:writer = new StringWriter()。
// merge() 执行 template 并把结果写入内存 writer,此时还没有覆盖任何文件。
template.merge(context, writer);
// 初始状态:template.merge(context, writer)。
// 作用:template.merge(context, writer);;把上下文渲染为文本。
String source = writer.toString();
// 作用:String source = writer.toString();;把上下文渲染为文本。
// 初始状态:source = writer.toString()。
System.out.println(source.contains("UserService"));
// 输出:模板使用 $className 时为 true
return source;
}
}不常用但需要知道
VelocityEngine.getTemplate:按 UTF-8 加载受信任模板
用途:用于从初始化时配置的资源目录加载固定模板名,并返回可重复合并的模板对象。
java
// 作用:通过 VelocityEngine.getTemplate 按 UTF-8 加载受信任模板。
import java.nio.charset.StandardCharsets;
import org.apache.velocity.Template;
import org.apache.velocity.app.VelocityEngine;
public class VelocityLoadDemo {
static Template load(VelocityEngine engine) {
String templateName = "java/model.vm";
// 初始状态:templateName = "java/model.vm"。
// templateName 是代码选定的相对路径,不接受请求参数拼接的 ../。
Template template = engine.getTemplate(
templateName,
StandardCharsets.UTF_8.name());
// 初始状态:template = engine.getTemplate(。
// 作用:Template template = engine.getTemplate(;按 UTF-8 加载受信任模板,返回读取结果。
// template 来自 engine 配置的 templates 根目录,并以 UTF-8 解码。
System.out.println(template.getName());
// 输出:java/model.vm
return template;
}
}简单案例
在受信任目录创建 templates/java/model.vm:
text
package $packageName;
public record $className(long id, String name) {}下面的完整程序初始化引擎、加载模板、准备上下文并合并;四个对象使用同一组变量和模板名,因此示例可以直接串起来理解。
java
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
public class GenerateModelDemo {
public static void main(String[] args) {
Properties config = new Properties();
// 作用:Properties config = new Properties();;按 UTF-8 加载受信任模板。
// 初始状态:config = new Properties()。
// config 把 engine 的模板根目录固定为 templates。
config.setProperty("resource.loaders", "file");
// 作用:config.setProperty("resource.loaders", "file");;按 UTF-8 加载受信任模板。
config.setProperty("resource.loader.file.path", "templates");
// 作用:config.setProperty("resource.loader.file.path", "templates");;按 UTF-8 加载受信任模板。
config.setProperty("resource.default_encoding", "UTF-8");
// 作用:config.setProperty("resource.default_encoding", "UTF-8");;按 UTF-8 加载受信任模板。
VelocityEngine engine = new VelocityEngine(config);
// 作用:VelocityEngine engine = new VelocityEngine(config);;按 UTF-8 加载受信任模板。
// 初始状态:engine = new VelocityEngine(config)。
engine.init();
// 作用:engine.init();;按 UTF-8 加载受信任模板。
VelocityContext context = new VelocityContext();
// 作用:VelocityContext context = new VelocityContext();;按 UTF-8 加载受信任模板。
// 初始状态:context = new VelocityContext()。
context.put("packageName", "example.user");
// 作用:context.put("packageName", "example.user");;按 UTF-8 加载受信任模板。
context.put("className", "UserView");
// 作用:context.put("className", "UserView");;按 UTF-8 加载受信任模板。
// context 为 model.vm 中的 $packageName 和 $className 提供确定值。
Template template = engine.getTemplate("java/model.vm", StandardCharsets.UTF_8.name());
// 初始状态:template = engine.getTemplate("java/model.vm", StandardCharsets.UTF_8.name())。
// 作用:Template template = engine.getTemplate("java/model.vm", StandardCharsets.UTF_8.name());;按 UTF-8 加载受信任模板,返回读取结果。
StringWriter writer = new StringWriter();
// 作用:StringWriter writer = new StringWriter();;按 UTF-8 加载受信任模板。
// 初始状态:writer = new StringWriter()。
// template 从 engine 的受信任目录加载;merge 只把渲染结果写入 writer。
template.merge(context, writer);
// 作用:template.merge(context, writer);;按 UTF-8 加载受信任模板。
System.out.println(writer.toString().replace(System.lineSeparator(), " ").trim());
// 输出:package example.user; public record UserView(long id, String name) {}
}
}易混点
VelocityContext.put不会替 Java、SQL、HTML 或路径做转义;应按目标语言校验标识符和文本。Template.merge成功只表示渲染完成,不表示生成的 Java 能编译或 SQL 可以安全执行。- 模板目录和模板名必须来自配置或代码白名单;不要允许外部输入拼接
../选择任意文件。 - 若要落盘,应先规范化目标路径并确认它仍位于允许的生成根目录,再明确拒绝覆盖或采用原子替换。
- 要求输出稳定时,应给模板传入有稳定顺序的
List或LinkedHashMap。
课后小问
- 为什么不能把数据库连接或 Spring 容器放进上下文? 答:模板只需要数据,能力对象会扩大模板可调用面,也会让模板难以隔离测试和审计。
- 为什么合并后还应编译或解析生成物? 答:模板语法正确不代表目标语言正确,编译或解析能发现缺值、非法标识符和结构错误。
- 如何避免模板或生成文件越过允许目录? 答:模板名使用代码白名单,目标路径规范化后校验仍在固定根目录内,并拒绝未经授权的覆盖。
本节小结
Velocity 代码生成的主线是:用固定版本依赖创建 VelocityEngine,从受信任资源目录取得 Template,用 VelocityContext.put 提供最小数据,再由 Template.merge 写入内存并校验结果。模板、输入、生成内容和落盘路径都需要独立边界,生成物仍应进入格式化、编译、测试与代码审查。
阿源的知识库