FreeMarker 本身不提供传统意义上的“插件系统”(如 WordPress 或 IDE 插件),但其强大的 可扩展性 允许开发者通过多种方式实现“插件化”功能,以增强模板能力、集成外部服务或封装业务逻辑。
一、核心概念
在 FreeMarker 语境中,“插件”通常指以下几种可扩展机制:
概念 | 说明 |
---|---|
1. 自定义指令(Custom Directive) | 通过实现 TemplateDirectiveModel 接口,创建可在模板中使用的 #@myDirective param="value"/ 语法的指令。这是最强大、最常用的“插件”形式。 |
2. 自定义方法(Custom Method) | 实现 TemplateMethodModelEx 接口,创建可在模板中调用的函数,如 ${myMethod(arg1, arg2)} 。 |
3. 自定义内建函数(Custom Built-ins) | 扩展变量的 ? 操作符,如 ${user.name?myFilter} 。需通过 Configuration.setCustomAttribute() 或反射注入(较复杂,不推荐新手)。 |
4. 共享变量(Shared Variables) | 通过 Configuration.setSharedVariable("serviceName", serviceObject) 将 Java 对象暴露给所有模板使用,是最简单的“插件”注册方式。 |
5. 模板加载器(TemplateLoader) | 自定义模板来源(如数据库、网络、加密文件),实现 TemplateLoader 接口。 |
6. 宏(Macro)库 | 虽然不是 Java 插件,但可通过 .ftl 文件定义宏集合,实现模板层面的“插件”复用。 |
本指南重点:自定义指令(Custom Directive)和自定义方法(Custom Method),它们是实现业务插件的核心。
二、操作步骤(非常详细)
以下以 Maven + FreeMarker 2.3.32 + JUnit 5 为例,演示开发一个名为 highlight
的自定义指令插件。
步骤 1:创建 Maven 项目并添加依赖
pom.xml
:
<dependencies>
<!-- FreeMarker 核心 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
步骤 2:开发自定义指令插件
创建插件类 HighlightDirective.java
:
package com.example.plugin;
import freemarker.core.Environment;
import freemarker.template.*;
import java.io.IOException;
import java.util.Map;
/**
* 自定义指令:#@highlight color="yellow" text="Hello"/
* 或 <#@highlight color="yellow">Hello</@highlight>
*/
public class HighlightDirective implements TemplateDirectiveModel {
// 定义参数名常量
private static final String PARAM_COLOR = "color";
private static final String PARAM_TEXT = "text";
private static final String DEFAULT_COLOR = "yellow";
@Override
public void execute(Environment env,
Map params,
TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// 1. 解析参数
String color = DEFAULT_COLOR;
String text = null;
// 获取 color 参数
TemplateModel colorModel = (TemplateModel) params.get(PARAM_COLOR);
if (colorModel != null) {
if (colorModel instanceof TemplateScalarModel) {
color = ((TemplateScalarModel) colorModel).getAsString();
} else {
throw new TemplateModelException("The '" + PARAM_COLOR + "' parameter must be a string.");
}
}
// 获取 text 参数(可选)
TemplateModel textModel = (TemplateModel) params.get(PARAM_TEXT);
if (textModel != null) {
if (textModel instanceof TemplateScalarModel) {
text = ((TemplateScalarModel) textModel).getAsString();
} else {
throw new TemplateModelException("The '" + PARAM_TEXT + "' parameter must be a string.");
}
}
// 2. 处理嵌套内容(body)
if (text == null && body != null) {
// 如果未提供 text 参数,则使用指令体内容
StringWriter writer = new StringWriter();
body.render(writer);
text = writer.toString();
}
if (text == null) {
throw new TemplateModelException("Either 'text' parameter or directive body must be provided.");
}
// 3. 生成 HTML 高亮输出
String highlighted = String.format("<mark style='background-color:%s;'>%s</mark>", color, text);
// 4. 写入输出流
env.getOut().write(highlighted);
}
}
步骤 3:开发自定义方法插件(可选)
创建 StringLengthMethod.java
:
package com.example.plugin;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;
import java.util.List;
/**
* 自定义方法:${stringLength("hello")} -> 5
*/
public class StringLengthMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments == null || arguments.isEmpty()) {
throw new TemplateModelException("Missing argument for stringLength method.");
}
Object arg = arguments.get(0);
if (arg instanceof TemplateScalarModel) {
String str = ((TemplateScalarModel) arg).getAsString();
return str.length();
}
throw new TemplateModelException("Argument must be a string.");
}
}
步骤 4:注册插件到 FreeMarker 配置
创建测试类 PluginTest.java
:
package com.example;
import com.example.plugin.HighlightDirective;
import com.example.plugin.StringLengthMethod;
import freemarker.template.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class PluginTest {
private Configuration configuration;
@BeforeEach
void setUp() {
configuration = new Configuration(new Version(2, 3, 32));
configuration.setDefaultEncoding("UTF-8");
// === 注册插件(关键步骤)===
// 1. 注册自定义指令(使用 shared variable 方式)
configuration.setSharedVariable("highlight", new HighlightDirective());
// 2. 注册自定义方法
configuration.setSharedVariable("stringLength", new StringLengthMethod());
// 3. 也可以注册其他共享服务
// configuration.setSharedVariable("userService", new UserService());
}
@Test
void shouldUseHighlightDirective() throws Exception {
String templateContent = """
<p>Normal text.</p>
<p>Using param: <#@highlight color="pink" text="Important!"/></p>
<p>Using body: <#@highlight color="lightgreen">Very Important!</@highlight></p>
""";
// 使用 StringTemplateLoader 加载字符串模板(测试用)
StringTemplateLoader loader = new StringTemplateLoader();
loader.putTemplate("test.ftl", templateContent);
configuration.setTemplateLoader(loader);
Template template = configuration.getTemplate("test.ftl");
Writer out = new StringWriter();
template.process(new HashMap<>(), out);
String result = out.toString();
assertThat(result).contains("background-color:pink");
assertThat(result).contains("Important!");
assertThat(result).contains("background-color:lightgreen");
assertThat(result).contains("Very Important!");
}
@Test
void shouldUseStringLengthMethod() throws Exception {
String templateContent = "Length: ${stringLength('FreeMarker')}";
StringTemplateLoader loader = new StringTemplateLoader();
loader.putTemplate("method.ftl", templateContent);
configuration.setTemplateLoader(loader);
Template template = configuration.getTemplate("method.ftl");
Writer out = new StringWriter();
template.process(new HashMap<>(), out);
assertThat(out.toString()).isEqualTo("Length: 10");
}
}
步骤 5:在真实模板中使用插件
创建 src/main/resources/templates/page.ftl
:
<!DOCTYPE html>
<html>
<head><title>Plugin Demo</title></head>
<body>
<h1>Welcome</h1>
<p>Here is a <#@highlight color="gold">highlighted phrase</@highlight>.</p>
<p>The word "FreeMarker" has ${stringLength("FreeMarker")} letters.</p>
</body>
</html>
Java 代码加载并渲染:
// 生产环境:使用 FileTemplateLoader 或 ClassTemplateLoader
configuration.setDirectoryForTemplateLoading(new java.io.File("src/main/resources/templates"));
Template template = configuration.getTemplate("page.ftl");
Writer out = new StringWriter();
template.process(dataModel, out);
String html = out.toString();
三、常见错误
错误 | 原因 | 解决方案 |
---|---|---|
Unknown directive: highlight |
插件未注册或名称拼写错误 | 检查 setSharedVariable("name", plugin) 是否执行 |
The parameter must be a string |
传入了非字符串类型(如数字、对象) | 在插件中加强类型检查,或提供类型转换 |
No value for directive body |
忘记闭合指令 <#@highlight>...</@highlight> |
使用成对标签或自闭合 <#@highlight .../> |
TemplateModelException |
插件代码抛出异常 | 捕获并包装为 TemplateModelException ,提供清晰错误信息 |
ClassCastException |
未检查 TemplateModel 类型 |
务必使用 instanceof 判断类型 |
IOException 写入失败 |
env.getOut() 写入异常 |
通常由上层处理,插件中可捕获并包装 |
四、注意事项
- 线程安全:
TemplateDirectiveModel
和TemplateMethodModel
实例应在Configuration
中共享,必须是线程安全的。避免在插件类中使用实例变量存储状态。 - 异常处理:插件中抛出的异常应为
TemplateModelException
或TemplateException
,以便 FreeMarker 正确处理并定位错误模板行号。 - 参数验证:对输入参数进行严格校验(非空、类型、范围)。
- 命名规范:插件名避免与 FreeMarker 关键字冲突(如
if
,list
)。 - 性能敏感:避免在插件中进行耗时操作(如远程调用、大文件读取)。如需,应缓存结果或异步处理。
- HTML 转义:如果输出 HTML,注意是否需转义输入内容(除非明确信任)。可使用
?html
或fmpp
工具辅助。
五、使用技巧
- 封装复杂逻辑:将数据库查询、API 调用、复杂计算封装在插件中,模板只需调用。
- 使用
@env
获取上下文:Environment
对象可访问当前模板、命名空间、局部变量等。 - 支持多种参数模式:如
highlight
指令同时支持text
参数和指令体。 - 提供默认值:为可选参数设置合理默认值。
- 日志输出:在插件中使用 SLF4J 记录调试日志(需引入日志依赖)。
- 组合使用:一个指令可调用其他方法或指令。
六、最佳实践与性能优化
实践 | 说明 |
---|---|
✅ 单一职责 | 每个插件只做一件事(如 highlight 只负责高亮)。 |
✅ 易于测试 | 插件逻辑独立,可通过单元测试验证。 |
✅ 文档化 | 为插件编写文档,说明参数、用法、示例。 |
✅ 版本兼容 | 注意 FreeMarker 版本 API 变化,保持向后兼容。 |
✅ 错误信息友好 | 抛出异常时包含参数名、期望类型等信息。 |
⚡ 性能优化 | |
- 缓存计算结果:对耗时操作(如远程 API)结果缓存(注意过期策略)。 | @Cacheable 注解或手动缓存 |
- 避免重复解析:如解析 CSS 颜色名,可缓存映射表。 | static final Map |
- 轻量级实现:插件代码应简洁高效。 | |
- 异步加载(高级):对非关键资源,可返回占位符,由前端异步加载。 | 结合 JavaScript |
🔐 安全考虑 | |
- 输入验证与清理:防止 XSS、命令注入。 | 使用 ?html , ?js_string 等内建函数 |
- 权限控制(可选):在插件中检查用户权限。 | 结合 Spring Security 等 |
总结
FreeMarker “插件开发”本质是通过 自定义指令 和 自定义方法 扩展模板能力。核心步骤为:
- 实现
TemplateDirectiveModel
或TemplateMethodModelEx
接口 - 在
Configuration
中通过setSharedVariable
注册 - 在模板中使用
#@directive
或${method()}
调用
通过插件机制,您可以将复杂的业务逻辑、工具函数、外部服务集成到模板中,提升开发效率和模板表达力。遵循最佳实践,确保插件线程安全、健壮、高效、安全,是构建高质量 FreeMarker 应用的关键。