Java Web 21-22
- [21. Thymeleaf](#21. Thymeleaf)
-
- [21.1 当前项目弊端](#21.1 当前项目弊端)
- [21.2 Thymeleaf 概述](#21.2 Thymeleaf 概述)
- [21.3 Thymeleaf 处理流程](#21.3 Thymeleaf 处理流程)
- [21.4 Servlet 中使用 Thymeleaf](#21.4 Servlet 中使用 Thymeleaf)
-
- [22.4.1 添加Thymeleaf 的 jar 包](#22.4.1 添加Thymeleaf 的 jar 包)
- [21.4.2 初始化模板引擎](#21.4.2 初始化模板引擎)
- [21.4.3 编写 Servlet](#21.4.3 编写 Servlet)
- [21.4.4 创建模板文件](#21.4.4 创建模板文件)
- [21.4.5 部署和测试](#21.4.5 部署和测试)
- [21.4.6 thymeleaf 添加日志框架](#21.4.6 thymeleaf 添加日志框架)
-
- [21.4.6.1 添加 logback 的 jar 包](#21.4.6.1 添加 logback 的 jar 包)
- [21.4.6.2 添加 logback 日志框架的配置文件](#21.4.6.2 添加 logback 日志框架的配置文件)
- [21.5 Thymeleaf 的必要语法](#21.5 Thymeleaf 的必要语法)
-
- [21.5.1 **基础模板结构(必须掌握)**](#21.5.1 基础模板结构(必须掌握))
- [21.5.2 **变量表达式(90%场景使用)**](#21.5.2 变量表达式(90%场景使用))
- [21.5.3 **条件判断(if/unless)**](#21.5.3 条件判断(if/unless))
- [21.5.4 关于 if 的小细节](#21.5.4 关于 if 的小细节)
- [21.5.5 **循环遍历(列表渲染)**](#21.5.5 循环遍历(列表渲染))
- [21.5.6 **链接与路径(URL生成)**](#21.5.6 链接与路径(URL生成))
- [21.5.7 **表单绑定(重点!)**](#21.5.7 表单绑定(重点!))
- [21.5.8 **片段复用(布局优化)**](#21.5.8 片段复用(布局优化))
- [21.5.9 **实用工具函数(选学)**](#21.5.9 实用工具函数(选学))
- [21.6 Thymeleaf 变量查找顺序](#21.6 Thymeleaf 变量查找顺序)
- [21.7 Thymeleaf 的其他取值](#21.7 Thymeleaf 的其他取值)
- [21.8 Thymeleaf 的内联 JavaScript 语法](#21.8 Thymeleaf 的内联 JavaScript 语法)
- [22. 改造部门管理](#22. 改造部门管理)
-
- [22.1 整合 Thymeleaf](#22.1 整合 Thymeleaf)
-
- [22.1.1 引入 Thymeleaf 的相关 jar](#22.1.1 引入 Thymeleaf 的相关 jar)
- [22.1.2 监听器初始化引擎模板](#22.1.2 监听器初始化引擎模板)
- [22.1.3 创建 `templates`目录](#22.1.3 创建
templates目录) - [22.1.4 Thymeleaf 整合 logback](#22.1.4 Thymeleaf 整合 logback)
- [22.2 字符编码过滤器](#22.2 字符编码过滤器)
- [22.3 ThymeleafViewServlet](#22.3 ThymeleafViewServlet)
- [22.4 部门列表](#22.4 部门列表)
- [22.5 跳转到添加部门页面](#22.5 跳转到添加部门页面)
- [22.6 添加部门](#22.6 添加部门)
- [22.7 查看部门](#22.7 查看部门)
- [22.8 删除部门](#22.8 删除部门)
- [22.9 跳转到修改页面](#22.9 跳转到修改页面)
- [22.10 修改部门](#22.10 修改部门)
21. Thymeleaf
21.1 当前项目弊端
- 代码耦合性高,难以维护
前端代码(HTML/CSS/JS)与后端Java逻辑混杂在同一个Servlet中,违反**关注点分离(SoC)**原则。修改前端时需要重新编译Servlet,调试困难;团队协作时前后端开发者互相干扰。
- 开发效率低
Java字符串拼接HTML(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">out.println("<div>...</div>")</font>**)极其繁琐且易错。
- 安全性风险
XSS漏洞:手动拼接HTML容易遗漏转义导致 XSS 攻击。
XSS(Cross-Site Scripting) 是一种攻击方式,黑客通过注入恶意脚本(通常是JavaScript)到网页中,使其在其他用户的浏览器里执行。后果:窃取用户Cookie、篡改页面内容、重定向到恶意网站等。
假设有一个Servlet,接收用户输入的**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">name</font>**并显示在页面上:
java
String userName = request.getParameter("name"); // 用户输入
out.println("<div>Welcome, " + userName + "!</div>");
如果用户输入的是:
java
<script>alert('XSS Attack!');</script>
最终生成的HTML会是:
java
<div>Welcome, <script>alert('XSS Attack!');</script>!</div>
浏览器会执行这段脚本,弹出一个警告框(实际攻击可能是窃取Cookie或跳转到恶意网站)。如何解决这个问题,可以手动转义。
但手动转义容易遗漏,因此现代框架(如JSP、Thymeleaf、React、Vue)默认会自动转义。
-
可测试性差:前端逻辑与后端深度耦合,无法独立测试。
-
多端适配困难:同一Servlet无法灵活响应Web/移动端(不同HTML结构或API格式)。
21.2 Thymeleaf 概述
官网地址:https://www.thymeleaf.org/
引用官网(2026 年 4 月): Thymeleaf 3.1.5.RELEASE is the latest version. It requires Java SE 8 or newer(翻译:目前最新发布版 3.1.5,要求 JDK 版本最低 8)
Thymeleaf 是一个用于 Web 和独立环境的现代 Java 模板引擎 ,主要用于处理 HTML、XML、JavaScript 等文件,支持自然模板(允许静态原型直接作为模板使用)。它由Daniel Fernández 领导的团队开发,最初于2011年发布,旨在替代 JSP,提供更优雅的模板解决方案。Thymeleaf 强调语法简洁、与 Spring 框架深度集成,常用于 Spring Boot 项目。目前(截至 2026 年 6 月)仍在积极维护,最新稳定版本为 3.1.5,社区活跃且持续更新。
Java模板引擎 是一种用于动态生成文本(如HTML、XML、邮件内容等)的工具,它通过将静态模板文件 和动态数据结合,生成最终的输出内容。
现代开发中项目多数是前后端分离,Thymeleaf 这种模板技术市场份额在减少,为什么还要保留 Thymeleaf?
- 简单页面需求 :对于管理后台、报表打印、静态内容页等无需复杂前端交互的场景,Thymeleaf直接生成HTML比搭建全套前后端分离项目更高效。
- 快速原型开发:初期验证阶段,用Thymeleaf快速渲染页面比协调前后端联调更省时。
- SEO友好:搜索引擎直接抓取服务端渲染的HTML(前后端分离的SPA需额外SSR处理)。
- 支持混合模式:Thymeleaf允许在传统服务端渲染中逐步引入前端框架(如部分页面用Vue,其他仍用Thymeleaf),适合遗留系统改造。
- 邮件模板:动态生成HTML邮件内容时,模板引擎仍是标准方案。
- PDF/Excel导出:通过模板生成结构化文档(如Apache POI + Thymeleaf)。
21.3 Thymeleaf 处理流程
html
<!-- 模板文件 index.html -->
<html>
<body>
<p th:text="'Hello, ' + ${name}">Hello, Guest</p>
</body>
</html>
Thymeleaf 的处理步骤:
- 模板解析:解析 HTML,识别
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">th:text</font>**表达式。 - 表达式处理:计算表达式
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">${name}</font>**(从 Servlet 作用域中获取值或从 Spring MVC 的 Model 中获取值)。 - 渲染阶段:替换静态文本(
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">Hello, Guest</font>**)为动态值(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">Hello, Alice</font>**)。
提示:
- Thymeleaf 模板是合法的 HTML,可直接用浏览器打开预览。
- Thymeleaf 默认对所有表达式输出进行 HTML 转义,有效防御 XSS。
- Thymeleaf 不依赖 Servlet 容器,可以在非 Web 环境(如邮件模板)中使用。
- Thymeleaf 无 Java 代码嵌入 :所有逻辑通过属性(如
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">th:text</font>**、**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">th:if</font>**)实现。
21.4 Servlet 中使用 Thymeleaf
22.4.1 添加Thymeleaf 的 jar 包
jar 包从 apache 提供的 maven 中央仓库中下载,地址:https://repo.maven.apache.org/maven2/

将 jar 包拷贝 WEB-INF/lib目录下,并且只需要将 thymeleaf-3.1.3.RELEASE.jar添加到 classpath 中:

21.4.2 初始化模板引擎
写一个 ServletContextListener 监听器,在服务器启动阶段,初始化 Thymeleaf 的模板引擎:
java
package com.jkweilai.thymeleaf.listeners;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.WebApplicationTemplateResolver;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
@WebListener
public class ThymeleafInitializer implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 获取ServletContext对象
ServletContext application = sce.getServletContext();
// 创建Thymeleaf与Servlet的适配器,用它处理Web请求
JakartaServletWebApplication jakartaServletWebApplication = JakartaServletWebApplication.buildApplication(application);
// 传入适配器,创建模板解析器
WebApplicationTemplateResolver templateResolver = new WebApplicationTemplateResolver(jakartaServletWebApplication);
// 设置模板文件存放的基础路径
templateResolver.setPrefix("/WEB-INF/templates/");
// 设置模板文件的后缀名
templateResolver.setSuffix(".html");
// 指定解析模式为HTML,启用HTML特有语法支持
templateResolver.setTemplateMode(TemplateMode.HTML);
// 禁用模板缓存(开发环境建议关闭,确保开发时修改模板立即生效)
templateResolver.setCacheable(false);
// 创建模板引擎
TemplateEngine templateEngine = new TemplateEngine();
// 模板引擎关联模板解析器
templateEngine.setTemplateResolver(templateResolver);
// 把适配器保存到ServletContext,供后续请求使用
application.setAttribute("jakartaServletWebApplication", jakartaServletWebApplication);
// 把模板引擎保存到ServletContext,供所有Servlet共享使用
application.setAttribute("templateEngine", templateEngine);
}
}
21.4.3 编写 Servlet
java
package com.jkweilai.thymeleaf.servlets;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 向request域中绑定数据
request.setAttribute("message", "Hello from Request Scope!");
request.setAttribute("currentTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
request.setAttribute("requestURI", request.getRequestURI());
// 交给模板引擎去处理
response.setContentType("text/html;charset=UTF-8");
ServletContext application = getServletContext();
// 从上下文中获取Thymeleaf的Servlet适配器实例
JakartaServletWebApplication jakartaServletWebApplication = (JakartaServletWebApplication)application.getAttribute("jakartaServletWebApplication");
// 从上下文中获取Thymeleaf模板引擎实例
TemplateEngine templateEngine = (TemplateEngine) application.getAttribute("templateEngine");
// 创建Thymeleaf上下文对象,包装当前请求/响应和语言环境
// buildExchange() 把 request/response 包装成 Thymeleaf 的 IWebExchange 接口对象(Thymeleaf不依赖Servlet API)
// request.getLocale() 获取浏览器期望的语言偏好
// WebContext 是 Thymeleaf 的 Web 环境上下文对象,它承载了一次请求所需的所有数据和工具,供模板引擎渲染时使用。
WebContext webContext = new WebContext(jakartaServletWebApplication.buildExchange(request, response), request.getLocale());
// 使用模板引擎渲染名为"hello"的模板,结果写入响应输出流
templateEngine.process("hello", webContext, response.getWriter());
}
}
21.4.4 创建模板文件
在WEB-INF/templates/下创建hello.html:
html
<!DOCTYPE html>
<!--必须添加 th 命名空间-->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>first thymeleaf</title>
</head>
<body>
<!-- 直接从request域获取数据 -->
<h1 th:text="${message}">Default Message</h1>
<p>Current time: <span th:text="${currentTime}"></span></p>
<p>Request URI: <span th:text="${requestURI}"></span></p>
</body>
</html>
21.4.5 部署和测试
- 部署到Servlet容器(如Tomcat)
- 访问
[http://localhost:8080/thymeleaf/hello](http://localhost:8080/thymeleaf/hello)

21.4.6 thymeleaf 添加日志框架
thymeleaf 添加日志框架 logback。这样 thymeleaf 运行出错时会自动打印日志信息。便于调试。
21.4.6.1 添加 logback 的 jar 包
我们项目中已经存在 slf4j-api-2.0.16.jar。这个是日志门面,还需要一个具体的实现 slf4j 门面的日志框架,logback就是一个具体的实现。
首先我们需要引入 logback 的 jar 包,放到 WEB-INF/lib 目录下:

21.4.6.2 添加 logback 日志框架的配置文件
在类的根路径下新建 logback.xml,然后将配置以下内容:
xml
<configuration>
<!-- 只输出 ERROR 级别及以上的日志 -->
<logger name="org.thymeleaf" level="ERROR"/>
<!-- 关闭 Thymeleaf 初始化 DEBUG 日志 -->
<logger name="org.thymeleaf.TemplateEngine" level="INFO"/>
<!-- 控制台输出格式简化 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level - %msg%n</pattern>
</encoder>
</appender>
<!-- 全局日志级别设为 WARN -->
<root level="WARN">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
21.5 Thymeleaf 的必要语法
21.5.1 基础模板结构(必须掌握)
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <!-- 启用Thymeleaf命名空间 -->
<head>
<title th:text="${pageTitle}">默认标题</title> <!-- 静态默认值会被动态替换 -->
</head>
<body>
<!-- 所有Thymeleaf属性以 th: 开头 -->
</body>
</html>
关键点:
th:命名空间声明- 静态内容作为默认值(直接打开HTML文件可预览)
21.5.2 变量表达式(90%场景使用)
html
<div th:text="${user.name}">用户名默认值</div> <!-- 即使是一段HTML代码,也只是当做普通文本处理,转义,防止XSS攻击 -->
<div th:utext="${htmlContent}">HTML内容</div> <!-- 对HTML代码解释执行,不转义,存在XSS攻击风险,谨慎使用 -->
21.5.3 条件判断(if/unless)
和 if/else不一样。两个是独立的,没有关系。
- 对于
th:if来说,为true时显示元素。 - 对于
th:unless来说,为false时显示元素。
html
<div th:if="${user.isAdmin}">管理员可见</div>
<div th:unless="${user.isGuest}">非访客可见</div>
21.5.4 关于 if 的小细节
有这样一个 VO(View Object:视图对象,负责专门在页面上展示数据的对象)UserVO,代码如下:
java
package com.jkweilai.thymeleaf.model.vo;
public class UserVO {
private String username;
private Boolean isAdmin; // 注意这个属性哈。
public UserVO() {
}
public UserVO(String username, Boolean isAdmin) {
this.username = username;
this.isAdmin = isAdmin;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Boolean getAdmin() { // isAdmin 属性生成的get方法不是getIsAdmin(),而是getAdmin()。
return isAdmin;
}
public void setAdmin(Boolean admin) {
isAdmin = admin;
}
}
在 Servlet 当中创建 UserVO 对象,代码如下:
java
UserVO user = new UserVO("admin", true);
request.setAttribute("user", user);
thymeleaf 的模板文件中是这样写的:
html
<div th:if="${user.isAdmin}"><span th:text="${user.username}"></span>是管理员</div>
测试时出现了以下异常:

这个问题是因为我们使用了 ${user.isAdmin},thymeleaf 底层会去找 user对象的 getIsAdmin()方法,但是这个方法不存在而导致的。针对这个问题,有多种解决方案,最直接的方式是将模板文件中的代码修改一下,修改为:
html
<div th:if="${user.admin}"><span th:text="${user.username}"></span>是管理员</div>
这样就可以访问了。
21.5.5 循环遍历(列表渲染)
html
<ul>
<li th:each="item : ${items}" th:text="${item.name}">商品示例</li>
</ul>
关键点:
th:each="item : ${集合}"语法- 循环状态变量:
html
<li th:each="item,status : ${items}" th:text="|${status.index} ======== ${item.name}|"></li>
双竖线 |...| 的作用是定义 文本字面量表达式(Literal Expressions),它是一种简化字符串拼接的语法。
管道符内的内容会被视为一个整体,Thymeleaf 会自动将其拼接成一个字符串,无需手动加引号或 + 连接。
21.5.6 链接与路径(URL生成)
html
<a th:href="@{/user/details(id=${userId})}">用户详情</a>
关键点:
@{}自动生成上下文路径(避免硬编码)- 支持参数:
@{/path(param1=${val1}, param2=${val2})}
21.5.7 表单绑定(重点!)
html
<form th:action="@{/save}" th:object="${user}" method="post">
<input type="text" id="username" name="username" th:value="*{username}" placeholder="用户名">
<input type="submit" value="保存">
</form>
关键点:
th:object绑定表单对象th:value="*{属性名}"
21.5.8 片段复用(布局优化)
html
<!-- 定义片段 -->
<div th:fragment="header">
<h1>公共头部</h1>
</div>
<!-- 引用片段 -->
<div th:replace="~{::header}"></div> <!-- header片段的定义如果在当前文件中的话,使用这种语法。 -->
<div th:replace="~{commons :: header}"></div> <!-- 这种语法表示引入commons.html中定义的header片段。 -->
关键点:
th:fragment定义可复用块th:replace直接替换当前标签
21.5.9 实用工具函数(选学)
html
<p th:text="${#strings.toUpperCase(user.name)}"></p> <!-- 字符串大写 -->
<p th:text="${#dates.format(now, 'yyyy-MM-dd')}"></p> <!-- 日期格式化 -->
工具类:
#strings、#numbers、#dates、#lists等。
工具函数的具体使用,可以参考 thymeleaf 的官方帮助文档,按照以下步骤查找帮助:
首页面点 Docs:

Read online在线阅读,或者下载 PDF等:

在以下页面中搜索,例如搜索 #strings:

21.6 Thymeleaf 变量查找顺序
当 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">${message}</font>** 被解析时,Thymeleaf 会按以下顺序查找:
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">WebContext</font>**显式设置的变量 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ctx.setVariable("message", "WebContext message")</font>**)**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">HttpServletRequest</font>**属性 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">request.setAttribute("message", "request message")</font>**)
同名变量时,高优先级的会覆盖低优先级的。
21.7 Thymeleaf 的其他取值
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">HttpSession</font>**属性 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">session.setAttribute("message", "session message")</font>**),从 session 中取值需要这样写:<font style="color:rgb(64, 64, 64);">${session.message}</font>**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ServletContext</font>**全局属性 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">servletContext.setAttribute("message", "application message")</font>**),从 application 中取值需要这样写:<font style="color:rgb(64, 64, 64);">${application.message}</font>- 请求参数 (
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">request.getParameter("message")</font>**),从查询参数中取值需要这样写:<font style="color:rgb(64, 64, 64);">${param.message}</font> **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">th:with</font>******局部变量(仅限当前作用域)
html
<div th:with="message='Local Message'">
<span th:text="${message}"></span> <!-- 输出 "Local Message" -->
</div>
21.8 Thymeleaf 的内联 JavaScript 语法
html
<!-- script脚本中使用Thymeleaf语法需要在script标签上添加 th:inline="javascript" -->
<script th:inline="javascript">
// Thymeleaf的内联JavaScript语法
// 也就是说:/*[[这里可以编写thymeleaf的语法]]*/
let contextPath = /*[[@{/}]]*/ "";
function deleteByDeptno(elt){
let deptno = elt.getAttribute("data-deptno");
if(confirm("您确定删除数据吗?")){
document.location.href = contextPath + "deleteByNo?deptno=" + deptno;
}
}
</script>
22. 改造部门管理
Servlet + Thymeleaf 改造之前的部门管理系统。
Servlet 负责核心业务的处理,将处理完成的数据收集起来。交给 Thymeleaf 来负责数据的展示。各司其职,分工协作。
22.1 整合 Thymeleaf
22.1.1 引入 Thymeleaf 的相关 jar

然后将 thymeleaf jar放到 classpath中。
这里将 logback的 jar 包一并引进来了。
22.1.2 监听器初始化引擎模板
java
package com.jkweilai.dept.listeners;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.WebApplicationTemplateResolver;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
@WebListener
public class ThymeleafInitializer implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 获取ServletContext对象
ServletContext application = sce.getServletContext();
// 创建Thymeleaf与Servlet容器的桥梁对象
JakartaServletWebApplication jakartaServletWebApplication = JakartaServletWebApplication.buildApplication(application);
// 创建模板解析器并关联到Servlet环境
WebApplicationTemplateResolver templateResolver = new WebApplicationTemplateResolver(jakartaServletWebApplication);
// 设置模板文件存放的基础路径
templateResolver.setPrefix("/WEB-INF/templates/");
// 设置模板文件的后缀名
templateResolver.setSuffix(".html");
// 指定模板处理模式为HTML格式
templateResolver.setTemplateMode(TemplateMode.HTML);
// 禁用模板缓存(开发环境建议关闭)
templateResolver.setCacheable(false);
// 创建模板引擎
TemplateEngine templateEngine = new TemplateEngine();
// 模板引擎关联模板解析器
templateEngine.setTemplateResolver(templateResolver);
// 将Thymeleaf与Servlet容器的桥梁对象存储到应用域中
application.setAttribute("jakartaServletWebApplication", jakartaServletWebApplication);
// 将模板引擎对象存储到应用域中
application.setAttribute("templateEngine", templateEngine);
}
}
22.1.3 创建 templates目录

22.1.4 Thymeleaf 整合 logback
类的根路径下新建 logback.xml文件,提供以下配置:
xml
<configuration>
<!-- 只输出 ERROR 级别及以上的日志 -->
<logger name="org.thymeleaf" level="ERROR"/>
<!-- 关闭 Thymeleaf 初始化 DEBUG 日志 -->
<logger name="org.thymeleaf.TemplateEngine" level="INFO"/>
<!-- 控制台输出格式简化 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level - %msg%n</pattern>
</encoder>
</appender>
<!-- 全局日志级别设为 WARN -->
<root level="WARN">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
22.2 字符编码过滤器
提供一个字符编码过滤器,过滤所有的请求,解决请求体的中文乱码问题以及响应的中文乱码问题。
java
package com.jkweilai.dept.filters;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
filterChain.doFilter(request, response);
}
}
22.3 ThymeleafViewServlet
Servlet 处理完业务之后,交给 Thymleaf 来处理页面,都要编写以下的代码:
java
ServletContext application = getServletContext();
// 从上下文中获取Thymeleaf的Servlet适配器实例
JakartaServletWebApplication jakartaServletWebApplication = (JakartaServletWebApplication)application.getAttribute("jakartaServletWebApplication");
// 从上下文中获取Thymeleaf模板引擎实例
TemplateEngine templateEngine = (TemplateEngine) application.getAttribute("templateEngine");
// 创建Thymeleaf上下文对象,包装当前请求/响应和语言环境
WebContext webContext = new WebContext(jakartaServletWebApplication.buildExchange(request, response), request.getLocale());
// 使用模板引擎渲染名为"hello"的模板,结果写入响应输出流
templateEngine.process("hello", webContext, response.getWriter());
我们最好将以上的代码封装到一个 Servlet 当中,例如 ThymeleafViewServlet,这样代码可以得到复用:
java
package com.jkweilai.dept.servlets;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
import java.io.IOException;
@WebServlet("/view")
public class ThymeleafViewServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletContext application = getServletContext();
// 从上下文中获取Thymeleaf的Servlet适配器实例
JakartaServletWebApplication jakartaServletWebApplication = (JakartaServletWebApplication) application.getAttribute("jakartaServletWebApplication");
// 从上下文中获取Thymeleaf模板引擎实例
TemplateEngine templateEngine = (TemplateEngine) application.getAttribute("templateEngine");
// 创建Thymeleaf上下文对象,包装当前请求/响应和语言环境
WebContext webContext = new WebContext(jakartaServletWebApplication.buildExchange(request, response), request.getLocale());
// 获取模板名称
String template = (String) request.getAttribute("template"); // 先从request域中获取模板名称
if (template == null) {
template = request.getParameter("template"); // 如果request域中没有,再从请求参数上获取模板名称
}
// 使用模板引擎渲染模板,结果写入响应输出流
templateEngine.process(template, webContext, response.getWriter());
}
}
当以后需要让 Thymeleaf进行页面渲染时,可以编写以下代码:
java
// 向请求域中绑定模板名称
request.setAttribute("template", "list");
// 转发到 ThymeleafViewServlet 做页面渲染
request.getRequestDispatcher("/view").forward(request, response);
如果不能使用转发,必须使用重定向时,也可以通过请求参数来传递模板名称,例如:
java
response.sendRedirect(request.getContextPath() + "/view?template=list");
22.4 部门列表
封装 Dept类:
java
package com.jkweilai.dept.model;
public class Dept {
private Integer deptno;
private String dname;
private String loc;
// constructor
// setter and getter
// toString
}
DeptListServlet负责数据的收集:
java
package com.jkweilai.dept.servlets;
import com.jkweilai.dept.model.Dept;
import com.jkweilai.dept.utils.DbUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
@WebServlet("/list")
public class DeptListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Dept> deptList = new ArrayList<>();
// 连接数据库
Connection conn = null;
PreparedStatement pst = null;
ResultSet rs = null;
try {
conn = DbUtils.getConnection();
String sql = "select deptno,dname,loc from dept";
pst = conn.prepareStatement(sql);
rs = pst.executeQuery();
while(rs.next()){
String deptno = rs.getString("deptno");
String dname = rs.getString("dname");
String loc = rs.getString("loc");
// 封装对象
Dept dept = new Dept(Integer.valueOf(deptno),dname,loc);
// 存储到List集合
deptList.add(dept);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DbUtils.close(conn, pst, rs);
}
// 将list集合存储到request域
request.setAttribute("depts", deptList);
// 将模板名字存储到request域中
request.setAttribute("template", "list");
// 转发给Thymeleaf做页面渲染
request.getRequestDispatcher("/view").forward(request, response);
}
}
Thymeleaf 的模板页面**WEB-INF/templates/list.html**来负责数据展示:
html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>部门管理系统 - 部门列表</title>
<link rel="stylesheet" th:href="@{/css/list.css}">
</head>
<body>
<div class="container">
<div class="header">
<h1>部门列表</h1>
<a href="" class="add-btn">添加部门</a>
</div>
<table class="department-table">
<thead>
<tr>
<th>部门编号</th>
<th>部门名称</th>
<th>部门地理位置</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="dept : ${depts}">
<td th:text="${dept.deptno}"></td>
<td th:text="${dept.dname}"></td>
<td th:text="${dept.loc}"></td>
<td>
<a th:href="@{/detail(deptno=${dept.deptno})}" class="action-btn view-btn">查看</a>
<a th:href="@{/edit(deptno=${dept.deptno})}" class="action-btn edit-btn">修改</a>
<a th:href="@{/delete(deptno=${dept.deptno})}" class="action-btn delete-btn">删除</a>
</td>
</tr>
</tbody>
</table>
<div class="logout">
<a href="">退出登录</a>
</div>
</div>
</body>
</html>
注意:
- 以上代码中,把 css 样式单独提取出来放到
css/list.css文件中了,因此以上代码添加链接外部 css 样式文件。css 文件不能放到**<font style="color:#DF2A3F;">WEB-INF</font>**目录下。 - 当前页面中的 查看、修改、删除 等超链接的路径已经修改完毕,请参照以上代码。
22.5 跳转到添加部门页面

在 list.html页面中找到上图的添加部门 按钮。将请求路径修改为 th:href="@{/view(template=add)}"

该请求路径通过执行 ThymeleafViewServlet 来跳转到 WEB-INF/templates/add.html页面,因为 add.html页面中也需要使用 thymleaf 语法动态设置请求路径,因此需要将之前的 add.html移动到 WEB-INF/templates目录下:

22.6 添加部门
添加部门页面 add.html代码如下:(记得把 css 单独提取出来)
html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>部门管理系统 - 添加部门</title>
<link th:href="@{/css/add.css}" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header">
<h1>添加新部门</h1>
<a th:href="@{/list}" class="back-btn">返回列表</a>
</div>
<form th:action="@{/save}" method="post">
<div class="form-group">
<label for="deptName">部门名称</label>
<input type="text" id="deptName" name="dname" placeholder="请输入部门名称" required>
</div>
<div class="form-group">
<label for="location">部门地理位置</label>
<input type="text" id="location" name="loc" placeholder="请输入部门地理位置" required>
</div>
<div class="footer">
<button type="submit" class="submit-btn">保存</button>
</div>
</form>
</div>
</body>
</html>
注意:表单 form 的 th:action属性,设置表单提交的请求路径。
修改 DeptSaveServlet:
java
package com.jkweilai.dept.servlets;
import com.jkweilai.dept.utils.DbUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@WebServlet("/save")
public class DeptSaveServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取部门信息
String dname = request.getParameter("dname");
String loc = request.getParameter("loc");
// 连接数据库保存数据
// 查询当前最大的部门编号,新部门编号在当前最大部门编号基础上 + 1
Connection conn = null;
PreparedStatement ps = null;
PreparedStatement ps1 = null;
ResultSet rs = null;
try {
conn = DbUtils.getConnection();
String sql = "select max(deptno) as maxDeptno from dept";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
if (rs.next()) {
int maxDeptno = rs.getInt("maxDeptno");
synchronized (this){
int deptno = maxDeptno + 1;
String insertSql = "insert into dept values(?,?,?)";
ps1 = conn.prepareStatement(insertSql);
ps1.setInt(1, deptno);
ps1.setString(2, dname);
ps1.setString(3, loc);
ps1.executeUpdate();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DbUtils.close(null, ps1, null);
DbUtils.close(conn, ps, rs);
}
response.sendRedirect(request.getContextPath() + "/list");
}
}
22.7 查看部门
修改 DeptDetailServlet程序:
java
package com.jkweilai.dept.servlets;
import com.jkweilai.dept.model.Dept;
import com.jkweilai.dept.utils.DbUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@WebServlet("/detail")
public class DeptDetailServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String deptno = request.getParameter("deptno");
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Dept dept = null;
try {
conn = DbUtils.getConnection();
String sql = "select * from dept where deptno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, deptno);
rs = ps.executeQuery();
if(rs.next()) {
String dname = rs.getString("dname");
String loc = rs.getString("loc");
dept = new Dept(Integer.parseInt(deptno), dname, loc);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DbUtils.close(conn, ps, rs);
}
// 将dept存储到request域
request.setAttribute("dept", dept);
// 将模板名字存储到request域
request.setAttribute("template", "detail");
// 转发给Thymeleaf
request.getRequestDispatcher("/view").forward(request, response);
}
}
编写 WEB-INF/templates/detail.html:(记得将 css 单独提取出来)
html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>部门管理系统 - 部门详情</title>
<link rel="stylesheet" th:href="@{/css/detail.css}">
</head>
<body>
<div class="container">
<div class="header">
<h1>部门详细信息</h1>
<a th:href="@{/list}" class="back-btn">返回列表</a>
</div>
<div class="detail-card">
<div class="detail-row">
<div class="detail-label">部门编号</div>
<div class="detail-value" th:text="${dept.deptno}"></div>
</div>
<div class="detail-row">
<div class="detail-label">部门名称</div>
<div class="detail-value" th:text="${dept.dname}"></div>
</div>
<div class="detail-row">
<div class="detail-label">部门地理位置</div>
<div class="detail-value" th:text="${dept.loc}"></div>
</div>
</div>
<div class="action-btns">
<a th:href="@{/edit(deptno=${dept.deptno})}" class="edit-btn">编辑部门信息</a>
</div>
</div>
</body>
</html>
22.8 删除部门
只需要将 list.html页面中 删除 按钮发送的请求路径:

其它代码不需要修改。
22.9 跳转到修改页面
修改 DeptEditServlet,代码如下:
java
package com.jkweilai.dept.servlets;
import com.jkweilai.dept.model.Dept;
import com.jkweilai.dept.utils.DbUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@WebServlet("/edit")
public class DeptEditServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取部门编号
String deptno = request.getParameter("deptno");
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Dept dept = null;
try {
conn = DbUtils.getConnection();
String sql = "select * from dept where deptno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, deptno);
rs = ps.executeQuery();
if (rs.next()) {
String dname = rs.getString("dname");
String loc = rs.getString("loc");
dept = new Dept(Integer.parseInt(deptno), dname, loc);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DbUtils.close(conn, ps, rs);
}
request.setAttribute("dept", dept);
request.setAttribute("template", "edit");
request.getRequestDispatcher("/view").forward(request, response);
}
}
编写 WEB-INF/templates/edit.html,代码如下:(记得把 css 单独提取出来)
html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>部门管理系统 - 修改部门</title>
<link th:href="@{/css/edit.css}" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header">
<h1>修改部门信息</h1>
<a th:href="@{/list}" class="back-btn">返回列表</a>
</div>
<form th:action="@{/update}" th:object="${dept}" method="post">
<div class="form-group">
<label for="deptId">部门编号</label>
<input type="text" id="deptId" name="deptno" th:value="*{deptno}" readonly>
</div>
<div class="form-group">
<label for="deptName">部门名称</label>
<input type="text" id="deptName" name="dname" th:value="*{dname}" required>
</div>
<div class="form-group">
<label for="location">部门地理位置</label>
<input type="text" id="location" name="loc" th:value="*{loc}" required>
</div>
<div class="btn-group">
<a href="javascript:void(0)" class="cancel-btn" onclick="window.history.back()">取消</a>
<button type="submit" class="submit-btn">保存更改</button>
</div>
</form>
</div>
</body>
</html>
22.10 修改部门
这个功能基本上不需要修改,只需要将 解决请求体 的中文乱码问题 删除即可,因为字符编码过滤器:
java
package com.jkweilai.dept.servlets;
import com.jkweilai.dept.utils.DbUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
@WebServlet("/update")
public class DeptUpdateServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取表单提交的数据
String deptno = request.getParameter("deptno");
String dname = request.getParameter("dname");
String loc = request.getParameter("loc");
// 连接数据库更新部门
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DbUtils.getConnection();
String sql = "update dept set dname = ?, loc = ? where deptno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, dname);
ps.setString(2, loc);
ps.setString(3, deptno);
ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
DbUtils.close(conn, ps, null);
}
// 重定向到列表页面
response.sendRedirect(request.getContextPath() + "/list");
}
}
到此为止,我们之前实现的所有功能,就使用 thymeleaf 完成了所有的改造。大家记得跑通之后,让页面中所有的操作按钮,全部能用。