第一章:Thymeleaf 入门基础
1.1 什么是 Thymeleaf
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Thymeleaf | 一种适用于 Java 应用的现代服务器端模板引擎,支持 HTML5、XML、JavaScript、CSS 等模板类型。主要用于在服务端动态生成 HTML 页面。 | 不依赖特定 Web 框架,但与 Spring Boot 集成最紧密。 |
| 模板引擎 | 负责将数据模型与模板文件结合,生成最终输出(如 HTML)的组件。Thymeleaf 在运行时解析模板并注入数据。 | 模板文件本身是合法的 HTML,可直接在浏览器中预览(自然模板特性)。 |
| 自然模板(Natural Templates) | Thymeleaf 模板在未渲染状态下仍能被浏览器正确显示,便于前端开发与协作。 | 需合理使用 th:* 属性,避免破坏原始 HTML 结构。 |
1.2 Thymeleaf 的核心特性
| 特性名称 | 说明 | 注意事项 |
|---|---|---|
| 支持多种模板模式 | 包括 HTML、XML、TEXT、JAVASCRIPT、CSS、RAW 六种模板模式。 | 默认为 HTML 模式;其他模式需显式配置或通过文件扩展名识别。 |
| 标准方言(Standard Dialect) | 提供表达式、流程控制、属性设置等核心功能,基于 OGNL 或 SpringEL(Spring 环境下)。 | 在 Spring Boot 中默认使用 SpringEL 表达式。 |
| 可扩展性 | 支持自定义方言(Dialect),可添加新的属性、表达式或处理器。 | 自定义方言需实现 IProcessorDialect 接口。 |
| 无侵入性 | 模板中使用 th:* 属性,不影响原始 HTML 结构,便于前后端并行开发。 | 避免在 th:* 属性中写复杂逻辑,保持模板简洁。 |
| 国际化支持 | 内置 #messages 工具对象,支持多语言消息资源加载。 | 需配置 message.properties 文件及 LocaleResolver。 |
| 安全性 | 自动对输出内容进行 HTML 转义,防止 XSS 攻击。 | 如需输出原始 HTML,需使用 th:utext 而非 th:text。 |
1.3 Thymeleaf 与 JSP、FreeMarker 的对比
| 对比维度 | Thymeleaf | JSP(JavaServer Pages) | FreeMarker |
|---|---|---|---|
| 模板语法 | 基于属性(th:text 等),HTML 原生兼容 | 嵌入 Java 代码(<% %>)或 JSTL 标签 | 使用自定义指令(如 <#if>、$ {}) |
| 自然模板 | 支持,未渲染时可直接在浏览器查看 | 不支持,需服务器渲染 | 不支持 |
| 性能 | 中等,首次渲染较慢,后续缓存加速 | 较快(编译为 Servlet) | 较快,模板预编译 |
| 与 Spring 集成 | 官方推荐,无缝集成 | 支持,但已逐渐淘汰 | 支持良好 |
| 学习曲线 | 较低,HTML 开发者友好 | 中等,需了解 Servlet 生命周期 | 中等,需学习专属语法 |
| 响应式支持 | 支持(Thymeleaf 3.0+ 与 WebFlux 兼容) | 不支持(基于 Servlet,阻塞式) | 有限支持(需额外适配) |
| 社区活跃度 | 高(Spring 生态主力模板引擎) | 低(Oracle 已停止更新) | 中等 |
1.4 环境搭建(Spring Boot 集成)
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 添加依赖 | 在 pom.xml 中添加 spring-boot-starter-thymeleaf(见下方代码示例 1) | 不要手动添加 thymeleaf-spring5 依赖,starter 已包含。 |
| 配置模板路径 | 默认模板目录为 src/main/resources/templates,无需额外配置。 | 若需修改,可在 application.yml 中设置 spring.thymeleaf.prefix。 |
| 创建控制器 | 编写 Controller 返回视图名称(见下方代码示例 2) | 方法返回值为模板文件名(不含 .html 后缀)。 |
| 创建模板文件 | 在 templates 目录下创建 hello.html(见下方代码示例 3) | 必须声明 xmlns:th 命名空间以启用 IDE 提示(非强制运行,但推荐)。 |
| 启动应用并访问 | 运行 Spring Boot 应用,访问 http://localhost:8080/hello 查看渲染结果。 | 确保端口未被占用,默认 8080;若修改需同步调整 URL。 |
| 关闭模板缓存(开发) | 在 application.properties 中添加 spring.thymeleaf.cache=false | 仅用于开发环境,生产环境应开启缓存提升性能。 |
代码示例 1:添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
代码示例 2:创建控制器
@Controller
public class HomeController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("message", "Hello Thymeleaf!");
return "hello";
}
}
代码示例 3:创建模板文件
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Hello</title></head>
<body>
<p th:text="$ {message}">Default text</p>
</body>
</html>
第二章:Thymeleaf 基础语法
2.1 标准方言(Standard Dialect)概述
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 标准方言(Standard Dialect) | Thymeleaf 默认提供的核心功能集合,包含表达式、流程控制、属性处理等,适用于 HTML/XML 模板。 | 在 Spring Boot 中自动启用,无需额外配置。 |
| SpringStandardDialect | Spring 环境下使用的标准方言变体,使用 SpringEL(而非 OGNL)解析表达式。 | 所有 $ {} 表达式均通过 Spring 的 Expression Language 解析。 |
| 方言作用域 | 提供 th:* 命名空间下的所有属性(如 th:text、th:each 等)及内置对象(#dates、#strings 等)。 | 自定义方言可扩展此功能,但标准方言已覆盖绝大多数场景。 |
| 模板处理器 | 负责解析模板中的 th:* 属性并执行相应逻辑(如变量替换、条件判断等)。 | 处理顺序由 Thymeleaf 内部定义,开发者无需干预。 |
2.2 表达式语法($ {}、*{}、#{}、@{}、~{})
| 表达式类型 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 变量表达式 | $ {user.name} | 访问上下文中的变量(Model 中的数据),支持嵌套属性和方法调用。 | <p th:text="$ {user.email}"></p> | 若 user 为 null,会抛出异常;可配合 ?: 或 *{} 使用。 |
| 选择表达式 | *{name} | 在已选定对象(通过 th:object)上访问属性,简化写法。 | 见下方代码示例 1 | 必须在 th:object 作用域内使用,否则等同于 $ {}。 |
| 工具对象 | #{#dates.format(now, 'yyyy')} | 调用 Thymeleaf 内置工具对象(#dates、#strings、#numbers 等)。 | <span th:text="$ {#dates.format(#dates.createNow(), 'yyyy-MM-dd')}"></span> | 工具对象前需加 #,且不能省略括号。 |
| 链接 URL | @{/user/profile(id=3)} | 构造应用上下文相关的 URL,支持路径变量和查询参数。 | <a th:href="@{/order/details(id= $ {orderId})}">View Order</a> | 自动处理 context-path;相对路径以 / 开头。 |
| 片段表达式 | ~{fragments/footer :: copy} | 引用模板片段(用于 th:insert/th:replace),支持参数传递。 | <div th:insert="~{common :: footer(copyrightYear=2026)}"></div> | 片段文件路径相对于 templates 目录。 |
代码示例 1:选择表达式
<div th:object="$ {user}">
<p th:text="*{email}"></p>
</div>
2.3 文本与字面量处理
| 处理方式 | 语法/方法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 字面量文本 | 'Hello' | 在表达式中表示字符串常量。 | <p th:text="'Welcome, ' + $ {user.name}"></p> | 字符串必须用单引号包裹。 |
| 数字字面量 | 42, 3.14 | 表示数值常量。 | <span th:text="$ {price > 100 ? 'Expensive' : 'Cheap'}"></span> | 支持整数和浮点数。 |
| 布尔字面量 | true, false | 表示布尔值。 | <div th:if="$ {isActive == true}">Active</div> | 可直接使用 true/false,无需引号。 |
| 文本拼接 | + | 连接字符串、变量或表达式结果。 | <p th:text="$ {'Total: ' + #numbers.formatDecimal(total, 1, 2)}"></p> | 自动类型转换,但建议显式格式化。 |
| 转义文本输出 | th:text | 输出内容自动 HTML 转义(防止 XSS)。 | <p th:text="$ {userInput}">Default</p> | 若 userInput 含 <script>,将被转义。 |
| 非转义文本输出 | th:utext | 输出原始 HTML(不转义)。 | <div th:utext="$ {htmlContent}"></div> | 仅用于可信内容,否则有 XSS 风险。 |
2.4 属性设置(th:text、th:value、th:attr 等)
| 属性名称 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:text | th:text="$ {message}" | 设置元素的文本内容(替换标签内所有内容)。 | <h1 th:text="$ {title}">Fallback Title</h1> | 替换整个 innerHTML 文本部分。 |
th:value | th:value="$ {inputValue}" | 设置表单元素(如 input、textarea)的 value 属性。 | <input type="text" th:value="$ {username}" /> | 适用于非绑定表单;绑定表单推荐 th:field。 |
th:attr | th:attr="data-id= $ {id},title= $ {tooltip}" | 动态设置任意 HTML 属性。 | <button th:attr="disabled= $ {isDisabled}, class='btn'">Click</button> | 可同时设置多个属性,用逗号分隔。 |
th:alt/th:title | th:alt="$ {imageAlt}" | 专用于设置 alt、title 等常用属性(语法糖)。 | <img src="logo.png" th:alt="$ {siteName}" /> | 等价于 th:attr="alt= $ {siteName}",但更简洁。 |
th:placeholder | th:placeholder="#{msg.placeholder}" | 设置 input 的 placeholder 属性。 | <input type="email" th:placeholder="#{email.hint}" /> | 常与国际化消息配合使用。 |
th:style | th:style="'color:' + $ {color}" | 动态设置 style 属性。 | <span th:style="$ {'font-size: ' + fontSize + 'px'}">Text</span> | 注意 CSS 注入风险,确保值可信。 |
th:class | th:class="$ {isActive ? 'active' : 'inactive'}" | 动态设置 class 属性。 | <div th:class="$ {error ? 'alert alert-danger' : ''}">...</div> | 可结合条件表达式动态切换样式。 |
th:src/th:href | th:src="@{/images/logo.png}" | 设置资源路径(自动处理 context-path)。 | <script th:src="@{/js/app.js}"></script> | 推荐使用 @{...} 构造路径,避免硬编码。 |
第三章:流程控制与逻辑处理
3.1 条件判断(th:if、th:unless、th:switch/th:case)
| 属性/结构 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:if | th:if="$ {user != null}" | 当表达式为 true 时,渲染该元素;否则完全移除。 | 见下方代码示例 1 | 表达式结果需为布尔值或可转为布尔(null/empty 视为 false)。 |
th:unless | th:unless="$ {isEmpty}" | 当表达式为 false 时渲染元素(即 if 的反向)。 | <button th:unless="$ {isSubmitted}">Submit</button> | 等价于 th:if="$ {!isEmpty}",但语义更清晰。 |
th:switch | th:switch="$ {role}" | 多分支条件判断,配合 th:case 使用。 | 见下方代码示例 2 | 必须与 th:case 配合使用;* 表示 default 分支。 |
th:case | th:case="'ADMIN'" | 定义 switch 中的一个分支,支持字面量、变量或通配符 *。 | (见上方代码示例 2) | 字符串需加单引号;数字无需引号(如 th:case="1")。 |
| 嵌套条件 | 在 th:if 内部再使用 th:if | 实现复杂逻辑判断。 | 见下方代码示例 3 | 避免过深嵌套,建议在后端预处理逻辑。 |
代码示例 1:th:if
<div th:if="$ {isAdmin}">
<p>Admin Panel</p>
</div>
代码示例 2:th:switch/th:case
<div th:switch="$ {user.role}">
<p th:case="'ADMIN'">Administrator</p>
<p th:case="'USER'">Regular User</p>
<p th:case="*">Unknown Role</p>
</div>
代码示例 3:嵌套条件
<div th:if="$ {user != null}">
<span th:if="$ {user.active}">Active</span>
</div>
3.2 循环遍历(th:each)
| 属性/概念 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:each | th:each="item : $ {items}" | 遍历集合、数组、Map 等可迭代对象。 | 见下方代码示例 1 | 支持 List、Set、Array、Map、Iterable 等。 |
| 状态变量 | itemStat(自动注入) | 提供当前迭代状态信息(index、count、even、first、last 等)。 | 见下方代码示例 2 | 状态变量名可自定义(如 iterStat),默认为 变量名 + Stat。 |
| 状态属性 | index, count, size, even, odd, first, last | 分别表示:从0开始索引、从1开始计数、总数量、是否偶数行等。 | 见下方代码示例 3 | st.size 返回集合大小;st.last 可用于添加分隔符等。 |
| 遍历 Map | th:each="entry : $ {userMap}" | 遍历时 entry 为 Map.Entry,可用 entry.key / entry.value 访问。 | 见下方代码示例 4 | 不支持直接解构(如 key, value : map)。 |
| 空集合处理 | 结合 th:if 或提供默认内容 | 避免空列表导致 UI 异常。 | 见下方代码示例 5 | 可使用 #lists 工具对象判断空集合。 |
代码示例 1:基本遍历
<ul>
<li th:each="name : $ {userList}" th:text="$ {name}"></li>
</ul>
代码示例 2:状态变量
<tr th:each="prod, iterStat : $ {products}">
<td th:text="$ {iterStat.count}"></td>
<td th:text="$ {prod.name}"></td>
</tr>
代码示例 3:状态属性(奇偶行样式)
<div th:each="msg, st : $ {messages}"
th:class="$ {st.even} ? 'even-row' : 'odd-row'">
<p th:text="$ {msg}"></p>
</div>
代码示例 4:遍历 Map
<dl th:each="e : $ {configMap}">
<dt th:text="$ {e.key}"></dt>
<dd th:text="$ {e.value}"></dd>
</dl>
代码示例 5:空集合处理
<div th:if="$ {#lists.isEmpty(products)}">No products available.</div>
<ul th:unless="$ {#lists.isEmpty(products)}">
<li th:each="p : $ {products}">...</li>
</ul>
3.3 内联表达式与 JavaScript/CSS 中的使用(th:inline)
| 属性/用法 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:inline="text" | <span th:inline="text">Hello [[$ {name}]]!</span> | 在文本内容中直接使用 [[...]] 内联表达式,等价于 th:text。 | 见下方代码示例 1 | [[...]] 自动 HTML 转义;[($ {...})] 用于非转义(不推荐)。 |
th:inline="javascript" | <script th:inline="javascript">...</script> | 在 JavaScript 代码块中使用内联表达式,输出 JSON 或变量。 | 见下方代码示例 2 | 表达式结果会自动转为 JSON 格式(如 List → JS Array)。 |
th:inline="css" | <style th:inline="css">...</style> | 在 CSS 中动态插入值(较少使用)。 | 见下方代码示例 3 | 仅适用于简单值(颜色、尺寸等),避免复杂逻辑。 |
| 内联注释语法 | /[[$ {expr}]]/ | 在 JS/CSS 中安全嵌入 Thymeleaf 表达式,防止语法冲突。 | (见上方 JavaScript/CSS 示例) | 注释形式确保即使模板未渲染,JS 仍合法(如 userId = null)。 |
| 禁用内联 | th:inline="none" | 显式关闭内联处理(默认在 script/style 中不启用)。 | <script th:inline="none"> | 默认情况下,script 和 style 标签不启用内联,除非显式声明。 |
代码示例 1:文本内联
<p th:inline="text">Welcome, [[$ {user.name}]]! Your balance: [[$ {#numbers.formatCurrency(balance)}]].</p>
代码示例 2:JavaScript 内联
<script th:inline="javascript">
var userId = /*[[$ {userId}]]*/ null;
var userRoles = /*[[$ {roles}]]*/ [];
</script>
代码示例 3:CSS 内联
<style th:inline="css">
.highlight { color: /*[[$ {themeColor}]]*/ red; }
</style>
第四章:模板复用与布局
4.1 片段引入(th:insert、th:replace、th:include)
| 属性名称 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:insert | th:insert="~{fragments/footer :: copy}" | 将目标片段作为子节点插入到当前元素内部。 | 见下方代码示例 1 | 当前元素保留,片段内容作为其子元素插入,渲染为 <div><footer>...</footer></div>。 |
th:replace | th:replace="~{fragments/header :: nav}" | 用目标片段完全替换当前元素。 | 见下方代码示例 2 | 原始标签被完全移除,仅保留片段内容,渲染为 <nav class="top-nav">...</nav>。 |
th:include | th:include="~{fragments/sidebar :: menu}" | (Thymeleaf 3.0 起已废弃)旧版行为类似 th:insert,但仅插入片段内容(不含外层标签)。 | 见下方代码示例 3 | 不推荐使用;Thymeleaf 3.0+ 中 th:include 行为等同于 th:insert,官方建议统一用 th:insert/th:replace。 |
| 片段引用语法 | ~{template :: selector} 或 fragments/footer :: #main | 指定要引入的模板文件及其中的片段(通过 th:fragment 名称或 DOM 选择器)。 | <div th:insert="~{user/profile :: #contact-info}"></div> | 支持 CSS 选择器(如 #id、.class),但推荐使用 th:fragment 显式定义。 |
| 带参数引入 | th:insert="~{frag :: alert(msg='Error')}" | 向被引入的片段传递参数。 | <div th:insert="~{common :: alert(type='danger', message= $ {errorMsg})}"></div> | 参数在片段内通过 $ {type}、$ {message} 访问。 |
代码示例 1:th:insert
<!-- 使用前 -->
<div th:insert="~{common :: footer}"></div>
<!-- 渲染后 -->
<div><footer>...</footer></div>
代码示例 2:th:replace
<!-- 使用前 -->
<header th:replace="~{layout :: site-header}"></header>
<!-- 渲染后(假设 site-header 是 <nav class="top-nav">) -->
<nav class="top-nav">...</nav>
代码示例 3:th:include(已废弃)
<aside th:include="~{fragments :: sidebar-menu}"></aside>
4.2 使用 th:fragment 定义可复用模块
| 概念/用法 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:fragment | <footer th:fragment="site-footer">...</footer> | 定义一个可被其他模板引用的片段。 | 见下方代码示例 1 | 片段名可包含参数列表(用于接收传入值)。 |
| 无名片段 | 使用 DOM 选择器(如 #header) | 不使用 th:fragment,直接通过 ID 或 class 引用 HTML 元素。 | 见下方代码示例 2 | 依赖结构稳定性,不如 th:fragment 显式可靠。 |
| 参数默认值 | 在片段内部使用 ?: 提供默认值 | 避免未传参时出错。 | 见下方代码示例 3 | 推荐为所有参数设置默认值以增强健壮性。 |
| 嵌套片段 | 在 fragment 内部再引用其他 fragment | 构建模块化组件体系。 | 见下方代码示例 4 | 注意循环引用风险(A 引 B,B 又引 A)。 |
| 片段文件组织 | 存放于 templates 目录下的独立文件 | 提高可维护性,便于团队协作。 | 文件路径:src/main/resources/templates/components/buttons.html | 建议按功能分类(如 layout、components、forms)。 |
代码示例 1:th:fragment 定义
<!-- common.html -->
<div th:fragment="card(title, content)">
<h3 th:text="$ {title}"></h3>
<p th:text="$ {content}"></p>
</div>
代码示例 2:无名片段(通过 ID 引用)
<!-- 定义 -->
<nav id="main-nav">...</nav>
<!-- 引用 -->
<div th:replace="~{layout :: #main-nav}"></div>
代码示例 3:参数默认值
<span th:fragment="label(text)"
th:text="$ {text ?: 'Default Label'}"></span>
代码示例 4:嵌套片段
<div th:fragment="page">
<div th:replace="~{layout :: header}"></div>
<main>...</main>
<div th:insert="~{layout :: footer}"></div>
</div>
4.3 布局继承与模板装饰(Layout Dialect 可选)
| 概念/方案 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| 原生 Thymeleaf 布局 | 通过 th:replace/th:insert 手动组合 header/content/footer 实现布局复用。 | 见下方代码示例 1 | 无需额外依赖,纯标准方言实现;需约定 fragment 名称(如 content)。 |
| Thymeleaf Layout Dialect | 第三方扩展(如 nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect),提供 layout:decorator 等高级语法。 | 见下方代码示例 2 | 需添加 Maven 依赖;简化布局但增加外部依赖;Spring Boot 2.x+ 默认不集成。 |
| 装饰器模式优势 | 自动将内容”注入”到布局模板的指定位置。 | 见下方代码示例 3 | 更接近传统模板继承(如 Django、Jinja2);但需学习额外语法。 |
| 选型建议 | 简单项目用原生方案;复杂多页应用可考虑 Layout Dialect。 | — | Spring Boot 官方推荐优先使用标准方言;Layout Dialect 已多年未更新,兼容性需验证。 |
| 替代方案 | 使用 WebJars + 前端框架(如 Vue/React)实现 SPA,后端仅提供 API。 | — | 若项目前后端分离,Thymeleaf 布局意义降低;适用于服务端渲染(SSR)场景。 |
代码示例 1:原生 Thymeleaf 布局
<!-- base.html -->
<html>
<head>...</head>
<body>
<div th:replace="~{this :: content}"></div>
</body>
</html>
<!-- page.html -->
<html th:replace="~{base :: html}">
<th:block th:fragment="content">
<h1>My Page</h1>
</th:block>
</html>
代码示例 2:Layout Dialect 布局
<!-- page.html -->
<html layout:decorator="layout/base">
<title layout:title-pattern="$CONTENT_TITLE - My Site">Page Title</title>
<body>
<h1>Content Here</h1>
</body>
</html>
代码示例 3:装饰器模式中的布局模板
<!-- base.html -->
<body>
<header>...</header>
<div layout:fragment="content"></div>
<footer>...</footer>
</body>
第五章:国际化与本地化
5.1 国际化消息配置
| 配置项/文件 | 说明 | 代码示例 / 文件内容 | 注意事项 |
|---|---|---|---|
| 消息资源文件命名规则 | 格式:messages[_语言_国家].properties,如 messages_zh_CN.properties。 | 见下方代码示例 1 | 文件必须放在 src/main/resources 目录下;Spring Boot 自动加载。 |
| 支持的语言区域(Locale) | 通过 LocaleResolver 决定当前用户语言。 | Spring Boot 默认使用 Accept-Language 请求头自动解析 Locale。 | 可自定义 LocaleResolver(如基于 URL 参数或 Cookie)。 |
| 配置文件编码 | 必须为 UTF-8(Spring Boot 默认支持)。 | 在 IDE 中确保 .properties 文件保存为 UTF-8;若含中文,无需转 Unicode(Spring Boot 2.6+ 原生支持 UTF-8 properties)。 | 旧版 Spring 需手动配置 encoding,但 Spring Boot 已内置处理。 |
| 多参数消息 | 消息中可包含占位符 {0}, {1}… | 见下方代码示例 2 | 占位符从 0 开始计数,按顺序替换。 |
| 刷新缓存(开发) | 修改 messages 文件后需重启或配置 reload | application.properties:spring.messages.cache-duration=0 | 仅用于开发环境;生产环境应启用缓存提升性能。 |
代码示例 1:消息资源文件
# messages.properties(默认)
greeting=Hello
# messages_zh_CN.properties
greeting=你好
代码示例 2:多参数消息
# messages.properties
welcome.message=Welcome, {0}! You have {1} unread messages.
5.2 使用 #messages 工具对象
| 方法/表达式 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 获取简单消息 | #{'greeting'} | 根据 key 获取当前 Locale 对应的消息文本。 | <p th:text="#{greeting}"></p> | 若 key 不存在,会抛出异常;建议提供默认 fallback。 |
| 获取带参数的消息 | #{'welcome.message', user.name, count} | 替换消息中的占位符 {0}, {1}… | <p th:text="#{welcome.message($ {currentUser.name}, $ {unreadCount})}"></p> | 参数顺序必须与消息定义一致。 |
| 安静模式(不抛异常) | #{'unknown.key'}(配合默认值) | 避免因缺失 key 导致页面渲染失败。 | <p th:text="$ {#messages.msgOrNull('optional.hint') ?: 'Default Hint'}"></p> | #messages.msgOrNull() 返回 null 而非抛异常。 |
#messages.msg() | $ {#messages.msg('key')} | 在非 th:text 属性中获取消息(如 th:placeholder)。 | <input type="text" th:placeholder="$ {#messages.msg('search.placeholder')}" /> | 等价于 #{'key'},但可在任意表达式中使用。 |
#messages.msgOrNull() | $ {#messages.msgOrNull('maybe.missing')} | 安全获取消息,不存在时返回 null。 | 见下方代码示例 | 推荐用于可选内容。 |
| 消息 key 动态拼接 | #{'error.code.' + errorCode} | 根据变量动态构造 key。 | <span th:text="#{'error.code.' + $ {errorCode}}"></span> | 确保拼接后的 key 存在于资源文件中。 |
代码示例:#messages.msgOrNull() 使用
<div th:if="$ {#messages.msgOrNull('promo.banner') != null}"
th:text="#{promo.banner}"></div>
5.3 动态切换语言实现
| 步骤名称 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 配置 LocaleResolver | 定义基于 Cookie 或 Session 的 LocaleResolver。 | 见下方代码示例 1 | Cookie 方案可跨会话保持语言;Session 方案仅当前会话有效。 |
| 创建语言切换控制器 | 提供接口接收 lang 参数并设置 Locale。 | 见下方代码示例 2 | 使用 RequestContextUtils 获取已注册的 LocaleResolver。 |
| 前端语言切换链接 | 提供多语言切换按钮。 | 见下方代码示例 3 | href 路径需与控制器映射一致。 |
| 自动重定向回原页面 | 通过 Referer 或存储 returnUrl 实现无缝切换。 | (见上方控制器中 redirect: + request.getHeader("Referer")) | Referer 可能为空,需加空值判断。 |
| 浏览器默认语言适配 | 未显式设置时,按 Accept-Language 自动匹配。 | —(Spring Boot 默认行为) | 需在 messages 文件中覆盖常用语言(如 en, zh_CN)。 |
| 前端存储用户偏好 | 使用 JavaScript 读取 Cookie 并高亮当前语言。 | 见下方代码示例 4 | 仅用于 UI 反馈,实际语言由服务端控制。 |
代码示例 1:配置 LocaleResolver
@Configuration
public class LocaleConfig {
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setCookieName("LANG");
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
}
代码示例 2:语言切换控制器
@Controller
public class LanguageController {
@GetMapping("/lang/{lang}")
public String changeLang(@PathVariable String lang,
HttpServletRequest request,
HttpServletResponse response) {
LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request);
resolver.setLocale(request, response, StringUtils.parseLocaleString(lang));
return "redirect:" + request.getHeader("Referer");
}
}
代码示例 3:前端语言切换链接
<a href="/lang/en">English</a>
<a href="/lang/zh_CN">中文</a>
代码示例 4:前端读取 Cookie 高亮当前语言
<script>
const lang = document.cookie.split('; ')
.find(row => row.startsWith('LANG='))
?.split('=')[1];
if (lang) document.querySelector(`[href='/lang/${lang}']`)
.classList.add('active');
</script>
第六章:工具对象与内置功能
6.1 #dates、#calendars、#numbers、#strings 等工具对象
| 工具对象 | 方法/表达式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
#dates | ${#dates.format(date, 'yyyy-MM-dd')} | 格式化 java.util.Date 对象 | <span th:text="${#dates.format(user.birthDate, 'yyyy年MM月dd日')}"></span> | 模式字符串遵循 SimpleDateFormat 规范 |
${#dates.createNow()} | 获取当前时间(java.util.Date) | <p>Now: <span th:text="${#dates.format(#dates.createNow(), 'HH:mm:ss')}"></span></p> | 等价于 new Date(),但可在模板中直接调用 | |
${#dates.day(date)} | 提取日期中的”日”部分(1-31) | <span th:text="${#dates.day(orderDate)}"></span> | 返回 int 类型 | |
#calendars | ${#calendars.format(calendar, 'yyyy/MM/dd')} | 格式化 java.util.Calendar 对象 | <time th:text="${#calendars.format(eventCalendar, 'EEEE, MMMM dd')}"></time> | 用法与 #dates 类似,适用于 Calendar 类型 |
${#calendars.monthName(calendar)} | 获取月份全名(如 “January”) | <span th:text="${#calendars.monthName(currentCal)}"></span> | 依赖当前 Locale | |
#numbers | ${#numbers.formatDecimal(number, minInt, maxFrac)} | 格式化数字(整数位、小数位) | <span th:text="${#numbers.formatDecimal(price, 1, 2)}"></span> → 显示 “123.45” | minInt=最小整数位,maxFrac=最大小数位;不足补0 |
${#numbers.formatCurrency(amount)} | 按 Locale 格式化货币 | <span th:text="${#numbers.formatCurrency(total)}"></span> → 如 “$1,234.56” | 自动应用本地货币符号和千分位 | |
${#numbers.sequence(from, to)} | 生成数字序列(用于循环) | <select><option th:each="i : ${#numbers.sequence(1,10)}" th:text="${i}"></option></select> | 生成 [from, to] 的整数列表 | |
#strings | ${#strings.toUpperCase(str)} | 转大写 | <span th:text="${#strings.toUpperCase(username)}"></span> | 支持 null 安全(null 输入返回 null) |
${#strings.abbreviate(str, width)} | 截断字符串并加 ”…” | <p th:text="${#strings.abbreviate(description, 50)}"></p> | 若长度 ≤ width 则原样返回 | |
${#strings.defaultString(str, 'N/A')} | 提供默认值(当 str 为 null 或空) | <span th:text="${#strings.defaultString(email, '未填写')}"></span> | 空字符串("")也被视为需替换 | |
${#strings.contains(str, 'keyword')} | 判断是否包含子串 | <div th:if="${#strings.contains(comment, 'spam')}">Flagged</div> | 区分大小写 | |
#lists / #sets | ${#lists.isEmpty(list)} | 判断 List 是否为空 | <div th:if="${#lists.isEmpty(items)}">No items</div> | 推荐用于条件判断,比直接判 null 更安全 |
${#lists.size(list)} | 获取 List 长度 | <span th:text="${'Total: ' + #lists.size(products)}"></span> | 等价于 list.size(),但支持 null(返回 0) |
6.2 URL 处理(@{} 与 #httpServletRequest)
| 表达式/对象 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@{}(链接表达式) | @{/user/profile} | 生成相对于应用上下文的绝对路径 | <a th:href="@{/dashboard}">Dashboard</a> → 渲染为 <a href="/myapp/dashboard"> | 自动处理 server.servlet.context-path |
@{/order/{id}(id=${orderId})} | 路径变量替换 | <a th:href="@{/product/{pid}(pid=${product.id})}">View</a> | 支持多个路径变量 | |
@{/search(q=${query}, page=2)} | 添加查询参数 | <a th:href="@{/api/data(format='json', token=${authToken})}">API</a> | 参数自动 URL 编码 | |
@{https://example.com} | 绝对 URL(不加 context-path) | <img th:src="@{https://cdn.example.com/logo.png}"> | 以 http(s):// 开头时视为外部资源 | |
#httpServletRequest | ${#httpServletRequest.requestURI} | 获取当前请求 URI | <input type="hidden" name="redirect" th:value="${#httpServletRequest.requestURI}"> | 可用于登录后跳转回原页面 |
${#httpServletRequest.remoteAddr} | 获取客户端 IP | <p>IP: <span th:text="${#httpServletRequest.remoteAddr}"></span></p> | 注意代理环境下可能需读 X-Forwarded-For | |
${#httpServletRequest.getParameter('lang')} | 获取请求参数 | <span th:if="${#httpServletRequest.getParameter('debug') == 'true'}">Debug Mode</span> | 不推荐在模板中直接读参,应由 Controller 处理 | |
| 相对路径 vs 绝对路径 | @{profile}(无前导 /) | 相对于当前路径 | 若当前页为 /user/settings,则 @{profile} → /user/profile | 建议始终使用 @{/xxx} 避免歧义 |
| 静态资源处理 | @{/css/style.css} | 引用静态资源(自动加版本戳,若启用) | <link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}"> | Spring Boot 默认映射 /static、/public、/resources、/META-INF/resources |
6.3 安全表达式(Spring Security 集成)
| 安全表达式 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
sec:authorize | <div sec:authorize="hasRole('ADMIN')"> | 根据权限控制元素渲染(需引入 security dialect) | <button sec:authorize="hasAuthority('USER_DELETE')">Delete</button> | 必须添加 Thymeleaf Spring Security 依赖 |
#authentication | ${#authentication.name} | 获取当前认证用户名 | <span>Welcome, <b th:text="${#authentication.name}"></b>!</span> | 若未登录,#authentication 为 null |
${#authentication.principal.email} | 访问 UserDetails 自定义属性 | <p>Email: <span th:text="${#authentication?.principal?.email}"></span></p> | 使用 ?. 防止 NPE;需自定义 UserDetailsService 返回含 email 的对象 | |
${#authentication.authorities} | 获取权限集合 | <div th:each="auth : ${#authentication.authorities}" th:text="${auth.authority}"></div> | authorities 是 GrantedAuthority 列表 | |
| 内联权限判断 | th:if="${#authorization.expression('hasRole(''ADMIN'')')}" | 在标准方言中使用安全表达式 | <div th:if="${#authorization.expression('isAuthenticated()')}">Logout</div> | 需启用 #authorization 工具对象(Spring Security 集成后自动可用) |
| 常用表达式 | isAuthenticated() | 判断是否已认证 | <a th:if="${#authorization.expression('!isAuthenticated()')}" th:href="@{/login}">Login</a> | 其他:isAnonymous(), isRememberMe(), hasAnyRole('A','B') |
| 依赖配置 | Maven 依赖 | 启用 sec:* 属性支持 | 见下方代码示例 1 | Spring Boot 3.x 需用 springsecurity6;2.x 用 springsecurity5 |
| 命名空间声明 | xmlns:sec="http://www.thymeleaf.org/extras/spring-security" | 在 HTML 中启用 sec 前缀 | 见下方代码示例 2 | 仅用于 IDE 提示,运行时非必需 |
代码示例 1:Maven 依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
代码示例 2:命名空间声明
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
第七章:表单处理与数据绑定
7.1 th:object 与 *{} 表达式
| 概念/属性 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:object | th:object="$ {userForm}" | 将一个对象绑定为当前表单的作用域对象,后续可使用 *{} 简化属性访问。 | 见下方代码示例 1 | 绑定对象必须存在于 Model 中;通常为 DTO 或实体类实例。 |
*{} 表达式 | *{email} | 在 th:object 作用域内访问对象属性,等价于 $ {user.email}。 | <p>Current email: <span th:text="*{email}"></span></p> | 仅在 th:object 子元素中有效;脱离作用域将报错。 |
| 嵌套属性访问 | *{address.city} | 访问对象的嵌套属性(需确保中间对象非 null)。 | <input type="text" th:field="*{profile.phoneNumber}" /> | 若 profile 为 null,会抛出 PropertyAccessException;建议在后端初始化嵌套对象。 |
与 $ {} 混用 | th:value="$ {prefix} + *{suffix}" | 在同一表达式中混合使用全局变量和绑定对象属性。 | <input type="hidden" th:value="'ID_' + *{id}" /> | *{} 仍受限于 th:object 作用域。 |
| 作用域限制 | *{} 不能用于 th:object 外部 | 防止误用导致解析失败。 | 见下方代码示例 2 | 确保 *{} 位于 th:object 标签内部(包括子元素)。 |
代码示例 1:th:object 与 *{}
<form th:object="$ {user}" action="#" method="post">
<input type="text" th:value="*{name}" />
</form>
代码示例 2:作用域限制(错误示例)
<div th:object="$ {user}"></div>
<p th:text="*{name}"></p> <!-- 脱离作用域 -->
7.2 表单字段绑定(th:field)
| 属性/用法 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:field | th:field="*{username}" | 自动绑定表单字段到对象属性,自动设置 name、value、type 等属性。 | <input type="text" th:field="*{username}" /> → 渲染为 <input type="text" name="username" value="john" /> | 推荐用于 Spring MVC 表单绑定;比 th:value 更强大。 |
| 自动类型转换 | th:field="*{birthDate}"(Date 类型) | 支持日期、布尔、数字等类型的自动格式化与回填。 | <input type="date" th:field="*{birthDate}" /> | 日期格式需与 Spring 的 @DateTimeFormat 一致。 |
| 复选框(checkbox) | th:field="*{newsletter}" | 自动处理布尔值:checked 状态与 value 同步。 | <input type="checkbox" th:field="*{newsletter}" /> | 不要手动设置 value="true";Thymeleaf 自动处理。若 newsletter=true,则渲染 checked="checked"。 |
| 多选复选框 | th:field="*{roles}" | 绑定 List 或数组,自动匹配选中项。 | 见下方代码示例 1 | roles 需为 Collection 类型;value 必须显式指定。 |
| 单选按钮(radio) | th:field="*{gender}" | 根据字段值自动选中对应 radio。 | 见下方代码示例 2 | 所有 radio 共享同一 th:field 表达式。 |
| 下拉选择(select) | th:field="*{country}" | 自动选中 option;需配合 th:each 遍历选项。 | 见下方代码示例 3 | option 的 value 必须与绑定属性值类型一致(如 String vs Enum)。 |
| 禁用手动设置 name/value | — | 使用 th:field 后,不应再设置 name、value、checked 等属性。 | 见下方代码示例 4 | Thymeleaf 会覆盖手动属性,导致绑定失效。 |
代码示例 1:多选复选框
<input type="checkbox" th:field="*{roles}" value="ADMIN" />
<input type="checkbox" th:field="*{roles}" value="USER" />
代码示例 2:单选按钮
<input type="radio" th:field="*{gender}" value="M" /> Male
<input type="radio" th:field="*{gender}" value="F" /> Female
代码示例 3:下拉选择
<select th:field="*{country}">
<option th:each="c : ${countries}"
th:value="${c.code}"
th:text="${c.name}"></option>
</select>
代码示例 4:错误示例(禁用手动设置)
<!-- 错误:手动设置 name 会导致绑定失效 -->
<input type="text" th:field="*{name}" name="customName" />
7.3 错误信息展示(#fields.hasErrors、th:errors)
| 方法/属性 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
th:errors | th:errors="*{email}" | 显示指定字段的所有验证错误信息(来自 JSR-303 / Spring Validation)。 | <div th:errors="*{email}" class="error"></div> | 自动遍历 BindingResult 中该字段的错误;若无错误则不渲染元素。 |
#fields.hasErrors() | $ {#fields.hasErrors('email')} | 判断某字段是否有错误,常用于条件样式。 | 见下方代码示例 1 | 参数为字段名字符串(非 *{email} 表达式)。 |
#fields.errors() | $ {#fields.errors('password')} | 获取某字段的错误信息列表(List)。 | 见下方代码示例 2 | 可用于自定义错误展示结构。 |
| 全局错误 | th:errors="*" | 显示对象级别的全局错误(非字段绑定错误)。 | <div th:errors="*" class="alert alert-danger"></div> | 通常由 Validator 抛出 ObjectError。 |
| 字段名转义 | — | 字段名支持嵌套(如 "address.city")。 | <span th:if="$ {#fields.hasErrors('address.city')}">Invalid city</span> | 与 th:field 中的路径一致。 |
| 国际化错误消息 | messages.properties 中定义 | 错误消息可本地化。 | 见下方代码示例 3 | 占位符 {0}=字段名, {1}=max, {2}=min(依注解而定)。 |
| 自定义 CSS 类 | 结合 th:classappend | 动态添加错误样式类。 | 见下方代码示例 4 | 避免覆盖原有 class,使用 classappend 而非 class。 |
代码示例 1:#fields.hasErrors() 条件样式
<input type="email"
th:field="*{email}"
th:class="$ {#fields.hasErrors('email')} ? 'form-control is-invalid' : 'form-control'" />
代码示例 2:#fields.errors() 自定义错误展示
<ul th:if="$ {#fields.hasErrors('password')}">
<li th:each="err : $ {#fields.errors('password')}" th:text="$ {err}"></li>
</ul>
代码示例 3:国际化错误消息
# messages_zh_CN.properties
Size.user.password=密码长度必须在 {2} 到 {1} 位之间
代码示例 4:th:classappend 动态添加错误样式
<input type="text"
th:field="*{phone}"
th:classappend="$ {#fields.hasErrors('phone')} ? 'is-invalid'" />
第八章:高级特性与性能优化
8.1 模板缓存机制
| 配置项 / 概念 | 说明 | 代码示例 / 配置方式 | 注意事项 |
|---|---|---|---|
| 默认缓存行为 | Thymeleaf 在生产模式下默认启用模板缓存,提升渲染性能。 | Spring Boot 应用中无需额外配置;devtools 启用时自动禁用缓存。 | 开发环境建议关闭缓存以便实时预览修改。 |
| 缓存控制配置 | 通过 application.properties 控制缓存开关与有效期。 | 见下方代码示例 | cache-ttl 仅在 Thymeleaf 3.0.12+ 支持;旧版需自定义 TemplateResolver。 |
| 禁用缓存(开发) | 便于调试模板变更。 | application.properties:spring.thymeleaf.cache=false | 或通过启动参数:--spring.thymeleaf.cache=false |
| 手动清除缓存 | 在运行时强制刷新模板(适用于动态模板场景)。 | 见下方代码示例 | 仅在确实需要动态更新模板时使用;频繁调用影响性能。 |
| 缓存粒度 | 按模板名称缓存解析后的模板结构(非 HTML 字符串)。 | — | 多个请求渲染同一模板共享缓存;不同模板独立缓存。 |
| 生产环境建议 | 始终启用缓存,避免重复解析模板文件。 | — | 若使用外部存储(如数据库存模板),需自行实现缓存失效逻辑。 |
代码示例:缓存控制与手动清除
# application.properties
spring.thymeleaf.cache=true
spring.thymeleaf.cache-ttl=3600000 # 1小时(毫秒)
@Autowired
private TemplateEngine templateEngine;
public void clearCache() {
templateEngine.getConfiguration().getTemplateManager().clearCaches();
}
8.2 自定义方言(Dialect)开发
| 步骤名称 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 创建方言类 | 继承 AbstractProcessorDialect 或实现 IDialect 接口。 | 见下方代码示例 1 | 方言前缀(如 “my”)用于 th:* 替代(如 my:text)。 |
| 实现自定义处理器 | 继承 AbstractAttributeTagProcessor,重写 doProcess 方法。 | 见下方代码示例 2 | processor 名称即为属性名(如 “highlight” → my:highlight)。 |
| 注册方言 | 在 Spring Boot 中声明为 @Bean。 | 见下方代码示例 3 | Thymeleaf 自动发现所有 IDialect 类型的 Bean。 |
| 使用自定义属性 | 在模板中使用新方言前缀。 | <p my:highlight="true">This text will be highlighted.</p> | 需在 HTML 标签中声明命名空间(可选,仅 IDE 提示用):xmlns:my="http://example.com/mydialect" |
| 表达式对象扩展 | 通过 IExpressionObjectDialect 添加全局工具对象(如 #myUtils)。 | 见下方代码示例 4 | 工具对象可在表达式中调用:$ {#myUtils.doSomething()} |
| 调试技巧 | 日志输出处理器执行过程。 | 在 doProcess 中添加日志:System.out.println("Processing tag: " + tag.getElementCompleteName()); | 避免在生产环境打印过多日志。 |
代码示例 1:创建方言类
public class MyDialect extends AbstractProcessorDialect {
public MyDialect() {
super("MyDialect", "my", StandardDialect.PROCESSOR_PRECEDENCE);
}
@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
Set<IProcessor> processors = new HashSet<>();
processors.add(new MyCustomAttributeTagProcessor(dialectPrefix));
return processors;
}
}
代码示例 2:实现自定义处理器
public class MyCustomAttributeTagProcessor extends AbstractAttributeTagProcessor {
public MyCustomAttributeTagProcessor(String dialectPrefix) {
super(TemplateMode.HTML, dialectPrefix, "highlight",
true, null, false, 0);
}
@Override
protected void doProcess(ITemplateContext context,
IProcessableElementTag tag,
AttributeName attributeName,
String attributeValue,
IElementTagStructureHandler structureHandler) {
structureHandler.setAttribute("style", "background-color: yellow;");
}
}
代码示例 3:注册方言
@Bean
public MyDialect myDialect() {
return new MyDialect();
}
代码示例 4:表达式对象扩展
public class MyExpressionObjectDialect extends AbstractExpressionObjectDialect {
@Override
public Map<String, Object> getExpressionObjects(ITemplateContext context) {
Map<String, Object> objects = new HashMap<>();
objects.put("myUtils", new MyUtilityClass());
return objects;
}
}
8.3 模板预处理与后处理
| 处理阶段 | 机制 / 接口 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 预处理 | ITemplatePreProcessor | 在模板解析前修改原始模板文本(如替换占位符、注入全局变量)。 | 见下方代码示例 1 | 返回修改后的字符串;影响后续所有解析步骤。 |
| 后处理 | ITemplatePostProcessor | 在模板渲染完成后修改最终 HTML 输出(如压缩、添加统计代码)。 | 见下方代码示例 2 | 适用于 SEO 优化、安全头注入等场景。 |
| 注册预/后处理器 | 通过 TemplateEngine 配置 | 将自定义处理器加入 Thymeleaf 引擎。 | 见下方代码示例 3 | 多个处理器按添加顺序执行。 |
| 使用场景示例 | 预处理:多租户模板定制 | 动态适配不同客户 UI;增强安全策略。 | 预处理:替换 {{TENANT_LOGO}} 为实际 URL | 预处理操作需高效,避免正则回溯爆炸。 |
| 后处理:添加 CSP nonce | 后处理:在 <script> 标签插入 nonce="$ {nonce}" | — | — | |
| 性能影响 | 预/后处理在每次渲染时执行 | 可能成为性能瓶颈。 | — | 仅在必要时使用;复杂逻辑建议移至 Controller 或 View 层。 |
| 与缓存关系 | 预处理在缓存前执行,后处理在缓存后执行 | 预处理结果可被缓存,后处理结果不可缓存。 | 若模板经预处理后固定,则缓存有效;后处理每次生成新 HTML,无法缓存。 | 动态后处理(如含时间戳)会降低缓存命中率。 |
代码示例 1:预处理(去除注释)
public class CommentStripperPreProcessor implements ITemplatePreProcessor {
@Override
public String process(ITemplateContext context, String templateContent) {
return templateContent.replaceAll("<!--.*?-->", "");
}
}
代码示例 2:后处理(HTML 压缩)
public class HtmlMinifierPostProcessor implements ITemplatePostProcessor {
@Override
public String process(ITemplateContext context, String renderedHtml) {
return renderedHtml.replaceAll("\\s+", " ");
}
}
代码示例 3:注册预/后处理器
@Bean
public TemplateEngine templateEngine(SpringStandardDialect springDialect) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolvers(Arrays.asList(templateResolver()));
engine.addDialect(springDialect);
engine.addTemplatePreProcessor(new MyPreProcessor());
engine.addTemplatePostProcessor(new MyPostProcessor());
return engine;
}
第九章:Thymeleaf 与 Spring 生态集成
9.1 Spring MVC 数据模型传递
| 机制 / 注解 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@Controller + Model | public String home(Model model) | 将数据添加到模板上下文,供 Thymeleaf 渲染使用。 | 见下方代码示例 1 | 返回值为模板路径(不含 .html 后缀);自动解析为 templates/user/list.html。 |
@ModelAttribute | @ModelAttribute("currentUser") | 全局或方法级预加载公共数据(如用户信息、菜单)。 | 见下方代码示例 2 | 所有控制器方法均可访问 currentUser 变量。 |
RedirectAttributes | redirectAttributes.addFlashAttribute(...) | 重定向时传递一次性消息(如成功提示)。 | 见下方代码示例 3 | Flash 属性仅在下一次请求中可用,适合 POST-REDIRECT-GET 模式。 |
直接返回对象(@ResponseBody 冲突) | ❌ 不适用 | Thymeleaf 视图需返回 String 或 ModelAndView,不能返回 JSON 对象。 | 见下方代码示例 4 | 若需同时支持 HTML 和 JSON,应使用内容协商或分开接口。 |
ModelAndView | return new ModelAndView("view", "data", obj); | 显式构建视图与模型(较少用,Model 更简洁)。 | return new ModelAndView("product/detail", "product", productService.findById(id)); | 功能等价于 model.addAttribute + return "view"。 |
| 模板变量命名规范 | 使用语义化名称(如 userList 而非 list) | 提高模板可读性与维护性。 | model.addAttribute("orderSummary", summary); | 避免使用 Java 关键字或 Thymeleaf 保留词(如 object、field)。 |
代码示例 1:@Controller + Model
@Controller
public class UserController {
@GetMapping("/users")
public String listUsers(Model model) {
model.addAttribute("users", userService.findAll());
model.addAttribute("title", "User List");
return "user/list";
}
}
代码示例 2:@ModelAttribute 全局预加载
@ControllerAdvice
public class GlobalModel {
@ModelAttribute("currentUser")
public User getCurrentUser(Authentication auth) {
return auth != null ? (User) auth.getPrincipal() : null;
}
}
代码示例 3:RedirectAttributes Flash 消息
@PostMapping("/save")
public String save(User user, RedirectAttributes ra) {
userService.save(user);
ra.addFlashAttribute("message", "Saved successfully!");
return "redirect:/users";
}
代码示例 4:错误示例(返回对象而非视图)
// 错误:会返回 JSON 而非 HTML
@GetMapping("/page")
public List<User> getData() { ... }
9.2 Spring Security 权限控制标签
| 安全表达式 / 标签属性 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
sec:authorize | <div sec:authorize="hasRole('ADMIN')"> | 根据权限表达式决定是否渲染元素(需引入 security dialect)。 | <button sec:authorize="hasRole('ADMIN')">Delete</button> | 必须添加 thymeleaf-extras-springsecurity 依赖。 |
sec:authentication | <span sec:authentication="name"></span> | 直接输出认证对象的属性(如用户名、权限)。 | <span sec:authentication="name">Hello!</span> | 等价于 ${#authentication.name},但更简洁。 |
hasRole('ROLE') | 表达式:hasRole('ADMIN') | 判断用户是否拥有指定角色(自动加 ROLE_ 前缀)。 | <div sec:authorize="hasRole('ADMIN')">Reports</div> | 若数据库角色存为 “ROLE_ADMIN”,则表达式写 hasRole('ADMIN')。 |
hasAuthority('AUTH') | 表达式:hasAuthority('ORDER_CREATE') | 判断是否拥有指定权限(不加前缀,精确匹配)。 | <div sec:authorize="hasAuthority('ORDER_CREATE')">Create Order</div> | 推荐用于细粒度权限控制。 |
isAuthenticated() | 表达式:isAuthenticated() | 判断用户是否已通过认证(非匿名)。 | <a sec:authorize="isAuthenticated()">Logout</a> | 匿名用户(未登录)返回 false。 |
isAnonymous() | 表达式:isAnonymous() | 判断是否为匿名用户。 | <a sec:authorize="isAnonymous()">Login</a> | 与 !isAuthenticated() 等价。 |
hasAnyRole(...) | 表达式:hasAnyRole('USER','ADMIN') | 满足任一角色即授权。 | <div sec:authorize="hasAnyRole('USER','ADMIN')">Content Editor</div> | 参数为字符串列表,自动加 ROLE_ 前缀。 |
| 依赖配置 | Maven 依赖 | 启用 sec:* 标签支持。 | 见下方代码示例 1 | Spring Boot 3.x 使用 springsecurity6;2.x 用 springsecurity5。 |
| 命名空间声明 | xmlns:sec="..." | 在 HTML 中启用 sec 前缀(IDE 友好)。 | 见下方代码示例 2 | 运行时非必需,但建议保留以获 IDE 支持。 |
代码示例 1:Maven 依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
代码示例 2:命名空间声明
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
9.3 与 WebFlux 的兼容性(Reactive 支持)
| 特性 / 限制 | 说明 | 代码示例 / 配置方式 | 注意事项 |
|---|---|---|---|
| Thymeleaf 官方支持 | 自 3.0.8 起支持 Spring WebFlux(反应式渲染)。 | 见下方代码示例 1 | 无需额外依赖;Thymeleaf 自动适配反应式上下文。 |
| 控制器返回类型 | 使用 Mono<String> 或 Flux<?> 传递模型数据。 | 见下方代码示例 2 | 模型填充必须在返回 Mono 之前完成(通过 doOnNext 等操作符)。 |
| 模板引擎自动配置 | Spring Boot 自动配置 ThymeleafReactiveViewResolver。 | — | 无需手动配置 ViewResolver。 |
| 不支持的特性 | th:include(已废弃)、部分同步工具对象(如 #httpServletRequest) | ❌ 无法在模板中使用 ${#httpServletRequest} | 反应式环境下无 HttpServletRequest,应使用 ServerHttpRequest。 |
| 替代方案 | 使用 WebSession 代替 HttpSession | 见下方代码示例 3 | WebSession 是反应式会话对象。 |
| 性能考量 | 模板渲染仍为阻塞操作,但由专用线程池处理 | — | 高并发场景下需监控线程池;避免在模板中执行耗时逻辑。 |
| 流式渲染(实验性) | Thymeleaf 3.1+ 支持 I/O 流式输出(需手动配置) | 需自定义 ReactiveTemplateEngine 并启用流式模式 | 目前 Spring Boot 未默认启用;适用于超大页面分块传输。 |
代码示例 1:WebFlux 依赖(Gradle)
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
代码示例 2:Reactive 控制器
@Controller
public class ReactiveController {
@GetMapping("/reactive")
public Mono<String> view(Model model) {
return userService.findAll()
.collectList()
.doOnNext(users -> model.addAttribute("users", users))
.thenReturn("reactive/list");
}
}
代码示例 3:WebSession 替代方案
@GetMapping("/cart")
public Mono<String> cart(WebSession session, Model model) {
model.addAttribute("items", session.getAttribute("cart"));
return Mono.just("cart/view");
}
第十章:调试、测试与部署
10.1 模板语法错误排查
| 步骤名称 | 操作细节 | 代码示例 / 日志片段 | 注意事项 |
|---|---|---|---|
| 查看控制台错误日志 | Thymeleaf 在解析失败时会抛出 TemplateProcessingException。 | org.thymeleaf.exceptions.TemplateInputException: Error resolving template "user/profile", template might not exist or might not be accessible. | 首先确认模板路径是否正确(相对于 templates/ 目录)。 |
| 检查属性拼写 | 常见错误:th:tex → 应为 th:text;*{emial} → 应为 *{email}。 | 错误日志:org.thymeleaf.exceptions.PropertyAccessException: Could not access property "emial" of context object | IDE 插件(如 Thymeleaf Live Templates)可辅助提示。 |
| 验证表达式合法性 | 确保 ${}、*{}、@{} 表达式括号匹配、引号闭合。 | 错误示例:<span th:text="$ {user.name'}"></span> ← 单双引号混用 | 字符串字面量建议统一用单引号:'static text'。 |
| 检查对象是否为 null | 若绑定对象未传入 Model,*{} 表达式会报错。 | 控制器未调用 model.addAttribute("user", ...),但模板使用 th:object="$ {user}" | 使用 th:if="$ {user != null}" 包裹或提供默认值。 |
| 启用详细日志 | 配置 logging.level.org.thymeleaf=DEBUG | 见下方代码示例 | 开发阶段开启,便于定位表达式求值过程。 |
| 模板编码问题 | 文件保存为非 UTF-8 导致中文乱码或解析失败。 | 消息文件含中文但未保存为 UTF-8(旧版需转 Unicode) | Spring Boot 2.6+ 原生支持 UTF-8 .properties,但仍需 IDE 正确设置。 |
| 标签未闭合或嵌套错误 | HTML 结构不合法(如 <p> 内嵌 <div>)可能被浏览器修正,但 Thymeleaf 严格解析。 | <p><div>...</div></p> → 解析异常 | 使用 W3C 验证器检查 HTML 合法性。 |
代码示例:启用详细日志
# application.properties
logging.level.org.thymeleaf.templateparser=TRACE
logging.level.org.thymeleaf.expressions=DEBUG
10.2 单元测试模板渲染结果
| 测试方法 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 使用 SpringBootTest | 启动完整上下文,模拟请求并断言响应内容。 | 见下方代码示例 1 | 适用于端到端测试;启动较慢。 |
| 模拟 TemplateEngine 渲染 | 直接调用 TemplateEngine 渲染字符串,无需 HTTP 层。 | 见下方代码示例 2 | 需注入 TemplateEngine;适合测试片段模板。 |
| 验证模型数据传递 | 使用 MockMvc 检查 Controller 是否正确添加属性。 | 见下方代码示例 3 | 不验证 HTML,仅验证 Model 内容。 |
| 测试安全标签渲染 | 结合 @WithMockUser 模拟认证用户,验证 sec:authorize 行为。 | 见下方代码示例 4 | 需启用 Spring Security 测试支持。 |
| 断言特定元素存在 | 使用 HtmlUnit 或 JSoup 解析 HTML 并查询节点。 | 见下方代码示例 5 | 比字符串 contains 更精准,避免误匹配。 |
| 覆盖错误消息测试 | 模拟表单提交错误,验证 th:errors 是否渲染。 | 见下方代码示例 6 | 需配置 BindingResult 和 Validator。 |
代码示例 1:@SpringBootTest 端到端测试
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
class UserControllerTest {
@LocalServerPort int port;
@Test
void testUserListPage() {
String body = RestAssured.given().port(port)
.when().get("/users")
.then().statusCode(200)
.extract().asString();
assertThat(body).contains("User Management");
}
}
代码示例 2:直接调用 TemplateEngine 渲染
@Test
void testTemplateRendersCorrectly() {
Context context = new Context();
context.setVariable("message", "Hello Test");
String result = templateEngine.process("fragments/alert", context);
assertThat(result).contains("Hello Test");
}
代码示例 3:MockMvc 验证模型数据
@Test
void shouldAddUsersToModel() throws Exception {
mockMvc.perform(get("/users"))
.andExpect(model().attributeExists("users"))
.andExpect(model().attribute("users", hasSize(2)));
}
代码示例 4:@WithMockUser 测试安全标签
@Test
@WithMockUser(roles = "ADMIN")
void adminButtonVisible() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(content().string(containsString("Delete User")));
}
代码示例 5:JSoup 解析 HTML 断言元素
Document doc = Jsoup.parse(renderedHtml);
Element emailInput = doc.selectFirst("input[name=email]");
assertThat(emailInput.attr("value")).isEqualTo("test@example.com");
代码示例 6:表单错误消息测试
mockMvc.perform(post("/register")
.param("email", ""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("邮箱不能为空")));
10.3 生产环境配置建议
| 配置项 | 推荐值 / 方式 | 说明 | 注意事项 |
|---|---|---|---|
| 模板缓存 | spring.thymeleaf.cache=true | 提升性能,避免重复解析模板。 | 必须启用;开发环境可设为 false。 |
| 模板模式 | spring.thymeleaf.mode=HTML | 确保按 HTML5 规范解析(而非 XML、LEGACYHTML5 等)。 | 默认即为 HTML,通常无需显式设置。 |
| 缓存 TTL(可选) | spring.thymeleaf.cache-ttl=3600000(1小时) | 定期刷新缓存(Thymeleaf 3.0.12+ 支持)。 | 适用于模板可能动态更新的场景(如 CMS)。 |
| 禁用模板解析日志 | logging.level.org.thymeleaf=INFO | 避免输出大量 DEBUG/TRACE 日志影响性能。 | 生产环境应限制日志级别。 |
| 静态资源缓存 | 配置 Cache-Control 头(由 Spring ResourceHandler 处理) | spring.web.resources.cache.cachecontrol.max-age=3600 | Thymeleaf 模板本身不处理静态资源缓存。 |
| 模板位置 | 默认 classpath:/templates/ | 不建议修改;若需外部化(如热部署),可配置 spring.thymeleaf.prefix=file:/opt/app/templates/ | 外部模板需确保权限与安全访问控制。 |
| XSS 防护 | 默认自动转义(如 th:text) | 用户输入通过 th:text 输出时自动 HTML 转义。 | 若使用 th:utext(不转义),必须手动过滤(如 OWASP Java Encoder)。 |
| 错误页面定制 | 实现 /error 模板(如 error.html) | Spring Boot 自动渲染 templates/error.html。 | 可通过 @ControllerAdvice 全局异常处理补充模型数据。 |
| 内存与线程池 | 监控 TemplateEngine 的缓存内存占用 | Thymeleaf 缓存模板结构,大项目可能占用数百 MB 内存。 | 高并发下确保 JVM 堆内存充足。 |
| 安全头集成 | 结合 Spring Security 添加 CSP、X-Content-Type-Options 等 | 配置 SecurityFilterChain 添加 headers() | Thymeleaf 本身不处理 HTTP 头,需框架层支持。 |
第十一章:代码生成
11.1 配置依赖
| 依赖名称 | 语法(Maven) | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Thymeleaf | 见下方代码示例 1 | 提供模板引擎核心功能,用于渲染代码模板 | 见下方代码示例 1 | 若用于 Spring Boot 项目,建议使用 spring-boot-starter-thymeleaf。 |
| Spring Boot Starter Thymeleaf | 见下方代码示例 2 | 自动配置 Thymeleaf,集成 Spring MVC | 见下方代码示例 2 | 默认模板路径为 src/main/resources/templates/,生成代码时需覆盖或自定义模板解析器。 |
| MyBatis-Plus Generator | 见下方代码示例 3 | 提供代码生成器,可配合 Thymeleaf 模板 | 见下方代码示例 3 | 需额外引入 Velocity 或 Freemarker/Thymeleaf 作为模板引擎。 |
| Thymeleaf Extras Java8Time | 见下方代码示例 4 | 支持在模板中使用 Java 8 时间格式 | <span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd')}"> | 仅在模板中需要处理日期时间时使用。 |
代码示例 1:Thymeleaf 核心依赖
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
代码示例 2:Spring Boot Starter Thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
代码示例 3:MyBatis-Plus Generator
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.5</version>
</dependency>
代码示例 4:Thymeleaf Extras Java8Time
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.1.0</version>
</dependency>
11.2 生成后端代码
| 方法/组件 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| TemplateEngine 初始化 | TemplateEngine templateEngine = new TemplateEngine(); | 创建并配置 Thymeleaf 引擎用于代码生成 | 见下方代码示例 1 | 必须设置 TemplateMode 为 TEXT,否则会转义 HTML 特殊字符。 |
| 渲染模板生成 Java 类 | String result = templateEngine.process("EntityTemplate", context); | 使用上下文数据渲染模板,输出 Java 代码字符串 | 见下方代码示例 2 | 模板文件建议命名为 .java.thymeleaf,避免 IDE 误识别。 |
| 写入文件 | Files.write(Paths.get(...), javaCode.getBytes(StandardCharsets.UTF_8)); | 将生成的代码写入磁盘 | 见下方代码示例 3 | 需提前创建目录,注意文件编码统一为 UTF-8。 |
代码示例 1:TemplateEngine 初始化
TemplateResolver resolver = new ClassLoaderTemplateResolver();
resolver.setPrefix("templates/code/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.TEXT);
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(resolver);
代码示例 2:渲染模板生成 Java 类
Context context = new Context();
context.setVariable("entityName", "User");
context.setVariable("fields", fieldList);
String javaCode = templateEngine.process("Entity.java.thymeleaf", context);
代码示例 3:写入文件
Files.write(Paths.get("src/main/java/com/example/entity/User.java"),
javaCode.getBytes(StandardCharsets.UTF_8));
11.3 生成前端代码
| 方法/组件 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Thymeleaf 模板渲染 HTML | templateEngine.process("user-form.html", context) | 生成包含表单、列表等结构的 HTML 页面 | 见下方代码示例 | 模板中可使用 Thymeleaf 表达式如 th:each、th:text。 |
| 静态资源引用处理 | <link th:href="@{/css/style.css}" rel="stylesheet" /> | 在生成的 HTML 中正确引用静态资源 | — | 若用于离线生成(非 Web 环境),需禁用链接重写或替换为绝对路径。 |
| 前端模板变量注入 | ctx.setVariable("apiBaseUrl", "/api/v1"); | 动态注入 API 地址、标题等配置 | — | 避免硬编码,提升模板复用性。 |
代码示例:生成前端 HTML
Context ctx = new Context();
ctx.setVariable("columns", Arrays.asList("name", "email"));
String html = templateEngine.process("list-page.thymeleaf", ctx);
11.4 生成 SQL 脚本
| 方法/组件 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| SQL 模板定义 | CREATE TABLE [[${tableName}]] (...); | 使用 Thymeleaf 的 [[...]] 输出未转义文本 | 见下方代码示例 1 | 必须使用 TemplateMode.TEXT,否则括号会被转义。 |
| 渲染 SQL 文件 | templateEngine.process("create-table.sql.thymeleaf", context) | 生成建表、索引、初始化数据等 SQL 脚本 | 同 11.2 示例 | 注意字段类型映射(如 Java String → VARCHAR(255))。 |
| 批量生成多表脚本 | 循环调用 process 方法,传入不同表上下文 | 一次性生成多个表的 DDL | for (Table t : tables) { ... } | 可结合数据库元数据自动提取表结构。 |
代码示例 1:SQL 模板定义
CREATE TABLE [[${table.name}]] (
id BIGINT PRIMARY KEY,
[# th:each="col : ${columns}"]
[[${col.name}]] [[${col.type}]][# th:if="${!colStat.last}"],[/th:if]
[/th:each]
);
11.5 生成配置文件
| 配置类型 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
application.yml 模板 | 见下方代码示例 1 | 动态生成 Spring Boot 配置 | 见下方代码示例 1 | 使用 [[...]] 避免 YAML 缩进被破坏。 |
logback-spring.xml 模板 | <root level="[[${logLevel}]]"> | 生成日志配置 | 见下方代码示例 2 | XML 模板需设置 TemplateMode.TEXT,否则 < 会被转义为 <。 |
Dockerfile 模板 | FROM openjdk:17 / COPY [[${jarName}]] app.jar | 生成容器构建文件 | ctx.setVariable("jarName", "myapp.jar"); | 注意路径和权限设置,避免运行时错误。 |
代码示例 1:application.yml 模板
server:
port: [[${serverPort}]]
spring:
datasource:
url: [[${dbUrl}]]
代码示例 2:application.yml 渲染
Context ctx = new Context();
ctx.setVariable("serverPort", 8080);
ctx.setVariable("dbUrl", "jdbc:mysql://...");
11.6 生成 API 文档
| 方法/组件 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| OpenAPI/Swagger 模板 | summary: "[[${summary}]]" | 生成符合 OpenAPI 规范的 YAML 文档 | 见下方代码示例 1 | 需严格遵循 OpenAPI 结构,建议结合注解解析自动生成上下文。 |
| Markdown API 文档模板 | ## [[${endpoint}]] / **Method**: [[${method}]] | 生成人类可读的接口说明 | 同上 | 适合内部 Wiki 或快速文档交付。 |
| 集成 SpringDoc/OpenAPI 注解 | 不直接使用 Thymeleaf,但可导出 JSON/YAML 后用 Thymeleaf 二次渲染 | 自动提取 Controller 注解生成文档 | 先通过 /v3/api-docs 获取 JSON,再作为上下文传入模板 | 更推荐使用 Swagger UI,Thymeleaf 仅用于定制化文档导出。 |
代码示例 1:OpenAPI/Swagger YAML 模板
paths:
/users:
get:
summary: "[[${summary}]]"
代码示例 2:OpenAPI 渲染
Context ctx = new Context();
ctx.setVariable("summary", "获取用户列表");
String openapiYaml = templateEngine.process("openapi.yaml.thymeleaf", ctx);