Skip to content

第4章 Spring Boot 整合Thymeleaf模板引擎 ​

技术栈:Spring Boot 4.1.1 + JDK 17 + Thymeleaf 3.1.x 定位:服务端页面渲染入门,本章重点:从模板引擎基础到常用语法、表单绑定、模板布局、案例实战,配套完整动手练习


学习目标 ​

  1. ✅ 理解模板引擎作用,掌握Spring Boot整合Thymeleaf的项目搭建
  2. ✅ 熟练掌握Thymeleaf核心属性:th:text、th:each、th:if、th:href等常用语法
  3. ✅ 掌握变量、选择、链接、消息、片段五大标准表达式
  4. ✅ 掌握表单数据绑定、自动回显、下拉/单选/复选框处理
  5. ✅ 会使用th:fragment做页面公共片段抽取与复用
  6. ✅ 了解国际化配置、模板布局企业最佳实践
  7. ✅ 能独立完成图书管理系统模板页面开发,排查常见入门问题

4.1 先搞明白:什么是模板引擎 ​

4.1.1 为什么需要模板引擎 ​

前面我们写的接口都返回JSON数据,如果需要服务端渲染HTML页面返回浏览器,总不能用字符串拼接HTML:

java
// 笨办法:字符串拼接
@GetMapping("/hello")
public String hello() {
    String name = "张三";
    return "<html><body><h1>你好," + name + "!</h1></body></html>";
}

字符串拼接的痛点:

  • 页面复杂时代码极长,维护困难
  • 没有语法高亮,写错标签很难排查
  • 前端无法协作设计
  • 容易出现XSS安全漏洞

模板引擎就是解决这个问题的:

提前写好HTML模板骨架,预留动态数据占位符;程序把数据传给模板引擎,引擎把数据填充到对应位置,最终生成完整HTML页面返回浏览器。 公式:模板 + 数据 → 模板引擎 → 完整HTML页面

如果你学过JSP,那Thymeleaf和JSP是同类工具,只是语法更优雅、功能更强、和Spring无缝集成。

4.1.2 Spring Boot支持的模板引擎 ​

Spring Boot对主流模板引擎都提供了starter启动器:

模板引擎特点企业使用情况
Thymeleaf自然模板、语法优雅、Spring官方推荐目前主流,新项目首选
FreeMarker老牌模板引擎,功能强大逐步减少,老项目维护
Velocity更老的模板引擎基本不用
JSP传统Java Web技术Spring Boot不推荐,打包部署麻烦

💡 Spring Boot 4.x 内置Thymeleaf 3.1.x版本,引入starter自动配置,开箱即用。

4.1.3 为什么选Thymeleaf ​

优点一:自然模板(最大特色) ​

Thymeleaf模板文件本身就是合法的HTML文件,直接用浏览器打开就能看到静态原型效果;运行在服务器上,又能渲染动态数据。

  • JSP直接用浏览器打开是一堆乱码标签

  • Thymeleaf直接打开是正常页面,只是数据是示例原型

    意义:前端设计师可以直接出页面,后端只需要加语法,前后端协作更顺畅。

优点二:语法优雅 ​

所有动态语法都以th:开头的HTML属性形式存在,页面结构干净,不破坏HTML本身的结构。

优点三:和Spring无缝集成 ​

Spring Boot官方推荐,自动配置,支持Spring MVC表单绑定、校验、国际化、安全集成。

优点四:功能丰富 ​

条件判断、循环、模板布局、表单处理、国际化、Spring Security权限标签等。


4.2 快速入门:第一个Thymeleaf页面 ​

4.2.1 引入依赖 ​

创建Spring Boot项目,添加依赖,或者在已有项目pom.xml加入:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

🎯 引入starter后Spring Boot自动完成配置:

  • 默认模板位置:src/main/resources/templates/
  • 默认文件后缀:.html
  • 默认编码:UTF-8
  • 生产环境默认开启缓存

