适用版本:FreeMarker 2.3.x(主流稳定版本)
目标:提升模板渲染性能,减少 CPU 消耗,避免不必要的表达式求值


一、核心概念

1. 什么是“重复计算”?

在 FreeMarker 模板中,同一个表达式被多次求值,尤其是在循环、条件判断或深层嵌套中反复调用耗时方法,导致性能下降。

常见场景:

<!-- 场景1:多次调用方法 -->
${user.getProfile().getSettings().getTheme()}
${user.getProfile().getSettings().getLanguage()}
${user.getProfile().getSettings().getTimeout()}

<!-- 场景2:循环中重复计算 -->
<#list items as item>
  <#if expensiveCalculation(item) == "valid">
    ${item.name}
  </#if>
  <#if expensiveCalculation(item) == "urgent">
    <strong>${item.name}</strong>
  </#if>
</#list>

<!-- 场景3:条件判断重复 -->
<#if getUserById(userId).isActive()>
  <p>Welcome back!</p>
</#if>
<#if getUserById(userId).getRole() == "ADMIN">
  <p>Admin panel</p>
</#if>

2. 为什么需要避免?

  • 性能损耗:每次 ${...} 都可能触发 getter、方法调用或复杂表达式。
  • I/O 或数据库调用风险:若方法内部访问数据库,重复调用将雪崩。
  • 内存与 CPU 浪费:尤其在高并发场景下影响显著。

二、操作步骤(非常详细)

步骤 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.Version;

