一、FreeMarker 简介
FreeMarker 是一款强大的基于 Java 的模板引擎,用于生成各种文本输出(HTML、XML、JSON、配置文件、源代码等)。它遵循 MVC 模式,将页面表现层与业务逻辑完全分离,是 Java Web 开发中不可或缺的工具。
1.1 核心特性
| 特性 | 说明 |
|---|---|
| 模板与数据分离 | 模板文件独立于业务逻辑,便于维护和复用 |
| 强大的表达式语言 | 支持复杂的数据操作和逻辑处理 |
| 类型安全 | 在模板中可以进行类型检查 |
| 宏与函数 | 支持自定义宏和函数,提高代码复用 |
| 国际化支持 | 内置国际化和本地化功能 |
| 扩展性强 | 支持自定义指令和标签 |
1.2 工作原理
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 业务逻辑层 │────►│ FreeMarker │────►│ 输出结果 │
│ (Java Objects) │ │ Template Engine│ │ (HTML/XML/JSON) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ 模板文件 │
│ (.ftl 文件) │
└──────────────────┘
二、基本语法与用法
2.1 变量输出
<!-- 简单变量输出 -->
Hello, ${name}!
<!-- 带有默认值的变量 -->
Hello, ${name!"Guest"}!
<!-- 对象属性访问 -->
User: ${user.name}, Age: ${user.age}
<!-- 数组/列表访问 -->
First item: ${items[0]}
<!-- 集合大小 -->
Total items: ${items?size}
2.2 条件判断
<!-- if 语句 -->
<#if user.age >= 18>
<p>成年用户</p>
<#else>
<p>未成年用户</p>
</#if>
<!-- 多条件判断 -->
<#if score >= 90>
<p>优秀</p>
<#elseif score >= 80>
<p>良好</p>
<#elseif score >= 60>
<p>及格</p>
<#else>
<p>不及格</p>
</#if>
<!-- 空值判断 -->
<#if user??>
<p>用户存在: ${user.name}</p>
</#if>
<!-- 非空判断 -->
<#if user.name?has_content>
<p>用户名: ${user.name}</p>
</#if>
2.3 循环遍历
<!-- 遍历列表 -->
<ul>
<#list users as user>
<li>${user_index + 1}. ${user.name} - ${user.email}</li>
</#list>
</ul>
<!-- 遍历 Map -->
<#list map?keys as key>
<p>${key}: ${map[key]}</p>
</#list>
<!-- 遍历数字范围 -->
<#list 1..10 as i>
<p>Item ${i}</p>
</#list>
<!-- 空集合处理 -->
<#list users as user>
<p>${user.name}</p>
<#else>
<p>暂无用户</p>
</#list>
2.4 宏定义与调用
<!-- 定义宏 -->
<#macro greeting name>
<div class="greeting">
Hello, ${name}!
</div>
</#macro>
<!-- 调用宏 -->
<@greeting name="World"/>
<!-- 带参数默认值的宏 -->
<#macro welcome name="Guest">
<p>Welcome, ${name}!</p>
</#macro>
<!-- 调用带默认参数的宏 -->
<@welcome/>
<@welcome name="John"/>
<!-- 嵌套内容的宏 -->
<#macro box title>
<div class="box">
<h3>${title}</h3>
<div class="content">
<#nested>
</div>
</div>
</#macro>
<!-- 使用嵌套内容 -->
<@box title="My Box">
<p>This is the content inside the box.</p>
</@box>
2.5 内置函数
<!-- 字符串函数 -->
${"Hello World"?upper_case} <!-- HELLO WORLD -->
${"Hello World"?lower_case} <!-- hello world -->
${"Hello"?trim} <!-- Hello (去除首尾空格) -->
${"Hello World"?length} <!-- 11 -->
${"Hello"?index_of("ll")} <!-- 2 -->
<!-- 集合函数 -->
${users?size} <!-- 集合大小 -->
${users?sort_by("age")} <!-- 按年龄排序 -->
${users?reverse} <!-- 反转集合 -->
${users?first} <!-- 第一个元素 -->
${users?last} <!-- 最后一个元素 -->
<!-- 数字函数 -->
${3.14?round} <!-- 3 -->
${3.14?floor} <!-- 3 -->
${3.14?ceiling} <!-- 4 -->
${10?string(",###")} <!-- 10 -->
<!-- 日期函数 -->
${now?date} <!-- 2024-01-15 -->
${now?time} <!-- 14:30:00 -->
${now?datetime} <!-- 2024-01-15 14:30:00 -->
${now?string("yyyy-MM-dd")} <!-- 自定义格式 -->
2.6 自定义函数
<!-- 定义自定义函数 -->
<#function calculateDiscount price discount>
<#return price * (1 - discount / 100)>
</#function>
<!-- 使用自定义函数 -->
Original: $${price}
Discount: ${discount}%
Final: $${calculateDiscount(price, discount)}
三、Java 集成实战
3.1 基本配置
Maven 依赖:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
Spring Boot 集成:
@Configuration
public class FreeMarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/");
Properties props = new Properties();
props.setProperty("default_encoding", "UTF-8");
props.setProperty("number_format", "#.##");
configurer.setFreemarkerSettings(props);
return configurer;
}
}
3.2 基本使用示例
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
public class FreeMarkerExample {
public static void main(String[] args) throws Exception {
// 创建配置对象
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setClassForTemplateLoading(FreeMarkerExample.class, "/templates");
cfg.setDefaultEncoding("UTF-8");
// 创建数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("title", "FreeMarker Example");
dataModel.put("message", "Hello from FreeMarker!");
// 获取模板
Template template = cfg.getTemplate("hello.ftl");
// 处理模板并输出
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
// 输出结果
System.out.println(writer.toString());
}
}
3.3 Spring MVC 集成
Controller 示例:
@Controller
public class UserController {
@GetMapping("/users")
public String listUsers(Model model) {
List<User> users = userService.findAll();
model.addAttribute("users", users);
model.addAttribute("pageTitle", "用户列表");
return "users/list"; // 对应 templates/users/list.ftl
}
}
模板文件 users/list.ftl:
<!DOCTYPE html>
<html>
<head>
<title>${pageTitle}</title>
</head>
<body>
<h1>${pageTitle}</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<#list users as user>
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
<#else>
<tr>
<td colspan="3">暂无用户</td>
</tr>
</#list>
</tbody>
</table>
</body>
</html>
3.4 生成代码文件
public class CodeGenerator {
public void generateEntity(String entityName, List<String> fields) throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setClassForTemplateLoading(CodeGenerator.class, "/templates");
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("entityName", entityName);
dataModel.put("fields", fields);
dataModel.put("packageName", "com.example.entity");
Template template = cfg.getTemplate("entity.ftl");
try (FileWriter writer = new FileWriter("src/main/java/com/example/entity/" + entityName + ".java")) {
template.process(dataModel, writer);
}
}
}
代码模板 entity.ftl:
package ${packageName};
public class ${entityName} {
<#list fields as field>
private String ${field};
public String get${field?cap_first}() {
return ${field};
}
public void set${field?cap_first}(String ${field}) {
this.${field} = ${field};
}
</#list>
}
四、高级特性
4.1 自定义指令
public class CustomDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// 获取参数
String title = getParam(params, "title");
// 输出开始标签
env.getOut().write("<div class=\"custom-box\">");
env.getOut().write("<h2>" + title + "</h2>");
// 处理嵌套内容
if (body != null) {
body.render(env.getOut());
}
// 输出结束标签
env.getOut().write("</div>");
}
private String getParam(Map params, String name) throws TemplateModelException {
TemplateModel model = (TemplateModel) params.get(name);
if (model instanceof SimpleScalar) {
return ((SimpleScalar) model).getAsString();
}
return null;
}
}
注册自定义指令:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setSharedVariable("customBox", new CustomDirective());
使用自定义指令:
<@customBox title="My Custom Box">
<p>This is the content inside the custom box.</p>
</@customBox>
4.2 宏库与导入
<!-- common.ftl - 宏库文件 -->
<#macro button text color="primary">
<button class="btn btn-${color}">${text}</button>
</#macro>
<#macro alert type="info" message>
<div class="alert alert-${type}">${message}</div>
</#macro>
<!-- 在其他模板中导入宏库 -->
<#import "common.ftl" as common>
<@common.button text="Click Me"/>
<@common.button text="Submit" color="success"/>
<@common.alert type="warning" message="Warning!"/>
4.3 国际化支持
资源文件 messages_en.properties:
greeting=Hello
welcome=Welcome to our application
资源文件 messages_zh.properties:
greeting=你好
welcome=欢迎使用我们的应用
模板中使用国际化:
<#assign locale = "zh_CN">
<#assign bundle = ResourceBundle.getBundle("messages", locale)>
<p>${bundle.greeting}, ${user.name}!</p>
<p>${bundle.welcome}</p>
4.4 模板继承
<!-- base.ftl - 基础模板 -->
<!DOCTYPE html>
<html>
<head>
<title><#if title??>${title}</#if></title>
<link rel="stylesheet" href="style.css">
<#block name="head">
<!-- 子模板可以覆盖这里 -->
</#block>
</head>
<body>
<header>
<h1>My App</h1>
</header>
<div class="content">
<#block name="content">
<!-- 子模板内容 -->
</#block>
</div>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
<!-- 子模板继承基础模板 -->
<#include "base.ftl">
<#assign title = "Home Page">
<#block name="head">
<style>
.special { color: red; }
</style>
</#block>
<#block name="content">
<h2>Welcome to the Home Page</h2>
<p class="special">This is the home content.</p>
</#block>
五、性能优化与最佳实践
5.1 性能优化技巧
1. 模板缓存:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setCacheStorage(new MruCacheStorage(20, 250)); // 设置缓存大小
2. 避免重复计算:
<!-- 不推荐 -->
<#list items as item>
${getItemDetails(item)?html}
</#list>
<!-- 推荐:使用 assign 缓存结果 -->
<#assign itemDetails = getItemDetails()>
<#list items as item>
${itemDetails[item.id]?html}
</#list>
3. 使用 ?no_esc 避免不必要的转义:
<!-- 已知内容安全时使用 -->
${trustedContent?no_esc}
5.2 安全最佳实践
1. 输出转义:
<!-- HTML转义 -->
${userInput?html}
<!-- XML转义 -->
${userInput?xml}
<!-- JSON转义 -->
${userInput?json_string}
2. 禁用危险指令:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setWhitespaceStripping(true);
cfg.setDisableBuiltin("eval"); // 禁用 eval
3. 验证用户输入:
<#if userInput?matches("^[a-zA-Z0-9]+$")>
<p>Valid input: ${userInput}</p>
<#else>
<p>Invalid input</p>
</#if>
5.3 代码组织最佳实践
1. 模板目录结构:
templates/
├── layout/ # 布局模板
│ ├── base.ftl
│ └── sidebar.ftl
├── common/ # 公共宏库
│ ├── forms.ftl
│ └── widgets.ftl
├── users/ # 用户模块
│ ├── list.ftl
│ └── detail.ftl
└── products/ # 产品模块
├── list.ftl
└── detail.ftl
2. 使用宏库减少重复代码:
<!-- common/forms.ftl -->
<#macro input name label value="" type="text">
<div class="form-group">
<label for="${name}">${label}</label>
<input type="${type}" id="${name}" name="${name}" value="${value?html}"/>
</div>
</#macro>
3. 注释模板代码:
<#--
用户列表页面模板
参数:
- users: 用户列表
- pageTitle: 页面标题
-->
六、常见问题与解决方案
6.1 中文乱码问题
问题:模板输出的中文显示为乱码
解决方案:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputEncoding("UTF-8");
6.2 模板找不到
问题:TemplateNotFoundException
解决方案:
// 确保模板路径正确
cfg.setClassForTemplateLoading(MyClass.class, "/templates");
// 或者使用文件系统路径
cfg.setDirectoryForTemplateLoading(new File("/path/to/templates"));
6.3 空值异常
问题:变量为 null 时抛出异常
解决方案:
<!-- 使用默认值 -->
${user.name!"Unknown"}
<!-- 使用 if 判断 -->
<#if user.name??>
${user.name}
</#if>
6.4 性能问题
问题:模板渲染速度慢
解决方案:
- 启用模板缓存
- 减少嵌套循环
- 避免在模板中进行复杂计算
- 使用
?cache缓存昂贵的表达式
七、FreeMarker 与其他模板引擎对比
| 特性 | FreeMarker | Thymeleaf | Velocity | Mustache |
|---|---|---|---|---|
| 语言类型 | 专用模板语言 | HTML-first | 专用模板语言 | 极简标记 |
| 学习曲线 | 中等 | 低 | 低 | 极低 |
| 功能丰富度 | 高 | 中 | 中 | 低 |
| Spring集成 | 良好 | 优秀 | 一般 | 一般 |
| 国际化 | 内置 | 内置 | 内置 | 需扩展 |
| 性能 | 优秀 | 良好 | 良好 | 优秀 |
| 社区活跃度 | 稳定 | 活跃 | 较低 | 活跃 |
选择建议
选择 FreeMarker 当:
- 需要强大的模板功能和灵活的表达式语言
- 正在构建复杂的动态内容系统
- 需要代码生成或文档生成功能
- 已有 FreeMarker 项目需要维护
选择 Thymeleaf 当:
- 构建现代 HTML5 应用
- 追求自然模板(模板本身是有效的 HTML)
- 需要更好的 Spring Boot 集成
选择 Mustache 当:
- 需要跨平台模板(JavaScript、Java、Python等)
- 追求极简语法
- 构建轻量级应用
八、总结
FreeMarker 作为一款成熟的 Java 模板引擎,以其强大的功能、灵活的语法和良好的性能,在 Java Web 开发中占据重要地位。它特别适合以下场景:
- Web 页面生成:与 Spring MVC 集成,生成动态 HTML
- 代码生成:自动生成 Java 实体类、配置文件等
- 文档生成:生成报告、PDF 内容、邮件模板等
- 配置文件生成:根据模板生成各种格式的配置文件
掌握 FreeMarker 的核心语法和最佳实践,能够显著提高开发效率,使代码更加清晰和可维护。