4.2.2 编写Controller ​

⚠️ **新手第一坑:返回页面用@Controller,不是@RestController!

  • @RestController = @Controller + @ResponseBody,返回JSON字符串
  • @Controller:返回视图页面

创建controller包,新建HelloController.java:

java
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;

@Controller  // 注意:这里是 @Controller,不是 @RestController!
public class HelloController {

    @GetMapping("/hello")
    public String hello(Model model) {
        // 往模型里放数据,模板里可以取出来
        model.addAttribute("name", "张三");
        model.addAttribute("message", "欢迎学习 Thymeleaf!");

        // 返回视图名称(模板文件名,不用加 .html)
        return "hello";
    }
}

4.2.3 编写模板页面 ​

在src/main/resources/templates/目录下,新建hello.html:

html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>第一个Thymeleaf页面</title>
</head>
<body>
<h1>第一个Thymeleaf页面</h1>
<p>姓名:<span th:text="${name}">默认值</span></p>
<p>消息:<span th:text="${message}">默认消息</span></p>
</body>
</html>

代码讲解:

  1. xmlns:th="http://www.thymeleaf.org":声明Thymeleaf命名空间,编辑器识别th:属性,建议加上
  2. th:text="${name}":设置标签的文本内容,替换标签内所有内容
  3. 标签中间的"李四"、"默认消息":原型默认值,直接浏览器打开时显示;服务器运行时替换成真实数据

✨ 这就是「自然模板」特性:

  • 直接打开hello.html:看到李四、默认消息(原型效果)
  • 通过服务器访问/hello:看到张三、欢迎学习Thymeleaf!(真实数据)

4.2.4 启动测试 ​

启动项目,浏览器访问:http://localhost:8080/hello 看到动态渲染的内容,第一个Thymeleaf页面就跑通了。

4.2.5 开发环境配置(必改) ​

默认Thymeleaf开启缓存,改页面不重启不会生效,开发体验很差。 application.yml添加配置:

spring:
  thymeleaf:
    cache: false          # 开发环境关闭缓存,改完刷新浏览器就生效
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    mode: HTML

💡 最佳实践:

  • 开发环境:cache: false,热更新方便
  • 生产环境:cache: true,性能大幅提升
  • 配合Profile多环境配置,不同环境自动切换

4.3 Thymeleaf常用核心属性(重点⭐) ​

属性作用常用程度
th:text设置标签文本(自动转义,防XSS)⭐⭐⭐⭐⭐
th:utext设置标签文本(不转义)⭐⭐
th:value设置表单元素value值⭐⭐⭐⭐⭐
th:each循环遍历集合⭐⭐⭐⭐⭐
th:if条件成立才显示元素⭐⭐⭐⭐⭐
th:unless条件不成立才显示元素⭐⭐⭐⭐
th:switch/th:case多分支条件判断⭐⭐
th:href设置a标签链接地址⭐⭐⭐⭐⭐
th:src设置img标签图片地址⭐⭐⭐⭐
th:action设置form表单提交地址⭐⭐⭐⭐
th:class设置class属性⭐⭐⭐⭐
th:style设置style属性⭐⭐⭐
th:attr通用自定义属性⭐⭐

4.3.1 th:text / th:utext ​

<!-- th:text:自动转义HTML特殊字符,安全防XSS -->
<p th:text="${content}">默认文本</p>

<!-- th:utext:不转义,HTML标签直接生效 -->
<p th:utext="${content}">默认文本</p>

⚠️ 安全提醒:

  • 用户输入的内容、数据库取出的不确定内容,必须用th:text,防止XSS攻击
  • 只有确定100%安全的内容,才可以用th:utext

4.3.2 th:value ​

给input等表单元素设置value值:

<input type="text" name="username" th:value="${username}" value="默认值">

4.3.3 th:each 循环(最常用) ​

遍历集合生成多个元素,列表、表格必用。

基础用法 ​

Controller放数据:

