第 4 章 · Thymeleaf 模板与表单
本章目标:集成 Thymeleaf 服务端模板;实现商品列表/详情 HTML 页面(对标 shop-demo 的 catalog/ 模板);处理 表单提交、校验错误展示与重定向;理解 CSRF 机制及 Spring Security 启用后的 Token 行为;能对照 Django Template + {% csrf_token %} 说明异同;在 shop-spring-demo 中完成运营后台最小页面骨架。
学时建议:5~6 小时(含 2 小时跟练)
前置:完成 spring-boot-web ch03 与 java-dev;frontend-web 或 HTML 基础;django-web ch02 表单章节更佳。
4.1 场景说明:HTML 与 JSON 并存
shop-spring-demo 需同时服务:
| 通道 | 技术 | 用户 |
|---|---|---|
| JSON API | @RestController(ch03) | 移动端、前后端分离 SPA |
| HTML 页面 | @Controller + Thymeleaf | 运营人员浏览器访问 |
shop-demo(Django) 用 templates/catalog/product_list.html 渲染列表;本章在 Spring 侧实现等价页面,域名仍经虚构 https://api.example.com 或独立 https://admin.example.com 访问(教学占位)。
shop-spring-demo 双通道
/api/v1/* → JSON(RestController)
/catalog/* → HTML(Controller + Thymeleaf)
4.2 引入 Thymeleaf
4.2.1 依赖
pom.xml 增加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Initializr 可勾选 Thymeleaf 一并生成。
4.2.2 与 Django 模板对照
| 特性 | Django Templates | Thymeleaf |
|---|---|---|
| 语法 | {{ var }} {% tag %} | th:text="${var}" |
| 布局 | extends / block | th:replace / layout dialect |
| 静态文件 | {% static %} | th:src="@{/css/app.css}" |
| 自然模板 | 否 | 可在浏览器直接打开 html 预览 |
| 默认引擎 | Django | Spring Boot 自动配置 Thymeleaf |
4.3 配置与目录
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
cache: false # 开发关闭缓存
web:
resources:
static-locations: classpath:/static/
resources/
├── templates/
│ ├── layout/
│ │ └── main.html
│ └── catalog/
│ ├── product_list.html
│ └── product_form.html
└── static/
└── css/
└── shop.css
4.4 布局模板 layout/main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title th:text="${pageTitle} ?: 'Shop Spring Demo'">Shop Spring Demo</title>
<link rel="stylesheet" th:href="@{/css/shop.css}"/>
</head>
<body>
<header>
<h1><a th:href="@{/catalog}">商品目录</a></h1>
</header>
<main th:replace="${content}">内容区</main>
<footer><small>教学项目 shop-spring-demo · api.example.com</small></footer>
</body>
</html>
也可使用 Thymeleaf Layout Dialect;本章用片段简化。
4.5 列表页 Controller
web/CatalogPageController.java:
package com.example.shop.web;
import com.example.shop.catalog.ProductCatalogService;
import com.example.shop.catalog.ProductDto;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("/catalog")
public class CatalogPageController {
private final ProductCatalogService catalogService;
public CatalogPageController(ProductCatalogService catalogService) {
this.catalogService = catalogService;
}
@GetMapping
public String list(Model model) {
List<ProductDto> products = catalogService.listFeatured(50);
model.addAttribute("products", products);
model.addAttribute("pageTitle", "商品列表");
return "catalog/product_list";
}
@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
ProductDto product = catalogService.listFeatured(100).stream()
.filter(p -> p.id().equals(id))
.findFirst()
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
model.addAttribute("product", product);
model.addAttribute("pageTitle", product.name());
return "catalog/product_detail";
}
}
| 返回值 | 含义 |
|---|---|
"catalog/product_list" | 视图名 → templates/catalog/product_list.html |
Model | 等同 Django context 字典 |
Django:
return render(request, "catalog/product_list.html", {"products": products})
4.6 列表模板 catalog/product_list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title th:text="${pageTitle}">商品列表</title>
<link rel="stylesheet" th:href="@{/css/shop.css}"/>
</head>
<body>
<h1>商品列表</h1>
<p><a th:href="@{/catalog/new}">新建商品</a>(ch07 完善)</p>
<table>
<thead>
<tr><th>SKU</th><th>名称</th><th>价格</th><th>操作</th></tr>
</thead>
<tbody>
<tr th:each="p : ${products}">
<td th:text="${p.sku}">SKU</td>
<td th:text="${p.name}">名称</td>
<td th:text="${#numbers.formatDecimal(p.price, 1, 2)}">0.00</td>
<td><a th:href="@{/catalog/{id}(id=${p.id})}">详情</a></td>
</tr>
<tr th:if="${#lists.isEmpty(products)}">
<td colspan="4">暂无商品</td>
</tr>
</tbody>
</table>
</body>
</html>
4.6.1 常用 Thymeleaf 属性
| 属性 | 作用 |
|---|---|
th:text | 转义文本输出 |
th:each | 循环 |
th:if / th:unless | 条件 |
th:href="@{/path}" | URL 生成(含 context-path) |
th:object / th:field | 表单绑定(见下节) |
4.7 表单与命令对象
4.7.1 ProductForm(命令对象)
package com.example.shop.web.form;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;