Java Web 21-22

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 当前项目弊端

  1. 代码耦合性高,难以维护

前端代码(HTML/CSS/JS)与后端Java逻辑混杂在同一个Servlet中,违反**关注点分离(SoC)**原则。修改前端时需要重新编译Servlet,调试困难;团队协作时前后端开发者互相干扰。

  1. 开发效率低

Java字符串拼接HTML(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">out.println("<div>...</div>")</font>**)极其繁琐且易错。

  1. 安全性风险

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)默认会自动转义。

  1. 可测试性差:前端逻辑与后端深度耦合,无法独立测试。

  2. 多端适配困难:同一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 的处理步骤:

  1. 模板解析:解析 HTML,识别 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">th:text</font>** 表达式。
  2. 表达式处理:计算表达式 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">${name}</font>**(从 Servlet 作用域中获取值或从 Spring MVC 的 Model 中获取值)。
  3. 渲染阶段:替换静态文本(**<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>**)。

提示:

  1. Thymeleaf 模板是合法的 HTML,可直接用浏览器打开预览。
  2. Thymeleaf 默认对所有表达式输出进行 HTML 转义,有效防御 XSS。
  3. Thymeleaf 不依赖 Servlet 容器,可以在非 Web 环境(如邮件模板)中使用。
  4. 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 部署和测试

  1. 部署到Servlet容器(如Tomcat)
  2. 访问 [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 会按以下顺序查找:

  1. **<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>**)
  2. **<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 的其他取值

  1. **<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>
  2. **<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>
  3. 请求参数 (**<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>
  4. **<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>

注意:

  1. 以上代码中,把 css 样式单独提取出来放到 css/list.css文件中了,因此以上代码添加链接外部 css 样式文件。css 文件不能放到 **<font style="color:#DF2A3F;">WEB-INF</font>**目录下。
  2. 当前页面中的 查看、修改、删除 等超链接的路径已经修改完毕,请参照以上代码。

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 完成了所有的改造。大家记得跑通之后,让页面中所有的操作按钮,全部能用。

相关推荐
青柠之夏cc1 小时前
Python Java PHP底层对比:内存并发性能全解析
开发语言·python·线程
半亩码田1 小时前
C#转Python第5.1篇:异步编程:asyncio vs async/await,语法相似但机制不同
开发语言·python·c#
灯澜忆梦1 小时前
【Linux】#5 | 文件与目录基础操作命令
java·linux·运维
字节跳动的猫1 小时前
LikeShop 搜索功能优化二开:中文分词、商品权重排序与搜索热词管理
android·java·中文分词
计算机毕设定制辅导-无忧学长1 小时前
《基于微信小程序的校园文化活动报名与管理系统》
java·spring boot·微信小程序·uniapp·校园文化活动报名与管理小程序
靠沿1 小时前
MySQL数据库基础——索引的使用&alter、explain关键字
java·数据库·mysql
9624561 小时前
餐饮 SaaS 优惠券系统架构演进(七)营销定价引擎设计:活动、会员价、优惠券、买赠如何在同一 PriceQuote 中可解释地组合
java·运维·算法
码匠许师傅1 小时前
【C++三方组件】redis-plus-plus:现代 Redis C++ 客户端
java·c++·redis
LMT_Lanmantech1 小时前
MATLAB合规管理:如何避免侵权赔偿和审计风险
开发语言·matlab·信息可视化