@GetMapping("/users")
public String users(Model model) {
    List<User> users = Arrays.asList(
        new User(1, "张三", 25),
        new User(2, "李四", 28),
        new User(3, "王五", 22)
    );
    model.addAttribute("users", users);
    return "users";
}

模板:

<table border="1">
    <thead>
        <tr><th>ID</th><th>姓名</th><th>年龄</th></tr>
    </thead>
    <tbody>
        <tr th:each="user : ${users}">
            <td th:text="${user.id}">1</td>
            <td th:text="${user.name}">张三</td>
            <td th:text="${user.age}">25</td>
        </tr>
    </tbody>
</table>

循环状态变量 ​

th:each可以带状态变量,获取索引、计数、奇偶行等信息:

<tr th:each="user, status : ${users}">
    <td th:text="${status.index}">0</td>
    <!-- 索引从0开始 -->
    <td th:text="${status.count}">1</td>
    <!-- 计数从1开始 -->
    <td th:text="${status.size}">3</td>
    <!-- 集合总大小 -->
    <td th:text="${status.first}">true</td>
    <!-- 是否第一个 -->
    <td th:text="${status.last}">false</td>
    <!-- 是否最后一个 -->
    <td th:text="${status.even}">false</td>
    <!-- 是否偶数行 -->
    <td th:text="${status.odd}">true</td>
    <!-- 是否奇数行 -->
    <td th:text="${user.name}">张三</td>
</tr>

隔行变色示例 ​

<tr th:each="user, status : ${users}" 
    th:class="${status.even} ? 'even-row' : 'odd-row'">
    <td th:text="${user.name}"></td>
</tr>

配合CSS:偶数行even-row、奇数行odd-row不同背景色。

4.3.4 th:if / th:unless ​

  • th:if:条件成立,元素才显示
  • th:unless:条件不成立,元素才显示(和if相反)
<!-- 成年才显示 -->
<p th:if="${age >= 18}">你已经成年了</p>

<!-- 未成年才显示 -->
<p th:unless="${age >= 18}">你还未成年</p>

<!-- 判断对象不为空 -->
<div th:if="${user != null}" th:text="${user.name}"></div>

支持的运算符 ​

运算符说明示例
gt / >大于${age gt 18}
lt / <小于${age lt 18}
ge / >=大于等于${age ge 18}
le / <=小于等于${age le 18}
eq / ==等于${age eq 18}
ne / !=不等于${age ne 18}
and并且${age>18 and age<60}
or或者${age<18 or age>60}
! / not非${!isVip}

💡 提示:HTML属性里写< >可能有语法问题,推荐用gt、lt文本形式。

4.3.5 th:switch / th:case ​

多分支判断,类似Java switch-case:

<div th:switch="${role}">
    <p th:case="'admin'">管理员</p>
    <p th:case="'user'">普通用户</p>
    <p th:case="'vip'">VIP会员</p>
    <p th:case="*">未知角色</p>
    <!-- * 代表默认分支 -->
</div>

4.3.6 th:href + 链接表达式 ​

配合@{...}链接表达式,自动处理上下文路径:

<!-- 基础链接 -->
<a th:href="@{/user/list}">用户列表</a>

<!-- 带参数 -->
<a th:href="@{/user/detail(id=1)}">详情</a>

<!-- RESTful路径参数 -->
<a th:href="@{/user/{id}/detail(id=${userId})}">详情</a>

好处:项目有上下文路径时,自动帮你加上,不用手动拼接。

4.3.7 其他常用属性 ​

  • th:src:设置图片src属性
  • th:action:设置form表单action地址
  • th:class:动态设置class属性
  • th:style:动态设置style样式
  • th:attr:设置任意自定义属性

4.4 标准表达式 ​

Thymeleaf有5种标准表达式,各有用途:

