适用版本: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:使用 MapSimpleHash 替代复杂 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

四、注意事项

  1. 避免在模板中调用耗时方法:如 service.getUser().getProfile().getSettings()
  2. 不要传递整个 Spring Service 或 Repository:仅传递必要数据。
  3. 集合类型优先使用 List/Map:避免 Iterable(FreeMarker 会遍历两次)。
  4. 日期统一格式化后再传入:或使用 ?datetime 内置函数。
  5. 空值处理:使用 ??! 运算符避免 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 层做好准备,让模板只做“展示”工作,性能可提升数倍。