FreeMarker 是基於 Java 的模板引擎,專為生成文本輸出(HTML、XML、Email、代碼、配置)設計。遵循 MVC 模式:Java 準備數據模型,FreeMarker 負責視圖渲染。
org.freemarker
freemarker
2.3.32
${variable} // 基本輸出
${variable!default} // 空值預設
${variable?string} // 內建轉字符串
${variable?c} // 數字分組格式 (1,234)
${variable?html} // HTML 轉義
<#if condition>
...
<#elseif condition2>
...
<#else>
...
#if>
<#if list?? && list?size > 0> ... #if> // 非空檢查
<#list items as item>
${item_index} - ${item}
<#if item_has_next>,#if>
#list>
<#list map?keys as key>
${key} = ${map[key]!"null"}
#list>
<#include "header.ftl">
<#import "lib.ftl" as mylib>
<@mylib.copyright date="2024"/>
<@myDirective param1="value" param2=123>
內容體
@myDirective>
${title}
${title}
描述: ${description}
用戶列表
<#list users as user>
${user_index + 1}. ${user.name}
<#if user.role == "admin">
管理員
#if>
#list>
<#include "footer.ftl">
public class StaticPageGenerator {
private final Configuration cfg;
public StaticPageGenerator(String templateDir) throws IOException {
cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDirectoryForTemplateLoading(new File(templateDir));
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
cfg.setWrapUncheckedExceptions(true);
cfg.setFallbackOnNullLoopVariable(false);
}
public void generate(String templateName, Map data, File outputFile)
throws IOException, TemplateException {
Template template = cfg.getTemplate(templateName);
try (Writer out = new OutputStreamWriter(new FileOutputStream(outputFile), "UTF-8")) {
template.process(data, out);
}
}
// 使用範例
public static void main(String[] args) throws Exception {
StaticPageGenerator gen = new StaticPageGenerator("templates");
Map data = new HashMap<>();
data.put("title", "用戶報表");
data.put("description", "2024 年度用戶統計");
data.put("users", Arrays.asList(
new User("張三", "admin"),
new User("李四", "user"),
new User("王五", "user")
));
gen.generate("report.ftl", data, new File("output/report.html"));
System.out.println("靜態頁生成成功!");
}
}
public class PaginateDirective implements TemplateDirectiveModel {
private static final String PARAM_PAGE = "page";
private static final String PARAM_SIZE = "size";
private static final String PARAM_LIST = "list";
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// 1. 解析參數
int page = getIntParam(params, PARAM_PAGE, 1);
int size = getIntParam(params, PARAM_SIZE, 10);
TemplateSequenceModel listModel = (TemplateSequenceModel) params.get(PARAM_LIST);
if (listModel == null) {
throw new TemplateModelException("缺少必需參數: list");
}
// 2. 轉換為 Java List
List items = Collections.list(listModel);
int total = items.size();
int start = (page - 1) * size;
int end = Math.min(start + size, total);
// 3. 設置循環變量
if (loopVars.length >= 1) {
List pageItems = items.subList(start, end);
loopVars[0] = new SimpleSequence(pageItems, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build());
}
// 4. 設置分頁信息變量
env.setVariable("paginator", new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().wrap(
Map.of("page", page, "size", size, "total", total, "pages", (total + size - 1) / size)
));
// 5. 渲染內容體
if (body != null) {
body.render(env.getOut());
}
}
private int getIntParam(Map params, String name, int defaultVal) throws TemplateModelException {
TemplateModel model = (TemplateModel) params.get(name);
if (model == null) return defaultVal;
if (model instanceof TemplateNumberModel) {
return ((TemplateNumberModel) model).getAsNumber().intValue();
}
if (model instanceof TemplateScalarModel) {
return Integer.parseInt(((TemplateScalarModel) model).getAsString());
}
throw new TemplateModelException("參數 " + name + " 必須為數字");
}
}
// 註冊
cfg.setSharedVariable("paginate", new PaginateDirective());
// 模板中使用
<@paginate list=users page=1 size=5>
<#list paginate as user>
${user.name}
#list>
共 ${paginator.total} 條,第 ${paginator.page}/${paginator.pages} 頁
@paginate>
@Configuration
public class FreeMarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/");
configurer.setDefaultEncoding("UTF-8");
configurer.setConfiguration(freemarkerConfig -> {
freemarkerConfig.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
freemarkerConfig.setSharedVariable("paginate", new PaginateDirective());
});
return configurer;
}
}
// Controller
@GetMapping("/report")
public String report(Model model) {
model.addAttribute("users", userService.findAll());
return "report"; // 解析為 report.ftl
}
cfg.setSharedVariable("formatMoney", new TemplateMethodModelEx() {
@Override
public TemplateModel exec(List args) throws TemplateModelException {
if (args.size() != 1) throw new TemplateModelException("需一個參數");
Number num = ((TemplateNumberModel) args.get(0)).getAsNumber();
return new SimpleScalar(NumberFormat.getCurrencyInstance(Locale.CHINA).format(num));
}
});
// 模板: ${formatMoney(price)}
<@block name="title"/> - 系統
通用頭部
<@block name="content"/>
<#import "layout.ftl" as layout>
<@layout.layout>
<@block name="title">首頁@block>
<@block name="content">歡迎內容@block>
@layout.layout>
template.process(data, new Writer() {
private final OutputStream os = response.getOutputStream();
private final byte[] buffer = new byte[8192];
@Override public void write(char[] cbuf, int off, int len) throws IOException {
os.write(new String(cbuf, off, len).getBytes(StandardCharsets.UTF_8));
}
@Override public void flush() throws IOException { os.flush(); }
@Override public void close() throws IOException { os.close(); }
});
| 問題 | 解決方案 |
|---|---|
| 中文亂碼 | 模板文件 UTF-8、Java 讀寫指定 UTF-8、配置 `defaultEncoding` |
| Null 顯示 "null" | 使用 `${var!}` 或 `${var?default("")}`、`cfg.setClassicCompatible(true)` |
| 大量數據渲染慢 | 流式輸出、分頁指令、預編譯模板 `cfg.getTemplate()` 緩存 |
| 模板修改不生效 | 關閉模板緩存 `cfg.setTemplateUpdateDelayMilliseconds(0)` (開發環境) |
| 循環變量衝突 | 使用 `#list ... as item` 明確命名、避免巢狀同名 |