表达式语法作用常用程度
变量表达式${...}取变量值、运算、调用方法⭐⭐⭐⭐⭐
选择变量表达式*{...}取选定对象的属性⭐⭐⭐⭐
链接表达式@{...}生成链接地址⭐⭐⭐⭐⭐
消息表达式#{...}读取国际化消息⭐⭐
片段表达式~{...}引用模板片段⭐⭐⭐⭐

4.4.1 变量表达式 ${...} ​

最常用,读取变量、做运算、调用方法:

<!-- 取对象属性 -->
<p th:text="${user.name}"></p>

<!-- 算术运算 -->
<p th:text="${price * quantity}"></p>

<!-- 三元运算 -->
<p th:text="${age >= 18} ? '成年' : '未成年'"></p>

<!-- 调用方法 -->
<p th:text="${user.getName()}"></p>
<p th:text="${users.size()}"></p>

<!-- 字符串拼接简写 |...| -->
<p th:text="|你好,${name},欢迎回来!|"></p>

4.4.2 选择变量表达式 *{...} ​

配合th:object选定一个对象后,里面直接取对象属性,不用每次写前缀。

<!-- 普通写法 -->
<div>
    <p th:text="${user.name}"></p>
    <p th:text="${user.age}"></p>
</div>

<!-- 选择对象写法 -->
<div th:object="${user}">
    <p th:text="*{name}"></p>
    <p th:text="*{age}"></p>
</div>

表单绑定场景用得最多,后面表单部分详细讲。

4.4.3 链接表达式 @{...} ​

生成链接,自动处理上下文路径,支持绝对路径、相对路径、带参数。

4.4.4 消息表达式 #{...} ​

读取国际化配置文件里的消息,国际化部分详细讲。

4.4.5 片段表达式 ~{...} ​

引用其他模板里的片段,做页面布局复用,模板布局部分详细讲。


4.5 内置对象与工具类 ​

4.5.1 基础内置对象 ​

不用往Model里放,模板里直接用:

对象说明
#ctx上下文对象
#vars上下文变量
#requestHttpServletRequest对象
#responseHttpServletResponse对象
#sessionHttpSession对象
#servletContextServletContext对象

示例:

<!-- 取session里的登录用户名 -->
<p>当前用户:<span th:text="${session.userName}">游客</span></p>

<!-- 取请求参数 -->
<p th:text="${#request.getParameter('keyword')}"></p>

4.5.2 常用工具类 ​

都是#开头,提供常用工具方法,不用自己写工具。

#strings 字符串工具 ​

<!-- 判断为空 -->
<p th:if="${#strings.isEmpty(name)}">姓名为空</p>

<!-- 字符串长度 -->
<p th:text="${#strings.length(name)}"></p>

<!-- 转大小写 -->
<p th:text="${#strings.toUpperCase(name)}"></p>

<!-- 截取、替换、包含、开头结尾判断 -->
<p th:text="${#strings.substring(name,0,5)}"></p>
<p th:if="${#strings.contains(name,'张')}">包含张</p>

#dates 日期工具 ​

<!-- 格式化日期 -->
<p th:text="${#dates.format(birthday, 'yyyy-MM-dd HH:mm:ss')}"></p>

<!-- 获取年月日 -->
<p th:text="${#dates.year(birthday)}"></p>

<!-- 当前日期 -->
<p th:text="${#dates.createNow()}"></p>

#numbers 数字工具 ​

<!-- 保留2位小数 -->
<p th:text="${#numbers.formatDecimal(price, 1, 2)}"></p>

<!-- 百分比 -->
<p th:text="${#numbers.formatPercent(rate, 1, 2)}"></p>

#lists 集合工具 ​

<!-- 判空 -->
<p th:if="${#lists.isEmpty(users)}">列表为空</p>

<!-- 集合大小 -->
<p>共 <span th:text="${#lists.size(users)}">0</span> 条数据</p>

<!-- 是否包含 -->
<p th:if="${#lists.contains(users, user)}">包含</p>

💡 不用全背,常用的记住,用到查官方文档即可。 官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utility-objects