Configuration cfg = new Configuration(Version.VERSION_2_3_32);
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(freemarker.template.TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setTemplateUpdateDelayMilliseconds(Long.MAX_VALUE); // 生产环境关闭热更新

步骤 3:使用 #assign 缓存计算结果(模板层)

✅ 技巧 1:缓存复杂对象引用

<!-- ❌ 低效:重复调用 getter 链 -->
${user.profile.settings.theme}
${user.profile.settings.language}
${user.profile.settings.timeout}

<!-- ✅ 高效:使用 #assign 缓存 -->
<#assign settings = user.profile.settings />
${settings.theme}
${settings.language}
${settings.timeout}

✅ 效果:user.profile.settings 只计算一次。


✅ 技巧 2:缓存方法调用结果

<!-- ❌ 重复调用 -->
<#if getUserPermissions(userId)?seq_contains("edit")>
  <button>Edit</button>
</#if>
<#if getUserPermissions(userId)?seq_contains("delete")>
  <button>Delete</button>
</#if>

<!-- ✅ 使用 #assign 缓存 -->
<#assign perms = getUserPermissions(userId) />
<#if perms?seq_contains("edit")>
  <button>Edit</button>
</#if>
<#if perms?seq_contains("delete")>
  <button>Delete</button>
</#if>

✅ 技巧 3:在循环中缓存计算结果

<!-- ❌ 循环内重复计算 -->
<#list items as item>
  <#if calculateScore(item) > 80>
    <span class="high">${item.name}</span>
  </#if>
  <#if calculateScore(item) < 50>
    <span class="low">${item.name}</span>
  </#if>
</#list>

<!-- ✅ 缓存计算结果 -->
<#list items as item>
  <#assign score = calculateScore(item) />
  <#if score > 80>
    <span class="high">${item.name}</span>
  <#elseif score < 50>
    <span class="low">${item.name}</span>
  </#if>
</#list>

步骤 4:在 Java 层预计算(推荐做法)

✅ 技巧 4:将复杂计算提前到数据模型中

// Java 层预计算
Map<String, Object> model = new HashMap<>();

User user = userService.findById(userId);
List<Item> items = itemService.findByUser(userId);

// 预计算权限
List<String> permissions = permissionService.getPermissionsForUser(user);
model.put("userPermissions", permissions);

// 预计算评分
Map<String, Double> itemScores = items.stream()
    .collect(Collectors.toMap(Item::getId, this::calculateScore));
model.put("itemScores", itemScores);

// 扁平化设置
model.put("theme", user.getProfile().getSettings().getTheme());
model.put("language", user.getProfile().getSettings().getLanguage());

模板中直接使用:

<#list items as item>
  <#assign score = itemScores[item.id]!0 />
  <#if score > 80>High</#if>
</#list>

✅ 优势:模板无复杂逻辑,渲染极快。


步骤 5:使用 #function 封装可复用逻辑(避免重复代码)

<#function getDisplayName user>
  <#return user.firstName + " " + user.lastName>
</#function>

<!-- 使用 -->
${getDisplayName(userA)}
${getDisplayName(userB)}

⚠️ 注意:#function 每次调用仍会执行,不自动缓存。若函数耗时,仍需配合 #assign

<#assign name = getDisplayName(user) />
${name} (again: ${name})

步骤 6:避免在 #if#list#include 中重复表达式

<!-- ❌ 重复 -->
<#if getUserById(userId).isActive()>
  <#include "active-banner.ftl" />
</#if>
<#if getUserById(userId).getRole() == "ADMIN">
  <#include "admin-tools.ftl" />
</#if>

<!-- ✅ 缓存用户 -->
<#assign currentUser = getUserById(userId) />
<#if currentUser.isActive()>
  <#include "active-banner.ftl" />
</#if>
<#if currentUser.role == "ADMIN">
  <#include "admin-tools.ftl" />
</#if>

三、常见错误与解决方案

错误现象 原因 解决方案
模板渲染慢 表达式重复求值 使用 #assign 缓存
数据库查询多次 方法在模板中被多次调用 移到 Java 层预加载
?size 被多次调用 集合无缓存 使用 #assign size = list?size
?api 调用重复 反射开销大 禁用 ?api 或缓存结果
条件判断重复 未提取公共表达式 使用 #assign 提前计算

四、注意事项

  1. #assign 是局部变量:作用域为当前 <#assign>...</#assign> 块或宏。
  2. 不要缓存可变对象:如 #assign list = items?sort,若 items 变化,缓存无效。
  3. 避免在宏中重复计算:宏参数每次访问都可能重新求值。
  4. ?string?html 等内置函数可缓存
    <#assign safeName = user.name?html />
    ${safeName} ${safeName}
    
  5. ?api 调用代价高:尽量避免,必须用时务必缓存。

五、使用技巧

1. 使用 #local 在宏中避免重复

<#macro renderUser user>
  <#local profile = user.profile /> <!-- 局部变量,避免多次访问 -->
  <p>${profile.name}</p>
  <p>${profile.email}</p>
</#macro>

2. 使用 #global 共享计算结果(谨慎使用)

<#if !global.calculated>
  <#assign global.calcResult = heavyCalculation() />
  <#assign global.calculated = true />
</#if>

⚠️ 注意:global 是全局的,多线程下可能冲突,不推荐用于 Web 应用

3. 使用 ?cache 内置函数(FreeMarker 2.3.30+)

<!-- 实验性功能:缓存表达式结果 -->
${expensiveFunction(param)?cache}

⚠️ 注意:?cache 默认使用弱引用,生命周期短,不保证缓存命中,适合极耗时且幂等的函数。


六、最佳实践

实践 说明
✅ 优先在 Java 层预计算 最安全高效
✅ 模板中使用 #assign 缓存复杂表达式 减少重复求值
✅ 扁平化数据模型 避免深层 getter 链
✅ 封装逻辑到 #function#macro 提高复用性
✅ 避免在循环中调用方法 必须调用则先缓存
✅ 使用 ?cache(谨慎) 仅用于幂等、耗时函数
✅ 监控模板性能 记录渲染耗时

七、性能优化对比

场景 优化前 优化后 提升
重复 getter 链 5 次反射调用 1 次 + 缓存 ⬆️ 80%
循环中重复计算 N 次方法调用 N 次赋值 ⬆️ 50-90%
多次 ?size 多次调用 size() 1 次缓存 ⬆️ 20-30%
Java 层预计算 模板复杂 模板简单 ⬆️ 60%+

八、完整优化示例

Java 层(预计算)

Map<String, Object> model = new HashMap<>();
User user = userService.findById(1);

// 预计算设置
UserSettings settings = user.getProfile().getSettings();
model.put("theme", settings.getTheme());
model.put("lang", settings.getLanguage());

// 预计算权限
model.put("perms", permissionService.getPermissions(user));

// 预计算项目评分
Map<String, Double> scores = items.stream()
    .collect(Collectors.toMap(Item::getId, this::calcScore));
model.put("scores", scores);

模板层(避免重复)

<#-- 缓存 -->
<#assign userPerms = perms />

<#list items as item>
  <#assign score = scores[item.id]!0 />
  <#if score > 80 && userPerms?seq_contains("view")>
    <div class="high">${item.name}</div>
  </#if>
</#list>

九、总结:避免重复计算检查清单 ✅

优化项 是否完成
使用 #assign 缓存复杂表达式
避免在循环中重复调用方法
将计算移到 Java 层
扁平化数据模型
使用 ?cache(可选)
封装函数到 #function
避免重复 ?apinew 调用

一句话总结
FreeMarker 避免重复计算的核心是 “一次计算,多次使用” —— 通过 #assign 缓存、Java 层预处理、扁平化模型,显著提升渲染性能。