
本文介绍如何在 Freemarker 模板中对未知类型、数量的数据按 datatype 字段动态分组,并为每组调用统一宏(如 )进行渲染,无需硬编码类型值,兼顾可维护性与性能。
本文介绍如何在 freemarker 模板中对未知类型、数量的数据按 `datatype` 字段动态分组,并为每组调用统一宏(如 ``)进行渲染,无需硬编码类型值,兼顾可维护性与性能。
在实际报表类模板开发中,常需将原始数据按某个字段(如 datatype)分组后分别渲染——例如生成多个独立表格,每个表格对应一种数据类型。但 Freemarker 原生不支持类似 Java 的 Collectors.groupingBy() 或 JavaScript 的 reduce 分组能力,且模板层无法高效完成去重、索引、嵌套过滤等操作。若强行在模板中用 ?filter 遍历多次(如对每个可能的 datatype 调用一次),不仅逻辑冗余、可读性差,更会因 O(n) × k 复杂度导致性能隐患(k 为类型数未知,n 为总数据量)。
最佳实践是:将分组逻辑下沉至 Java 层,通过自定义模板方法暴露分组能力。 这既符合关注点分离原则,又避免模板过度承担业务逻辑,同时保障执行效率。
✅ 推荐方案:Java 端提供两个模板方法
- getDistinctDataTypes(dataList):返回所有唯一 datatype 值的有序列表(如 [1, 3, 4]);
- getDataForDatatype(dataList, targetDatatype):返回 dataList 中 datatype == targetDatatype 的子列表。
二者均实现 TemplateMethodModelEx 接口,并注册为共享变量:
// Java 配置代码(如 Spring Boot 中的 FreemarkerConfigurer)
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setSharedVariable("getDistinctDataTypes", new GetDistinctDataTypes());
cfg.setSharedVariable("getDataForDatatype", new GetDataForDatatype());
对应 Java 实现示例(简化版):
public class GetDistinctDataTypes implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
List<data> dataList = unwrapToList(arguments.get(0));
return dataList.stream()
.map(Data::getDatatype)
.distinct()
.sorted() // 保证输出顺序(可选)
.collect(Collectors.toList());
}
private List<data> unwrapToList(Object arg) {
// 实际需处理 DefaultListAdapter / SimpleSequence 等 Freemarker 包装类型
// 此处仅为示意,生产环境请使用 freemarker.ext.beans.BeanModel 或工具类安全解包
return (List<data>) ((TemplateModel) arg).toString(); // ⚠️ 注意:真实实现需严谨解包
}
}</data></data></data>
public class GetDataForDatatype implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
List<data> dataList = unwrapToList(arguments.get(0));
Integer target = ((Number) arguments.get(1)).intValue();
return dataList.stream()
.filter(data -> Objects.equals(data.getDatatype(), target))
.collect(Collectors.toList());
}
}</data>
✅ Freemarker 模板端使用方式
注册完成后,在 .ftl 文件中即可简洁、清晰地完成动态分组与渲染:
<h3>数据类型:${dt}</h3>
#list>
配套宏定义(示例):
| ID | DataType | 其他字段 |
|---|---|---|
| ${item.id!} | ${item.datatype!} | ${item.name!"N/A"} |
⚠️ 关键注意事项
- 解包安全:Freemarker 传递的 List 在 Java 端通常被包装为 DefaultListAdapter 或 SimpleSequence,切勿直接强转 List。应使用 freemarker.ext.beans.BeanModel 或 TemplateModel 工具类(如 DeepUnwrap.unwrap())安全提取原始对象。
- 性能优势:整个过程仅遍历原始数据 2 次(一次去重,一次分组),时间复杂度为 O(2n),远优于模板层多次 ?filter 的 O(k×n)。
- 扩展性:后续如需按多字段分组、添加排序规则或支持分页子集,只需扩展 Java 方法,模板层完全无感。
- 类型稳定性:datatype 字段若为 null,注意在 Java 方法中做空值处理(如 Objects.equals 已兼容),避免 Freemarker 报 null 错误。
通过该方案,你彻底摆脱了“硬编码类型”“手动维护类型列表”“模板内重复过滤”等反模式,以专业、可维护、高性能的方式实现了 Freemarker 中的动态数据分组与模块化渲染。