4.6 表单处理(企业常用⭐⭐⭐) ​

Thymeleaf和Spring MVC表单绑定配合非常好,自动回显、自动绑定。

4.6.1 表单回显 ​

编辑页面场景:打开页面,表单里自动显示原有数据。

Controller:

@GetMapping("/user/edit/{id}")
public String editUser(@PathVariable Integer id, Model model) {
    // 模拟从数据库查询
    User user = new User(id, "张三", 25, "zhangsan@example.com");
    model.addAttribute("user", user);
    return "user_edit";
}

模板:

<form th:object="${user}" th:action="@{/user/save}" method="post">
    <div>
        <label>ID:</label>
        <input type="text" th:field="*{id}" readonly>
    </div>
    <div>
        <label>姓名:</label>
        <input type="text" th:field="*{name}">
    </div>
    <div>
        <label>年龄:</label>
        <input type="number" th:field="*{age}">
    </div>
    <button type="submit">保存</button>
</form>

🎯 th:object + th:field 组合的作用:

  1. 自动设置name、id属性
  2. 自动设置value值(表单回显)
  3. 提交后Spring MVC自动封装成对象

4.6.2 表单提交 ​

@PostMapping("/user/save")
public String saveUser(User user) {
    // 保存用户业务逻辑
    System.out.println("保存用户:" + user);
    // 重定向到列表页,防止刷新重复提交
    return "redirect:/user/list";
}

💡 forward vs redirect:

  • forward(转发):地址栏不变,刷新会重复提交
  • redirect(重定向):地址栏变,刷新不会重复提交
  • 表单提交后必须用重定向

4.6.3 下拉框、单选框、复选框 ​

下拉框 ​

<!-- 固定下拉 -->
<select th:field="*{gender}">
    <option value="">请选择</option>
    <option value="1">男</option>
    <option value="2">女</option>
</select>

<!-- 从集合生成下拉 -->
<select th:field="*{deptId}">
    <option value="">请选择部门</option>
    <option th:each="dept : ${depts}"
            th:value="${dept.id}"
            th:text="${dept.name}"></option>
</select>

单选框 ​

<input type="radio" th:field="*{gender}" value="1"> 男
<input type="radio" th:field="*{gender}" value="2"> 女

复选框 ​

<!-- 单个布尔值 -->
<input type="checkbox" th:field="*{isVip}"> VIP

<!-- 多个值集合 -->
<input type="checkbox" th:field="*{hobbies}" value="reading"> 阅读
<input type="checkbox" th:field="*{hobbies}" value="music"> 音乐
<input type="checkbox" th:field="*{hobbies}" value="sports"> 运动

选中状态自动回显,不用自己判断。


4.7 模板布局:页面复用 ​

实际项目中,头部、底部、侧边栏都是公共的,每个页面都写一遍,维护麻烦。Thymeleaf支持抽取公共片段,其他页面引用。

4.7.1 定义片段 th:fragment ​

新建templates/fragments/header.html:

<div th:fragment="header">
    <h1>我的网站</h1>
    <nav>
        <a th:href="@{/}">首页</a>
        <a th:href="@{/book/list}">图书管理</a>
    </nav>
    <hr>
</div>

新建templates/fragments/footer.html:

<div th:fragment="footer">
    <hr>
    <p>© 2026 我的网站 版权所有</p>
</div>

4.7.2 三种引用方式 ​

方式效果常用程度
th:insert片段插入到当前标签内部⭐⭐⭐
th:replace片段替换整个当前标签⭐⭐⭐⭐⭐
th:include只包含片段内容,不要根标签⭐⭐

示例:

<body>
    <!-- 替换当前div为header片段 -->
    <div th:replace="~{fragments/header :: header}"></div>

    <div class="content">
        页面主体内容
    </div>

    <div th:replace="~{fragments/footer :: footer}"></div>
</body>

4.7.3 带参数的片段 ​

片段可以像函数一样传参数:

