
本文详解如何在 Thymeleaf 中正确使用 th:block 配合 th:each 和 th:if 渲染深度嵌套的 Map 数据结构,生成符合语义、结构清晰的 HTML 表格,避免标签不闭合与非法嵌套错误。
本文详解如何在 thymeleaf 中正确使用 `th:block` 配合 `th:each` 和 `th:if` 渲染深度嵌套的 `map
在 Thymeleaf 模板中, 你遇到的两个典型错误正源于此: ✅ 正确解法是:放弃“动态拼接标签”的思路,转而通过条件渲染单元格内容来实现视觉上的合并效果。参考你的数据结构: 对应的目标表格需满足: 以下是推荐的、语义清晰且完全合法的 Thymeleaf 模板写法: ? 关键要点说明:<block></block> 是一个逻辑容器标签,它本身不会被渲染为任何 HTML 元素,仅用于分组和条件控制,是处理复杂循环与条件逻辑的理想工具。但其使用需严格遵循 XML/HTML 语法规范:所有开始标签必须有对应结束标签,且不可跨标签边界拆分结构(如将 <tr> 的开始与结束分别放在不同 <code>th:block 内)。<tr> 内部用 <code>th:block 尝试“分段拼接” <tr> 标签(如 <code><tr> 开在第一个 <code>th:block,
闭在第二个),违反 HTML 嵌套规则;
th:if 属性——Thymeleaf 不支持在结束标签上使用任何属性指令,该语法直接导致解析失败。Map<String, Map<String, Map<String, String>>> result = Map.of(
"First", Map.of(
"time", Map.of("now", "asfas", "past", "lsefa", "future", "saeflkjle"),
"sleifj", Map.of("now", "dsff", "past", "sfesf", "future", "sefasefa")
),
"Second", Map.of(
"time", Map.of("now", "xyz", "past", "abc", "future", "def"),
"sleifj", Map.of("now", "123", "past", "456", "future", "789")
)
);
"First")仅在首行显示于第一列;"time"、"sleifj")的后续行,第一列留空(视觉上实现“跨行合并”效果);"time");"now"、"past"、"future" 的值。<table border="1" class="table">
<thead>
<tr>
<th>Map Name</th>
<th>Sub Name</th>
<th>Now</th>
<th>Past</th>
<th>Future</th>
</tr>
</thead>
<tbody>
<!-- 外层遍历:每个顶级 Map Key(如 "First", "Second") -->
<th:block th:each="entry : ${result}">
<!-- 内层遍历:每个子 Map(如 time/sleifj) -->
<tr th:each="subEntry : ${entry.value}">
<!-- 第一列:仅首行显示顶级 Key,其余为空 -->
<td th:if="${subEntryStat.first}" th:text="${entry.key}">First</td>
<td th:unless="${subEntryStat.first}"> </td>
<!-- 第二列:显示子 Key(如 "time") -->
<td th:text="${subEntry.key}">time</td>
<!-- 第三至五列:安全取值(建议使用 #maps.get 防 NPE) -->
<td th:text="${#maps.get(subEntry.value, 'now')}">asfas</td>
<td th:text="${#maps.get(subEntry.value, 'past')}">lsefa</td>
<td th:text="${#maps.get(subEntry.value, 'future')}">saeflkjle</td>
</tr>
</th:block>
</tbody>
</table>
th:block 作为纯粹的逻辑分组容器,包裹整个外层循环,确保不向 DOM 注入冗余节点;subEntryStat.first(而非 count == 0)更语义化且健壮;<td> 均完整定义,无标签拆分或非法属性,完全符合 HTML 规范;<li>推荐使用 <code>#maps.get(map, key) 替代 map.get('key'),可避免 NullPointerException(当子 Map 缺失某 key 时返回 null 而非报错);th:block 移至 <tbody> 上(即 <code>th:each 直接作用于 <tbody>),适用于需为每组数据添加独立 <code><tbody> 的场景。<p>? <strong>进阶提示:</strong><br>
若需真正实现 <code><td rowspan="2">First</td> 这类原生行合并,需在后端预处理数据(如计算每组子项数量并注入 rowspan 值),再由模板直接输出。但对多数报表场景,上述“首行显示 + 其余留空”的视觉方案已足够清晰且开发成本最低。
通过合理运用 th:block 的分组能力与 th:if/th:unless 的条件控制,即可优雅、安全地渲染任意深度嵌套的 Map 结构,无需牺牲模板可维护性与浏览器兼容性。

















