适用版本: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 提前计算 |
四、注意事项
#assign
是局部变量:作用域为当前<#assign>...</#assign>
块或宏。- 不要缓存可变对象:如
#assign list = items?sort
,若items
变化,缓存无效。 - 避免在宏中重复计算:宏参数每次访问都可能重新求值。
?string
、?html
等内置函数可缓存:<#assign safeName = user.name?html /> ${safeName} ${safeName}
?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 |
☐ |
避免重复 ?api 或 new 调用 |
☐ |
✅ 一句话总结:
FreeMarker 避免重复计算的核心是 “一次计算,多次使用” —— 通过 #assign
缓存、Java 层预处理、扁平化模型,显著提升渲染性能。