<!-- 定义带参片段 -->
<div th:fragment="greeting(name)">
    <p>你好,<span th:text="${name}">朋友</span>!</p>
</div>

<!-- 引用传参 -->
<div th:replace="~{fragments/common :: greeting('张三')}"></div>

4.7.4 企业最佳实践 ​

  • 公共头部、底部、侧边栏统一抽取到fragments目录
  • 业务页面统一引用,修改公共部分只改一个文件
  • 不要嵌套太深,维护困难

4.8 国际化(了解) ​

让网站支持多语言切换。

4.8.1 配置消息文件 ​

src/main/resources/i18n/目录下创建:

  • messages.properties(默认)
  • messages_zh_CN.properties(中文)
  • messages_en_US.properties(英文)

application.yml配置:

spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8

4.8.2 模板中使用 ​

<h1 th:text="#{welcome.title}">欢迎</h1>
<p th:text="#{welcome.message}">欢迎来到我的网站</p>

4.8.3 切换语言 ​

配置LocaleResolver和拦截器,通过lang参数切换语言,了解即可。


4.9 案例实战:图书管理系统 ​

通过完整案例把知识点串起来,跟着做一遍,Thymeleaf基本就入门了。

4.9.1 需求分析 ​

做一个简单图书管理系统:

  1. 图书列表展示,显示所有图书
  2. 条件搜索:按书名或作者模糊搜索
  3. 借阅功能:点击借阅按钮,借阅图书
  4. 权限控制:管理员看到借阅按钮,普通用户看不到
  5. 顶部显示当前登录用户信息

4.9.2 项目搭建 ​

创建Spring Boot项目,依赖:Spring Web、Thymeleaf、Lombok。

4.9.3 实体类 ​

Book.java:

java
@Data
@AllArgsConstructor
public class Book {
    private Integer id;
    private String name;
    private String author;
    private String press;
    private String status; // 0-可借阅 1-已借出 2-遗失
}

User.java:

java
@Data
@AllArgsConstructor
public class User implements Serializable {
    private String name;
    private String role; // ADMIN-管理员 USER-普通用户
}

4.9.4 模拟数据 ​

Data.java模拟数据库:

java
public class Data {
    public static List<Book> getBooks() {
        List<Book> books = new ArrayList<>();
        books.add(new Book(1, "楚辞", "屈原", "中国文联出版社", "0"));
        books.add(new Book(2, "纳兰词", "纳兰性德", "中国文联出版社", "1"));
        books.add(new Book(3, "西游记", "吴承恩", "中国文联出版社", "2"));
        return books;
    }
}

4.9.5 Controller编写 ​

BookController.java:

java
package com.example.demo.controller;

// 正确导入Spring MVC的Model!!
import org.springframework.ui.Model;
import com.example.demo.entity.Book;
import com.example.demo.entity.User;
import com.example.demo.model.Data;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("/book")
public class BookController {

    // 模拟数据库图书数据
    List<Book> books = Data.getBooks();

    /**
     * 查询所有图书
     */
    @RequestMapping("/list")
    public String list(Model model, HttpSession session) {
        // 模拟用户登录(实际项目从登录接口获取)
        session.setAttribute("user", new User("zhangSan", "ADMIN"));
        // 把图书列表放入模型,模板中可以读取
        model.addAttribute("books", books);
        return "books";
    }

    /**
     * 条件搜索:按书名/作者模糊搜索
     */
    @RequestMapping("/search")
    public String search(Book book, Model model, HttpSession session) {
        List<Book> result = new ArrayList<>();
        // 规范变量名:见名知意
        String bookName = book.getName();
        String bookAuthor = book.getAuthor();

        // 两个条件都为空,返回全部
        if ((bookName == null || bookName.isEmpty())
                && (bookAuthor == null || bookAuthor.isEmpty())) {
            result = books;
        } else {
            // 按书名或作者模糊匹配
            for (Book b : books) {
                boolean nameMatch = bookName != null && !bookName.isEmpty()
                        && b.getName().contains(bookName);
                boolean authorMatch = bookAuthor != null && !bookAuthor.isEmpty()
                        && b.getAuthor().contains(bookAuthor);
                if (nameMatch || authorMatch) {
                    result.add(b);
                }
            }
        }

        session.setAttribute("user", new User("zhangSan", "ADMIN"));
        model.addAttribute("books", result);
        return "books";
    }

