适用版本:FreeMarker 2.3.x(主流稳定版本)
目标:提升模板渲染性能、降低内存占用、增强可维护性
一、核心概念
1. 什么是数据模型(Data Model)?
数据模型是传递给 FreeMarker 模板的 键值对集合(Map<String, Object>
),用于在 .ftl
模板中通过 ${key}
访问数据。
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("user", userObj);
dataModel.put("items", itemList);
模板中使用:
<p>Hello ${user.name}, you have ${items?size} items.</p>
2. 数据模型的本质
- 在 FreeMarker 内部,数据模型被包装为
TemplateModel
接口实现(如TemplateScalarModel
,TemplateSequenceModel
等)。 - 包装过程由
ObjectWrapper
控制。 - 性能瓶颈常出现在:对象包装、深层属性访问、大集合处理。
3. 优化目标
目标 | 说明 |
---|---|
⚡ 减少对象包装开销 | 避免频繁反射调用 getter |
💾 降低内存占用 | 避免加载不必要的字段 |
🚀 提升渲染速度 | 减少模板中复杂表达式计算 |
🔁 提高缓存命中率 | 使 Template 和数据更易复用 |
二、操作步骤(非常详细)
步骤 1:引入依赖(Maven)
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
步骤 2:配置高效的 Configuration
(基础)
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.Version;
Configuration cfg = new Configuration(Version.VERSION_2_3_32);
// 设置编码
cfg.setDefaultEncoding("UTF-8");
// 错误处理(生产环境建议 RETHROW 或 SILENT)
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// 关闭开发期热更新(生产环境)
cfg.setTemplateUpdateDelayMilliseconds(Long.MAX_VALUE);
// 使用轻量级对象包装器(关键!)
cfg.setObjectWrapper(freemarker.template.ObjectWrapper.SIMPLE_WRAPPER); // 或自定义
✅
SIMPLE_WRAPPER
:仅支持基本类型、String、Collection、Map,不支持 JavaBean 反射,性能最高。
步骤 3:优化数据模型结构(核心)
✅ 技巧 1:扁平化数据结构(避免深层嵌套)
❌ 低效:
dataModel.put("user", userService.findById(1));
// user.getAddress().getCity().getName()
${user.address.city.name} <!-- 多次 getter 调用 -->
✅ 高效:提前“展平”数据
User user = userService.findById(1);
Map<String, Object> flatModel = new HashMap<>();
flatModel.put("userName", user.getName());
flatModel.put("userEmail", user.getEmail());
flatModel.put("userCity", user.getAddress().getCity().getName());
flatModel.put("userCountry", user.getAddress().getCountry().getName());
cfg.getTemplate("profile.ftl").process(flatModel, out);
${userName}, ${userCity} <!-- 直接访问,无 getter 链 -->
✅ 技巧 2:使用 Map
或 SimpleHash
替代复杂 POJO
// 使用 SimpleHash(FreeMarker 原生支持)
import freemarker.template.SimpleHash;
SimpleHash model = new SimpleHash();
model.put("title", "Welcome");
model.put("items", items);
✅ 优势:无需反射,直接映射,性能优于 JavaBean。
✅ 技巧 3:延迟加载与按需提供
// 只在需要时才加载数据
if (templateName.contains("detail")) {
model.put("fullData", heavyService.loadDetail());
} else {
model.put("summary", lightService.loadSummary());
}
步骤 4:使用 TemplateHashModel
实现惰性求值
public class LazyUserModel implements TemplateHashModel {
private final int userId;
private User cachedUser;
public LazyUserModel(int userId) {
this.userId = userId;
}
@Override
public TemplateModel get(String key) throws TemplateModelException {
if (cachedUser == null) {
cachedUser = userService.findById(userId); // 延迟加载
}
switch (key) {
case "name": return new SimpleScalar(cachedUser.getName());
case "email": return new SimpleScalar(cachedUser.getEmail());
default: return null;
}
}
@Override
public boolean isEmpty() throws TemplateModelException {
return false;
}
}
模板中使用:
${user.name} <!-- 仅当访问时才加载 user -->
✅ 优势:避免未使用字段的加载开销。
步骤 5:预处理集合与避免模板中复杂运算
❌ 模板中计算(低效)
<#assign total = 0 />
<#list items as item>
<#assign total = total + item.price * item.quantity>
</#list>
Total: ${total}
✅ Java 层预计算(高效)
double total = items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
model.put("orderTotal", total);
Total: ${orderTotal?string.currency}
步骤 6:使用 SharedVariables
减少重复传递
// 全局共享变量(如工具类、常量)
cfg.setSharedVariable("utils", new TemplateHashModel() {
public TemplateModel get(String key) {
if ("now".equals(key)) {
return new TemplateDateModel(new Date(), new java.text.SimpleDateFormat("yyyy-MM-dd"));
}
return null;
}
public boolean isEmpty() { return false; }
});
模板中直接使用:
Today: ${utils.now}
✅ 避免每个请求都 put 一次。
步骤 7:启用模板缓存与复用(性能关键)
// Configuration 默认启用模板缓存
// 可设置缓存大小
cfg.setSetting("template_cache_hash_size", "17");
cfg.setSetting("template_cache_concurrency_level", "5");
✅
Template
对象是线程安全的,可缓存复用。
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
模板渲染慢 | 数据模型过大或嵌套深 | 扁平化模型,预计算 |
Missing variable |
键名拼写错误 | 使用常量定义 key |
Method not found |
getter 不存在 | 检查 POJO 或改用 Map |
内存溢出 | 加载大集合 | 分页、流式处理、懒加载 |
?size 性能差 |
集合无 size() 方法 |
使用 ArrayList 而非 Iterable |
四、注意事项
- 避免在模板中调用耗时方法:如
service.getUser().getProfile().getSettings()
。 - 不要传递整个 Spring Service 或 Repository:仅传递必要数据。
- 集合类型优先使用
List
/Map
:避免Iterable
(FreeMarker 会遍历两次)。 - 日期统一格式化后再传入:或使用
?datetime
内置函数。 - 空值处理:使用
??
或!
运算符避免null
异常。
${user.nickname!''} 或 ${user.profile?? user.profile.name : 'Anonymous'}
五、使用技巧
1. 使用 @Directive
封装复杂逻辑
@TemplateDirective
public void renderUser(TemplateDirectiveBody body) throws IOException, TemplateException {
// 复杂渲染逻辑封装
}
模板中:
@renderUser(user=user)
2. 使用 TemplateModelCache
缓存计算结果
private final Map<String, TemplateModel> modelCache = new ConcurrentHashMap<>();
public TemplateModel getUserModel(int id) {
return modelCache.computeIfAbsent("user_" + id, k -> {
User u = userService.findById(id);
return new SimpleHash() {{
put("name", u.getName());
put("age", u.getAge());
}};
});
}
3. 使用 StopWatch
监控渲染耗时
StopWatch watch = new StopWatch();
watch.start();
template.process(model, out);
watch.stop();
log.info("Template {} rendered in {} ms", name, watch.getTotalTimeMillis());
六、最佳实践
实践 | 说明 |
---|---|
✅ 扁平化数据模型 | 减少 getter 链 |
✅ 预计算复杂表达式 | 在 Java 层完成 |
✅ 使用 Map /SimpleHash |
比 POJO 更快 |
✅ 懒加载大对象 | TemplateHashModel 实现 |
✅ 共享变量复用 | sharedVariables |
✅ 合理使用缓存 | 模板 + 数据 |
✅ 控制集合大小 | 避免 OOM |
✅ 统一空值处理 | 使用 ! 或 ?? |
七、性能优化总结
优化项 | 性能提升 |
---|---|
使用 SIMPLE_WRAPPER |
⬆️ 30-50% |
扁平化模型 | ⬆️ 20-40% |
预计算替代模板计算 | ⬆️ 50%+ |
懒加载大对象 | ⬇️ 内存占用 |
启用模板缓存 | ⬆️ 首次后 90%+ |
避免 Iterable |
⬆️ 10-20% |
八、完整优化示例代码
// 1. 配置
Configuration cfg = new Configuration(Version.VERSION_2_3_32);
cfg.setObjectWrapper(ObjectWrapper.SIMPLE_WRAPPER);
cfg.setTemplateUpdateDelayMilliseconds(Long.MAX_VALUE);
cfg.setDefaultEncoding("UTF-8");
// 2. 共享变量
cfg.setSharedVariable("env", new SimpleScalar("prod"));
// 3. 构建优化模型
User user = userService.findById(1);
Map<String, Object> model = new HashMap<>();
model.put("userName", user.getName());
model.put("userEmail", user.getEmail());
model.put("itemCount", user.getItems().size());
model.put("totalPrice", calculateTotal(user.getItems())); // 预计算
// 4. 渲染
Template template = cfg.getTemplate("user_summary.ftl");
Writer out = new StringWriter();
template.process(model, out);
九、总结:数据模型优化检查清单 ✅
优化项 | 是否完成 |
---|---|
使用扁平化数据结构 | ☐ |
避免深层 getter 调用 | ☐ |
预计算复杂表达式 | ☐ |
使用 SIMPLE_WRAPPER |
☐ |
懒加载大对象(如需) | ☐ |
使用 sharedVariables 共享工具 |
☐ |
控制集合大小与类型 | ☐ |
模板中避免复杂逻辑 | ☐ |
✅ 一句话总结:
FreeMarker 数据模型优化的核心是 “减少反射、提前计算、扁平结构、合理缓存”,通过在 Java 层做好准备,让模板只做“展示”工作,性能可提升数倍。