Thymeleaf
Thymeleaf 是 Spring Boot 默认推荐的服务端模板引擎,模板文件是合法的 HTML,可在浏览器直接打开预览(自然模板)。与 MVC 配合,渲染后的 HTML 由服务端返回给客户端。
依赖与配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>spring:
thymeleaf:
prefix: classpath:/templates/ # 模板根目录
suffix: .html
encoding: UTF-8
mode: HTML # HTML / XML / TEXT / JAVASCRIPT
cache: true # 生产环境开启;开发时设 false 实时刷新
check-template-location: true模板文件放在 src/main/resources/templates/,静态资源(CSS/JS/图片)放在 src/main/resources/static/,详见 静态资源。
Controller 返回视图
@Controller // 注意:不是 @RestController
@RequestMapping("/users")
public class UserViewController {
private final UserService userService;
@GetMapping("/{id}")
public String userDetail(@PathVariable Long id, Model model) {
model.addAttribute("user", userService.findById(id));
model.addAttribute("title", "用户详情");
return "user/detail"; // 对应 templates/user/detail.html
}
@GetMapping
public String userList(
@RequestParam(defaultValue = "0") int page,
Model model) {
model.addAttribute("users", userService.findAll(page));
return "user/list";
}
}核心语法
文本与表达式
<!-- 变量表达式 -->
<span th:text="${user.name}">占位文本</span>
<!-- 转义 HTML(默认)vs 不转义(th:utext,慎用) -->
<p th:text="${description}">描述</p>
<p th:utext="${richContent}">富文本</p>
<!-- 国际化消息 -->
<h1 th:text="#{page.title}">标题</h1>
<!-- URL 表达式 -->
<a th:href="@{/users/{id}(id=${user.id})}">查看详情</a>
<img th:src="@{/images/logo.png}" alt="logo">
<!-- 片段引用 -->
<div th:insert="~{fragments/header :: nav}"></div>条件判断
<!-- if / unless -->
<div th:if="${user.admin}">管理员菜单</div>
<div th:unless="${user.admin}">普通用户菜单</div>
<!-- switch -->
<span th:switch="${order.status}">
<span th:case="'PENDING'">待付款</span>
<span th:case="'PAID'">已付款</span>
<span th:case="'SHIPPED'">已发货</span>
<span th:case="*">未知状态</span>
</span>循环遍历
<table>
<thead>
<tr><th>序号</th><th>姓名</th><th>邮箱</th><th>状态</th></tr>
</thead>
<tbody>
<!-- iterStat 是循环状态变量(可选) -->
<tr th:each="user, iterStat : ${users}"
th:class="${iterStat.odd} ? 'odd' : 'even'">
<td th:text="${iterStat.count}">1</td>
<td th:text="${user.name}">张三</td>
<td th:text="${user.email}">test@example.com</td>
<td>
<span th:text="${user.active} ? '启用' : '禁用'"
th:class="${user.active} ? 'badge-success' : 'badge-danger'">
启用
</span>
</td>
</tr>
</tbody>
</table>循环状态变量属性:index(0 起)、count(1 起)、size、first、last、odd、even。
属性操作
<!-- 动态属性 -->
<input th:value="${user.name}" th:placeholder="#{form.name.placeholder}">
<!-- CSS class 合并 -->
<div class="card" th:classappend="${user.vip} ? 'card-gold' : ''"></div>
<!-- 内联 style -->
<span th:style="'color:' + ${user.themeColor}">文字</span>
<!-- 布尔属性(checked / disabled / selected) -->
<input type="checkbox" th:checked="${user.active}">
<button th:disabled="${!canEdit}">编辑</button>
<!-- 自定义属性 -->
<div th:attr="data-id=${user.id}, data-role=${user.role}"></div>内联表达式
在文本节点中嵌入表达式,避免 th:text 导致标签变形:
<!-- 文本内联 -->
<p>欢迎,[[${user.name}]]!您有 [[${msgCount}]] 条新消息。</p>
<!-- JavaScript 内联(自动 JSON 序列化) -->
<script th:inline="javascript">
const currentUser = /*[[${user}]]*/ null;
const config = /*[[${appConfig}]]*/ {};
</script>模板片段与布局
定义片段(fragments/common.html)
<!-- templates/fragments/common.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!-- 导航栏片段 -->
<nav th:fragment="navbar">
<a th:href="@{/}">首页</a>
<a th:href="@{/users}">用户</a>
<span th:text="${#authentication?.name}">用户名</span>
</nav>
<!-- 页脚片段 -->
<footer th:fragment="footer">
<p th:text="'© ' + ${#dates.year(#dates.createNow())} + ' My App'">© 2024</p>
</footer>
</body>
</html>引用片段
<!-- th:insert:插入片段(保留宿主标签) -->
<div th:insert="~{fragments/common :: navbar}"></div>
<!-- th:replace:替换宿主标签为片段 -->
<div th:replace="~{fragments/common :: footer}"></div>
<!-- 传参数给片段 -->
<div th:replace="~{fragments/common :: card(title='用户列表', count=${users.size()})}"></div>
<!-- 片段定义(带参数) -->
<div th:fragment="card(title, count)">
<h3 th:text="${title}">标题</h3>
<span th:text="${count}">0</span>
</div>布局模板(Thymeleaf Layout Dialect)
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency><!-- templates/layout/base.html -->
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<title layout:title-pattern="$CONTENT_TITLE - My App">My App</title>
<link rel="stylesheet" th:href="@{/css/app.css}">
</head>
<body>
<div th:replace="~{fragments/common :: navbar}"></div>
<main layout:fragment="content"><!-- 子页面内容 --></main>
<div th:replace="~{fragments/common :: footer}"></div>
</body>
</html>
<!-- templates/user/list.html(子页面) -->
<html layout:decorate="~{layout/base}">
<head><title>用户列表</title></head>
<body>
<div layout:fragment="content">
<!-- 这里写页面特有内容 -->
<h1>用户列表</h1>
</div>
</body>
</html>表单处理
// Controller:绑定表单对象
@GetMapping("/create")
public String showCreateForm(Model model) {
model.addAttribute("userForm", new UserForm());
return "user/create";
}
@PostMapping("/create")
public String submitCreateForm(
@Valid @ModelAttribute("userForm") UserForm form,
BindingResult result, RedirectAttributes ra) {
if (result.hasErrors()) {
return "user/create"; // 有错误返回表单页
}
userService.create(form);
ra.addFlashAttribute("success", "用户创建成功");
return "redirect:/users";
}<!-- templates/user/create.html -->
<form th:action="@{/users/create}" th:object="${userForm}" method="post">
<div>
<label for="name">姓名</label>
<input id="name" type="text" th:field="*{name}"
th:class="${#fields.hasErrors('name')} ? 'input-error' : ''">
<!-- 显示字段错误 -->
<span th:if="${#fields.hasErrors('name')}"
th:errors="*{name}" class="error-msg"></span>
</div>
<div>
<label for="email">邮箱</label>
<input id="email" type="email" th:field="*{email}">
<span th:errors="*{email}" class="error-msg"></span>
</div>
<div>
<label for="role">角色</label>
<select id="role" th:field="*{role}">
<option th:each="r : ${T(com.example.Role).values()}"
th:value="${r}" th:text="${r.label}">
</option>
</select>
</div>
<button type="submit">提交</button>
</form>th:field="*{name}" 等价于同时设置 id、name 和 value(绑定到 th:object 指定的对象)。
Spring Security 集成
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency><html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- 根据认证状态显示 -->
<div sec:authorize="isAuthenticated()">
欢迎,<span sec:authentication="name">用户名</span>
</div>
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/login}">登录</a>
</div>
<!-- 根据角色/权限显示 -->
<a sec:authorize="hasRole('ADMIN')" th:href="@{/admin}">管理后台</a>
<button sec:authorize="hasAuthority('order:delete')">删除订单</button>内置工具对象
Thymeleaf 在 # 命名空间下提供工具类:
<!-- 日期格式化 -->
<span th:text="${#temporals.format(order.createdAt, 'yyyy-MM-dd HH:mm')}"></span>
<!-- 字符串工具 -->
<p th:if="${#strings.isEmpty(user.bio)}">暂无简介</p>
<span th:text="${#strings.abbreviate(content, 100)}"></span>
<!-- 集合工具 -->
<p th:text="'共 ' + ${#lists.size(users)} + ' 条'"></p>
<!-- 数字格式化 -->
<span th:text="${#numbers.formatDecimal(price, 1, 2)}">0.00</span>
<!-- 对象判空 -->
<div th:if="${#objects.isEmpty(user)}">用户不存在</div>国际化
<!-- 使用消息键 -->
<h1 th:text="#{user.list.title}">用户列表</h1>
<p th:text="#{user.count(${users.size()})}">共 0 个用户</p># messages_zh_CN.properties
user.list.title=用户列表
user.count=共 {0} 个用户国际化配置详见 国际化。
开发调试
# 开发时关闭缓存,修改模板立即生效
spring:
thymeleaf:
cache: false
devtools:
restart:
enabled: true配合 热部署与DevTools 可以实现模板热更新而无需重启应用。