    /**
     * 借阅图书
     */
    @RequestMapping("/borrow/{id}")
    public String borrow(@PathVariable("id") Integer id) {
        // 实际项目中这里会修改数据库状态
        System.out.println("借阅图书ID:" + id);
        // 重定向到列表页,防止刷新重复提交
        return "redirect:/book/list";
    }
}

4.9.6 列表页面实现 ​

templates/books.html:

html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>图书管理系统</title>
    <style>
        body { font-family: "微软雅黑"; margin: 20px; }
        .header { display: flex; justify-content: space-between; align-items: center; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        .status-0 { color: green; }
        .status-1 { color: orange; }
        .status-2 { color: red; }
        .btn { 
            padding: 4px 10px; background: #007bff; color: white;
            text-decoration: none; border-radius: 3px; 
            display: inline-block;
        }
        .btn-disabled { 
            padding: 4px 10px; background: #ccc; color: white;
            border-radius: 3px; display: inline-block; cursor: not-allowed;
        }
    </style>
</head>
<body>

    <!-- 头部 -->
    <div class="header">
        <h1>图书管理系统</h1>
        <div th:if="${session.user != null}">
            欢迎您,<span th:text="${session.user.name}">游客</span>
            (<span th:text="${session.user.role}">USER</span>)
        </div>
        <div th:unless="${session.user != null}">
            欢迎您,游客(未登录)
        </div>
    </div>

    <!-- 搜索栏 -->
    <div style="margin: 20px 0;">
        <form th:action="@{/book/search}" method="get">
            书名:<input type="text" name="name" th:value="${param.name[0] ?: ''}">
            作者:<input type="text" name="author" th:value="${param.author[0] ?: ''}">
            <button type="submit">搜索</button>
            <a th:href="@{/book/list}" class="btn">重置</a>
        </form>
    </div>

    <!-- 图书列表 -->
    <table>
        <thead>
            <tr>
                <th>编号</th>
                <th>书名</th>
                <th>作者</th>
                <th>出版社</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="book : ${books}">
                <td th:text="${book.id}">1</td>
                <td th:text="${book.name}">楚辞</td>
                <td th:text="${book.author}">屈原</td>
                <td th:text="${book.press}">中国文联出版社</td>
                <td>
                    <span th:if="${book.status == '0'}" class="status-0">可借阅</span>
                    <span th:if="${book.status == '1'}" class="status-1">已借出</span>
                    <span th:if="${book.status == '2'}" class="status-2">遗失</span>
                </td>
                <td>
                    <!-- 管理员才显示操作按钮 -->
                    <span th:if="${session.user.role == 'ADMIN'}">
                        <a th:if="${book.status == '0'}"
                           th:href="@{/book/borrow/{id}(id=${book.id})}"
                           class="btn">借阅</a>
                        <span th:if="${book.status != '0'}" class="btn-disabled">不可借</span>
                    </span>
                    <span th:unless="${session.user.role == 'ADMIN'}">无权限</span>
                </td>
            </tr>
        </tbody>
    </table>

    <div style="margin-top: 20px;">
        共 <span th:text="${#lists.size(books)}">0</span> 本图书
    </div>

</body>
</html>

4.9.7 运行测试 ​

启动项目,访问http://localhost:8080/book/list 验证功能:

  1. 页面显示图书列表,状态不同颜色
  2. 搜索功能:输入关键词搜索对应图书
  3. 借阅功能:点击借阅,控制台打印ID
  4. 权限控制:修改角色为USER,操作列显示无权限

4.10 企业最佳实践与常见坑 ​

4.10.1 前后端分离 vs 模板引擎 ​

类型适用场景
前后端分离大型项目、复杂交互、多端共用、对体验要求高
模板引擎中小型项目、后台管理系统、SEO要求高的页面、邮件模板

现状:互联网公司大多前后端分离;传统企业、后台管理系统很多仍用模板引擎。两种都要会。

4.10.2 使用规范 ​

  1. 模板不写复杂业务逻辑:复杂计算、判断放到Controller,模板只负责展示
  2. 公共部分抽取复用:头部、底部统一抽取,不要每个页面重写
  3. XSS安全:用户输入内容用th:text,绝对不要随便用th:utext
  4. 缓存配置:开发关缓存,生产开缓存
  5. 配合Spring Security:sec:标签可以直接在模板做权限判断

4.10.3 新手高频踩坑排查 ​

问题原因解决
返回字符串不是页面@Controller写成@RestController返回页面用@Controller,返回JSON用@RestController
模板找不到404模板放错目录、文件名写错模板必须在templates/目录,大小写拼写一致
th:xxx不生效直接访问HTML文件,没走Controller通过Controller访问,不要直接打开HTML
改页面不生效缓存开着开发环境spring.thymeleaf.cache=false,重启项目
表达式空指针报错对象为null还访问属性先判断非空th:if="${user != null}",或用安全导航${user?.name}
表单重复提交用了forward转发表单提交后用redirect:重定向
th:field绑定失败Model里没有对应对象确保Controller里addAttribute对应名称的对象

4.11 本章小结 ​

  1. 基础认知:模板引擎作用,Thymeleaf自然模板、语法优雅、无缝集成三大特点
  2. 快速入门:引入starter、写Controller、写模板、开发关缓存
  3. 核心属性:th:text、th:each、th:if、th:href、th:field等常用属性
  4. 标准表达式:变量、选择、链接、消息、片段五大表达式
  5. 表单处理:th:object+th:field自动绑定回显,下拉单选复选
  6. 模板布局:th:fragment定义片段,th:replace引用,页面复用
  7. 案例实战:图书管理系统完整实现,覆盖常用功能

4.12 课后作业 ​

一、基础必做题(全员必做,动手实操) ​

  1. 第一个Thymeleaf页面 创建Spring Boot项目,编写/hello接口,返回hello页面,显示“姓名:XXX”和“欢迎学习Spring Boot!”。
  2. 循环与条件练习 编写用户列表页面,遍历3个用户数据,表格展示; 实现隔行变色效果; 年龄≥18显示“成年”,否则显示“未成年”。
  3. 表单练习 编写用户编辑页面,表单回显用户ID、姓名、年龄、邮箱; 提交表单后,重定向到列表页面。
  4. 模板布局练习 抽取公共头部、底部片段; 用户列表页、编辑页都引用公共头尾。
  5. 图书管理案例 完整实现本章图书管理系统,跑通列表、搜索、借阅、权限控制全部功能。

二、进阶选做题(学有余力完成) ​

  1. 国际化:配置中英文两套消息,页面顶部增加中文/English切换按钮。
  2. 权限控制:结合session,管理员显示操作按钮,普通用户不显示。
  3. 分页组件:使用th:each实现简单分页条组件,支持上一页、下一页、页码显示。

三、简答题(面试/笔试) ​

  1. Thymeleaf是什么?有什么核心特点?
  2. th:text和th:utext有什么区别?哪个更安全,为什么?
  3. Thymeleaf常用的表达式有哪些?分别有什么作用?
  4. 模板布局引用片段有哪几种方式?有什么区别?
  5. 为什么开发环境要关闭Thymeleaf缓存?生产环境呢?
  6. 返回页面应该用@Controller还是@RestController?为什么新手容易搞混?

下一章我们学习Spring Boot数据访问,整合MyBatis操作数据库。

基于 Vite 强力驱动 | 纯静态轻量托管