第8章 Thymeleaf¶
第 8 章¶
Thymeleaf¶
第 7 章介绍了 Servlet 的相关知识和应用,借助 Servlet 实现了接收客户端的请求数据,并生成响应结果最终返回给客户端的过程。但返回的数据一般为文本数据,如果我们想直接返回一个页面,该如何实现呢?虽然借助 Servlet 可以通过手写输出流的方式输出页面,但代码太过烦琐,不推荐使用,这就用到了本章将要学习的内容 ——Thymeleaf。Thymeleaf 是一个模板引擎,主要作用是把响应回客户端的数据渲染到 HTML 页面中。本章内容主要包括 Thymeleaf 的介绍、基本语法、案例应用,以及 MVC 模型的介绍等。
8.1 MVC 简介¶
在学习 Thymeleaf 前,首先需要了解一下 MVC 和三层架构的概念。MVC, 全称为 Model View Controller, 是在表述层开发中运用的一种设计理念。主要目的是把封装数据的模型、显示用户界面的视图、协调调度的控制器区分开来。
另外还有一种软件开发模式,即三层架构,与 MVC 类似也分为三层,分别是 UI 层表示用户界面、BLL 层表示业务逻辑,以及 DAL 层表示数据访问。两者具有相同的设计理念,即把视图设计与数据持久化进行分离,从而降低耦合性,易于扩展,提高团队开发效率,以至于很多程序员将 MVC 当作三层架构来使用,但其实两者并不相同,三层架构是基于业务逻辑划分的,而 MVC 则基于页面划分,MVC 和三层架构之间的关系如图 8-1 所示。

MVC 的处理过程如下,首先控制器接受用户的请求,并决定应该调用哪个模型来进行处理,然后模型用业务逻辑来处理用户的请求并返回数据,最后控制器用相应的视图对该模型返回的数据进行格式化,并通过表示层呈现给用户。
使用 MVC 模型,能够将视图分离出来,进一步实现各个组件之间的解耦,让各个组件可以单独维护,同时让后端工程师和前端工程师的对接变得更方便。
MVC 可以是三层框架中的一个表现层框架,属于表现层。也就是说,MVC 把三层架构中的表现层再度进行了分化,分成了 Controller、View、Model 三部分,只不过实体的应用贯穿整个三层架构。三层架构

和 MVC 是可以共存的,并且三层架构的分层模式是典型的上下级关系,上层依赖于下层,而 MVC 作为表现模式是不存在上下级关系的,属于相互协作关系。
使用 MVC 的目的在于将 M(模型)和 V(用户界面)的实现代码分离,从而使同一个程序可以使用不同的表现形式。C(控制器)存在的目的则是确保 M 和 V 同步,一旦 M 改变,V 应该同步更新。Model、View 和 Controller 三者的关系如图 8-2 所示。
而对于 MVC 的视图层,我们将借助 Thymeleaf 实现,Thymeleaf 对视图层进行了封装,在静态页面上渲染显示动态
数据,简化视图层的操作,从而实现组件间解耦合,可以单独维护。
8.2 初识 Thymeleaf¶
8.2.1 什么是 Thymeleaf¶
Thymeleaf 是一个现代化的、渲染 XML/XHTML/HTML5 等内容的、服务端的 Java 模板引擎。类似 JSP、Velocity、FreeMaker 等,它可以与 Spring MVC 等 Web 框架进行集成,作为 Web 应用的模板引擎。它的主要作用是在静态页面上渲染显示动态数据。面向于后端开发人员,其最大的优势就是,它是一个自然语言的模板,语法非常简单,相比其他模板引擎,上手较快,比较适合简单的单体应用。不足之处在于,Thymeleaf 不是高性能的模板引擎,如果我们要开发高并发应用,并且需要实现页面跳转功能,最好使用前后端分离技术。
另外值得一提的是,Thymeleaf 是 SpringBoot 官方推荐使用的视图模板技术,能够与 SpringBoot 完美整合,而且 Thymeleaf 不经过服务器端运算仍然可以直接查看原始值,对于前端工程师而言,同样很友好。使用 Thymeleaf 渲染的前端页面,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${username}">Original Value</p>
</body>
</html>
其中,username 是被放在请求域中的数据,通过上述代码的方式,可以将该属性对应的具体值渲染到页面中。
8.2.2 物理视图和逻辑视图¶
在 Servlet 中,将请求转发到一个 HTML 页面文件时,使用完整的转发路径,被称为物理视图。例如,对于该项目的 login_success.html 页面,对应的物理视图为 “/pages/user/login_success.html”,项目的目录结构如图 8-3 所示。

如果我们把所有的 HTML 页面都放在某个统一的目录下,那么转发地址就会呈现出明显的规律。
/pages/user/login.html
/pages/user/login_success.html
/pages/user/regist.html
/pages/user/regist_success.html
.....
综上可知,路径的开头均为 “/pages/user/”,路径的结尾均为 “.html”。为了方便管理,把完整的路径分为三部分,并进行了统一命名,路径开头的部分称为视图前缀,路径结尾的部分称为视图后缀,路径中间不同的部分称为逻辑视图,如表 8-1 所示。也就是说,物理视图 = 视图前缀 + 逻辑视图 + 视图后缀。
表 8-1 物理视图的划分
| 物理视图 | 视图前缀 | 逻辑视图 | 视图后缀 |
| /pages/user/login.html | /pages/user/ | login | .html |
| /pages/user/login_success.html | /pages/user/ | login_success | .html |
在之后的开发中,我们会将视图前缀和视图后缀统一设置,编写代码时只需提供逻辑视图即可。
8.3 Thymeleaf 入门案例¶
下面编写一个 Thymeleaf 的入门案例,来进一步了解。
(1)首先创建 Web 项目,命名为 “chapter08_thymeleaf”,并创建 lib 文件夹导入所需 jar 包,创建 views 文件夹添加所需页面,目录结构如图 8-4 所示。
(2)配置全局初始化参数,设置视图前缀和视图后缀。
在 web.xml 文件中对参数进行配置,示例代码如下。其中 key 值为 view-prefix,该值是可以自定义的,但对应的 value 值,必须根据实际路径确定。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee"
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--配置全局初始化参数:配置 Thymeleaf 视图的前缀和后缀-->
<context-param>
<!--key 值是自定义的-->
<param-name>view-prefix</param-name>
<!--Value 值是根据你的项目结构分析出来的-->
<param-value>/views;</param-value>
</context-param>
<context-param>
<param-name>view-suffix</param-name>
<param-value>.html</param-value>
</context-param>
</web-app>
需要注意的是,Thymeleaf 只能渲染视图前缀和视图后缀中间的网页文件。不符合视图前缀和视图后缀的其他路径都无法被渲染。
(3)创建 Servlet 基类,命名为 KuwaitBaseServlet 示例代码如下。
该类暂时不用手动编写,直接复制过来使用即可,后续使用框架编程后,Servlet 类将被完全取代。
package com.atguigu.servlet;
//省略 import 语句
public class++,
ViewBaseServlet extends Web
{
//模板引擎
private TemplateEngine templateEngine;
@Override
public void init() throws ServletException {
// 1.获取 ServletContext 对象
ServletContext servletContext = this.getServletContext();
// 2.创建 Thymeleaf 解析器对象
ServletContextTemplateResolver templateResolver
= new ServletContextTemplateResolver(servletContext);
// 3.给解析器对象设置参数
// ①HTML 是默认模式,明确设置是为了代码更容易理解
templateResolver.setTemplateMode(TemplateMode.HTML);
// ②设置前缀
String viewPrefix = servletContext.getInitParameter("view-prefix");
templateResolver.setPrefix(viewPrefix);
// ③设置后缀
String viewSuffix = servletContext.getInitParameter("view-suffix");
templateResolver.setSuffix(viewSuffix);
// ④设置缓存过期时间(毫秒)
templateResolver.setCacheTTLMs(60000L);
// ⑤设置是否缓存
templateResolver.setCacheable(true);
// ⑥设置服务器端编码方式
templateResolver.setCharacterEncoding("utf-8");
// 4.创建模板引擎对象
templateEngine = new TemplateEngine();
// 5.给模板引擎对象设置模板解析器
templateEngine.setTemplateResolver(templateResolver);
}
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1.设置响应体内容类型和字符集
resp.setContentType("text/html;charset=UTF-8");
// 2.创建WebContext 对象
WebContext webContext = new WebContext(req, resp, getServletContext());
}
}
上述代码中 processTemplate () 方法的功能,就是对静态页面进行动态数据渲染。包含了三个参数,其中 templateName 表示逻辑视图;req 为请求域对象,携带请求数据;resp 为响应体对象,携带服务器端要返回给浏览器的内容。原理是先通过模板引擎渲染数据,然后转发到指定页面,由于采用转发方式,所以请求域中的共享数据也是可以传递到客户端的。
(4)编写 index.html 文件创建超链接,单击访问 ThymeleafTestServlet 类,示例代码如下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<a href="thymeleafTest">单击访问 ThymeleafTestServlet</a>
</body>
</html>
(5)创建 ThymeleafTestServlet 类,继承 KuwaitBaseServlet 类,然后调用其 init () 方法进行初始化,调用 processTemplate () 方法实现页面渲染及转发操作。同样使用 @WebServlet 注解将该类注入容器,且映射路径和 index.html 文件中保持一致,为 “/thymeleafTest”,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebSocket("/thymeleafTest")
public class ThymeleafTestServlet extends++,ViewBaseServlet {
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
System.out.println("访问到了 ThymeleafTestServlet");
String msg="用户名或密码错误";
//1. 设置共享数据(请求域)
request.setAttribute("msg", msg);
//2. 进行转发操作(/views/admin.html)
this.processTemplate("admin", request, response);
}
}
在 views 目录下创建 admin.html 文件,引入 Thymeleaf 名称空间,通过 Thymeleaf 的表达式从请求域对象中取值,示例代码如下。
<!DOCTYPE html>
<!--在 html 标签内通过属性方式引入 Thymeleaf 名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>admin</title>
</head>
<body>
<h1>admin 页面</h1>
<!--通过 Thymeleaf 的表达式从请求域对象中取值-->
<h2 th:text="${msg}">这是显示后台数据的位置</h2>
</body>
</html>
(6)运行代码,首页的效果如图 8-5 所示。单击此超链接进行页面渲染,渲染后的页面效果如图 8-6 所示。


同时,查看控制台输出如下结果,表示成功访问 ThymeleafTestServlet 类,如图 8-7 所示。

8.4 Thymeleaf 基本语法¶
下面介绍 Thymeleaf 的基本语法,包括表达式语法、域对象的使用,以及如何获取请求参数、分支与迭代等。
8.4.1 表达式语法¶
Thymeleaf 常见的表达式语法,如表 8-2 所示。
表 8-2 表达式语法
| 表达式 | 语法 | 用途 |
| 变量表达式 | ${...} | 获取请求域、session域、对象等值 |
| 选择表达式 | * {...} | 获取上下文对象值 |
| 消息表达式 | # {...} | 获取国际化消息 |
| URL 表达式 | @ {...} | 支持绝对路径和相对路径。其中,相对路径又支持跨上下文调用 url 和协议的引用 |
| 代码块表达式 | ~ {...} | 类似 jsp:include 作用,引入公共页面片段 |
字面量的使用,包括文本值、数字、布尔值、空值和变量等。
运算符包括数学运算符、布尔运算符、比较运算符和条件运算符等,如表 8-3 所示。
表 8-3 运算符
| 运算符 | 说明 |
| 数学运算符 | +、-、*、/、% |
| 布尔运算符 | and、or、!、not |
| 比较运算符 | >、<、>=、<= (gt、lt、ge、le);==、!= (eq、ne) |
| 条件运算符 | if-then: (if)?(then);if-then-else: (if)?(then):(else);Default: (value)?(defaultvalue) |
| 特殊运算符 | 无操作:_ |
8.4.2 Thymeleaf 常见属性¶
Thymeleaf 大部分属性和 HTML 的一样,只是需要在 HTML 的属性前面多加一个 “th” 前缀,不过 “th” 主要用来从后台传值到前端,如果没有取值也可以不用使用。常见属性如下。
常见属性的优先级,如表 8-4 所示。
表 8-4 属性优先级
| 优先级 | 说明 | 属性 |
| 1 | 插入和替换 | th:insertth:replaceth:include |
| 2 | 迭代 | th:each |
| 3 | 条件判断 | th:ifth:unlessth:swithth:case |
| 4 | 对象和变量相关 | th:objectth:with |
| 5 | 确定具体属性的值 | th:valueth:hrefth:src... |
| 6 | 修改文本值 | th:textth:utext |
| 7 | 声明片段 | th:fragment |
| 8 | 删除数据 | th:remove |
需要注意的是,如果想要在页面中使用 Thymeleaf 属性,首先需要声明名称空间。示例代码如下。
<!DOCTYPE html>
<!--在 html 标签中添加 Thymeleaf 名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
......
</html>
我们可以使用 Thymelea 属性对 HTML 网页内容做渲染,我们演示一下渲染标签体内容、标签属性值,以及如何获取上下文路径。代码如下。
<!-- ① 使用 Thymeleaf 修改标签体内容 -->
<p th:text="标签体新值">标签体原始值</p>
<!-- ② 使用 Thymeleaf 修改属性值 -->
<input type="text" name="username" th:value="文本框新值" value="文本框旧值" />
<img th:src="图片新路径" src="图片原路径"/>
<!--
③ 使用 Thymeleaf 解析 url 地址,并携带请求参数
语法:@{/请求路径(key1=value1,key2=value2...)}
上述写法等 Thymeleaf 渲染完毕后依然是:请求路径?key1=value1&key2=value2...
-->
<a th:href="@{/index.html(key1=value1,key2=value2...)}">访问 index.html</a>
8.4.3 域对象的使用¶
域对象是在服务器端中有一定作用域范围的对象,在这个范围内的所有动态资源都能够共享域对象中
保存的数据。常见域对象分为三种:请求域、会话域和应用域。
(1) 请求域。¶
在请求转发的场景下,我们可以借助 HttpServletRequest 对象内部提供的存储空间,携带数据,把数据发送给转发的目标资源。请求域的范围是 HttpServletRequest 对象内部提供的存储空间。请求域对象的数据传递过程如图 8-8 所示。
(2) 会话域。¶
会话域(Session Scope)指的是一次会话,也就是当前服务器端与客户端连接期间。浏览器向服务器端发送第一次请求时,
服务器端会获取一个 Session 对象,并把 sessionid 以 cookie 的形式发送给浏览器,浏览器将 sessionid 保存起来,接下来的每一次请求,浏览器都会将 sessionid 发送到服务器端中去找对应的 Session 对象,因此每次请求使用的都是同一个 Session 对象。只要浏览器不关闭,不管发送多少次请求,获取的都是同一个

Session 对象。会话域的作用范围如图 8-9 所示。
当关闭浏览器或者退出浏览器时,Session 会失效,当前会话也会失效,在第 9 章会详细介绍。
(3) 应用域。¶
应用域(Application Scope)指的是整个项目全局。有效作用于整个服务器端启动期间,关闭浏览器或退出并不会失效,当在服务器端关闭时失效,如图 8-10 所示。


应用域的数据存放在 ServletContext 对象中,而 ServletContext 又被称为 Servlet 上下文是运行环境,其域属性空间是所有 ServletConfig 对象共享的,对象范围大,生命周期长。一般满足以下三个条件的情况下,使用该应用域对象:一是所有用户共享的数据;二是这个共享的数据量很小;三是这个共享的数据很少有修改操作。实际上向应用域中绑定数据,相当于把数据放到缓存(Cache)中,然后用户访问的时候直接从缓存中获取,减少 IO 的操作,能够大大提升系统的性能。
以上三个作用域的作用范围从大到小依次是应用域、会话域、请求域。其中应用域和会话域的区别如下。
应用域具有共享性,因为作用于服务器端期间,且其他用户均能访问,所以多用于聊天室以及留言板等;而 Session 不具有共享性,因为是针对用户的会话,而其他用户访问时会创建一个新的会话,所以用户之间不能互相访问,从而进行保密,多用于购物车的隐藏及一些信息的隐藏。由于暂时不需要会话域和应用域,这里我们暂时只对请求域展开介绍。接下来我们进一步了解在 Thymeleaf 中是如何操作域对象,进行数据传递的。
通常的做法是,在 Servlet 类中将数据存储到域对象中,而在使用了 Thymeleaf 的前端页面中取出域对象中的数据并展示。
首先了解两个概念,请求参数(Parameter)和属性(Attribute)。请求参数是客户端(浏览器)发送请求时携带的数据由用户提供。而属性是由服务器端的组件(Servlet)设置的。请求参数和属性的区别如表 8-5 所示。
表 8-5 请求参数和属性的区别
| 请求参数 | 属性 | |
| 来源 | 请求参数来源于用户提交的 HTTP 请求。以 GET 方法提交的请求来源于 URL;以 POST 提交的请求来源于请求体 | 属性是在服务器端进行设置的,通过ioniAttribute()方法 |
| 操作 | 参数的值只能读取不能修改,读取可以通过 getParameter()方法 | 属性的值既可以读取也可以修改,读取可以通过ioniAttribute()方法,设置通过ioniAttribute()方法,删除通过 removeAttribute()方法 |
| 数据类型 | 字符串类型,不管请求中传递的值的语义是什么,在服务器端获取时都以 String 类型接收,且客户端的参数值只能是简单类型的值,不能是复杂类型,如对象类型 | 任意的继承自 Object 的对象类型 |
请求参数和属性的共同点是两者都被封装在 HttpServletRequest 对象中。
例如,将 name 属性对应值为 zhang3 的数据存储到请求域对象中,在 Servlet 类中的具体操作,如下代码所示。
String requestAttrName = "name";
String requestAttrValue = "zhang3";
request.setAttribute(requestAttrName, requestAttrValue);
request.setAttribute () 方法用来设置 request 域对象的属性,作为响应数据如上述代码,将 name 和 zhang3 设置为 request 对象的一对属性和属性值。
而后,在页面中便可以通过 Thymeleaf 的 “th:text” 从 request 域对象中获取,示例代码如下。
另外,通过 Thymeleaf 表达式获取客户端传递的请求参数,语法格式如下。
例如,根据一个参数名获取一个参数值,示例代码如下。
页面显示效果,如图 8-11 所示。
例如,根据一个参数名获取多个参数值,示例代码如下。
页面显示效果,如图 8-12 所示。

页面显示:tom

页面显示:[German, France]
图 8-11 根据一个参数名获取一个参数值
图 8-12 根据 1 个参数名获取多个参数值
如果想要精确获取某一个值,可以使用数组下标。示例代码如下。
页面显示效果,如图 8-13 所示。

下面通过一个案例来演示请求域对象的使用。
首先,创建 TestViewServlet01 类,继承 ViewBaseServlet 基类,将数据存储在请求域对象 HttpServletResponse 中,并通过 Thymeleaf 将数据渲染到页面中,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebServlet("/testViewServlet01")
public class TestViewServlet01 extends++,ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取到生日的请求参数,计算得出年龄
String birthday = request.getParameter("birthday");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse(birthday, formatter);
//2. 将年龄存储在请求域内
int age=LocalDate.now().getYear()-date.getYear();
request.setAttribute("age",age);
//3. 通过 Thymeleaf 渲染页面
this.processTemplate("info_01",request,response);
}
然后在 index.html 文件中编写代码创建超链接,提供请求参数 name、birthday 和 hobby,单击后跳转 TextViewServlet01 类进行处理,示例代码如下。
在 views 目录下,创建 info_01.html 文件编写代码,用来显示 TextViewServlet01 类处理后的结果,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>info</title>
</head>
<body>
<h1>个人信息</h1>
<!--
姓名和爱好从请求参数中获取
年龄从请求域中获取
-->
姓名:<span th:text="${param.name}"></span><br/>
年龄:<span th:text="${age}"></span><br/>
爱好:<span th:text="${param.hobby[0]}"></span>
<span th:text="${param.hobby[1]}"></span>
</body></html>
运行代码查看页面效果,如图 8-14 所示。单击此超链接,访问 TestViewServlet01,如图 8-15 所示。


8.4.4 内置对象¶
内置对象就是在 Thymeleaf 的表达式中可以直接使用的对象。通过这些内置对象,使得 Thymeleaf 支持直接访问 Servlet Web 原生资源,如 HttpServletRequest、HttpServletResponse、HttpSession、ServletContext 等。Thymeleaf 内置对象可以分为两类,分别是基本内置对象和公共内置对象。
其中基本内置对象包括如下几种。
以上这些内置对象在页面中都可以直接访问,调用方法时也可以直接传入参数。如果不清楚某一内置对象有哪些方法可以使用,可以通过 getClass ().getName () 方法获取该对象的全类名,进而查看此对象包括的所有方法。
例如,调用 “#request” 对象的 getContextPath () 方法,获取当前页面所在应用的名字;调用 getAttribute () 方法读取属性域,示例代码如下。
<h3>表达式的基本内置对象</h3>
<p th:text="${request.getContextPath()}">
调用#request对象的getContextPath()方法
</p>
<p th:text="${request.getAttribute('helloRequestAttr')}">调用#request对象的getAttribute()方法,读取属性域
</p>
而公共内置对象,包括以下几种。

一般情况下,公共内置对象对应的源码位置,如图 8-16 所示。
下面以 List 集合为例,演示 Thymeleaf 内置 #lists 对象的使用。
例如,首先在 Servlet 类中,实现将如下两个 List 集合数据存入 request
请求域,其中一个为空集合,另一个集合包含 “aaa”“bbb” 和 “ccc” 三个元素,示例代码如下。
request.setAttribute("List1", Arrays.asList("aaa", "bbb", "ccc"));
request.setAttribute("anEmptyList", new ArrayList<>());
在 HTML 页面中调用 #list 对象的 isEmpty 方法,分别判断两个集合是否为空,示例代码如下。
<p>#list 对象 isEmpty 方法判断集合整体是否为空,List1:
<span th:text="${#lists.isEmpty(List1)}">
测试#lists
</span>
</p>
<p>#list 对象 isEmpty 方法判断集合整体是否为空,anEmptyList:
<span th:text="${#lists.isEmpty(anEmptyList)}">
测试#lists
</span>
</p>
下面通过一个案例来演示日期、字符串、List 集合等常见内置对象的使用。
首先,创建 TestViewServlet02 类,继承 ViewBaseServlet 基类,将日期、字符串、List 集合等类型的数据存储在请求域对象 HttpServletRequest 中,并通过 Thymeleaf 将数据渲染到页面中,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebServlet("/testViewServlet02")
public class TestViewServlet02 extends GapBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 将日期、字符串、List 集合等对象共享在请求域内
Date date = new Date();
String str = "http://www.atguigu.com";
List list = new ArrayList();
list.add("java");
list.add("javaWeb");
request.setAttribute("date", date);
request.setAttribute("str", str);
request.setAttribute("list", list);
//2. 通过 Thymeleaf 渲染页面
this.processTemplate("info_02", request, response);
}
}
在 index.html 文件中编写代码创建超链接,单击后跳转 TestViewServlet02 类进行处理,示例代码如下。 <a href="testViewServlet02"> 内置对象测试 </a>
在 views 目录下创建 info_02.html 文件编写代码,用来显示 TextViewServlet02 类处理后的结果,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>结果页面</title>
</head>
<body>
<h1>常用内置对象使用</h1>
<!--#dates 对时间进行格式化-->
请求域中的时间:
<span th:text="${#dates.format(date,'yyyy-MM-dd HH:mm:ss')}">
</span>
<br/>
</body>
</html>
请求域中的字符串操作:<br/>
<!--#strings 可以对字符串进行处理,
类似于 javaSE 阶段学习的 String 类的常用方法,此处只演示一小部分
-->
字符串的长度:<span th:text="${#strings.length(str)}"></span><br/>
字符串是否以 http 开头:
<span th:text="${#strings.startsWith(str,'http')}"></span><br/>
字符串是否包含 atguigu:
<span th:text="${#strings.contains(str,'atguigu')}">
</span><br/>
<!-- #lists 对 list 集合进行操作-->
请求域中的 List 集合操作:<br/>
list 集合是否有值:
<span th:text="${#lists.isEmpty(list)}"></span><br/>
list 集合的长度:<span th:text="${#lists.size(list)}"></span><br/>
list 集合是否包含 java:
<span th:text="${#lists.contains(list,'java')}"></span><br/>
</body>
</html>
运行代码查看页面效果,如图 8-17 所示。单击此超链接访问 TestViewServlet02,如图 8-18 所示。


8.4.5 OGNL 语言¶
OGNL 全称为 Object Graph Navigation Language,翻译为对象图导航语言。它是一种功能强大的表达式语言,通过简单一致的表达式语法,可以存取对象的任意属性调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。所谓对象图,即从根对象出发,通过特定的语法逐层访问对象的各种属性,如图 8-19 所示。

OgnlContext 对象是 OGNL 表达式语言的核心,它实现了 java.utils.Map 接口,本质上是一个 Map 结构,且可以使用 Map 方法。
关于 OGNL 表达式语法,这里主要介绍 $符号的用法。$符号主要用来在国际化资源文件中或者配置文件中引用 OGNL 表达式,例如如下文件中所包含的 min 和 max 属于 OGNL 表达式,示例代码如下。
<validators>
<field name="intb">
<field-validator type="int">
<param name="min">10</param>
<param name="max">100</param>
<message>BAction-test 校验:数字必须为${min}为${max}之间!</message>
</field-validator>
</field>
</validators>
在 Thymeleaf 环境下,${} 中的表达式可以从下列元素开始,包括访问属性域的起点、param,以及内置对象。其中访问属性域的起点又包括请求域属性名、session 和 application。param 用来获取请求参数,常见的内置对象有 request、session、lists 和 strings 等。
下面演示借助 OGNL 语言实现将 Student 对象共享到请求域中,并通过 Thymeleaf 将数据渲染到页面。
首先创建 Student、Teacher、Subject、School 等实体类。
Student 类示例代码如下。
package com.atguigu.bean;
import java.util.List;
import java.util.Map;
public class Student {
private String studentName; // 姓名
private Subject subject; // 学科
private List<School> schoolList; // 就读过的学校
private Map<String, Teacher> teacherMap; // 学科老师
public String getStudentName() {
return studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public Subject getSubject() {
return subject;
}
public void setSubject(Subject subject) {
this.subject = subject;
}
public List<School> getSchoolList() {
return schoolList;
}
public void setSchoolList(List<School> schoolList) {
this.schoolList = schoolList;
}
}
public Map<String, Teacher> getTeacherMap() {
return teacherMap;
}
public void setTeacherMap(Map<String, Teacher> teacherMap) {
this.teacherMap = teacherMap;
}
Teacher 类示例代码如下。
package com.atguigu.bean;
public class Teacher {
private String teacherName;
public String getTeacherName() {
return teacherName;
}
public void setTeacherName(String teacherName) {
this.teacherName = teacherName;
}
}
Subject 类示例代码如下。
package com.atguigu.bean;
public class Subject {
private String subjectName;
public String getSubjectName() {
return subjectName;
}
public void setSubjectName(String subjectName) {
this.subjectName = subjectName;
}
}
School 类示例代码如下。
package com.atguigu.bean;
public class School {
private String schoolName;
public String getSchoolName() {
return schoolName;
}
public void setSchoolName(String schoolName) {
this.schoolName = schoolName;
}
}
创建 TestOgnlServlet 类,继承 ViewBaseServlet 基类,将 Student 对象存储在请求域对象 HttpServletRequest 中,并通过 Thymeleaf 将数据渲染到页面中,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebServlet("/testOgnlServlet")
public class TestOgnlServlet extends GapBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 创建一个 Student 对象
Subject subject = new Subject();
subject.setSubjectName("java");
School school01 = new School();
school01.setSchoolName("XX 大学");
School school02 = new School();
school02.setSchoolName("尚硅谷");
List <School> schoolList = new ArrayList<>();
schoolList.add(school01);
schoolList.add(school02);
Teacher teacher01 = new Teacher();
teacher01.setTeacherName("A 老师");
Teacher teacher02 = new Teacher();
teacher02.setTeacherName("B 老师");
Map<String, Teacher > teacherMap = new HashMap<>();
teacherMap.put("teacher01", teacher01);
teacherMap.put("teacher02", teacher02);
Student student = new Student();
student.setStudentName("小尚同学");
student.setSubject(subject);
student.setSchoolList(schoolList);
student.setTeacherMap(teacherMap);
//2. 将 Student 对象共享到请求域内
request.setAttribute("student", student);
//3. 通过 Thymeleaf 渲染页面
this.processTemplate("info_03", request, response);
}
在 index.html 文件中编写代码,创建超链接,单击后跳转 TestOgnlServlet 类进行处理,示例代码如下。 <a href="testOgnlServlet">OGNL 测试 </a>
在 views 目录下,创建 info_03.html 文件,编写代码,用来显示 TestOgnlServlet 类处理后的结果,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>结果页面</title>
</head>
</head>
<body>
<h1>OGNL 语法</h1>
学生姓名:<span th:text="${student.studentName}"></span><br/>
学科名字:<span th:text="${student.subject.subjectName}"></span><br/>
<!--已知两个学校的前提下,后面如果不知道个数,采用迭代操作即可-->
学校名字:
<span th:text="${student.schoolList[0].schoolName}"></span>
<span th:text="${student.schoolList[1].schoolName}">
</span><br/>
老师姓名:
<span th:text="${student.teacherMap.teacher01.teacherName}">
</span>
<span th:text="${student.teacherMap.teacher02.teacherName}">
</span><br/>
</body>
</html>
运行代码查看页面效果,如图 8-20 所示。然后单击此超链接,访问 TestOgnlServlet,如图 8-21 所示。


8.4.6 分支与迭代¶
前面介绍了 th:if 和 th:unless 用于条件判断,根据条件决定对应内容是否需要显示,th:switch 和 th:case 属于选择语句,而 th:each 用来循环迭代。接下来演示其具体的应用。
首先,创建 Employee 实体类,示例代码如下。
package com.atguigu.bean;
public class Employee {
private Integer id;
private String name;
private Double salary;
public Employee() {
}
public Employee(Integer id, String name, Double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
创建 EmployeeServlet 类,实现将 Employee 对象数据存储至 ArrayList 集合中,然后将其存储至 request 请求域中。示例代码如下。
// 1. 创建 ArrayList 对象并填充
List<Employee> employeeList = new ArrayList<>();
employeeList.add(new Employee(1, "tom", 500.00));
employeeList.add(new Employee(2, "jerry", 600.00));
employeeList.add(new Employee(3, "harry", 700.00));
// 2. 将集合数据存入请求域
request.setAttribute("employeeList", employeeList);
// 3. 调用父类方法渲染视图
super.processTemplate("list", request, response);
}
然后在 views 下创建 list.html 前端页面,在页面中借助 th:if 和 th:unless 来显示数据,示例代码如下。
<table>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
</tr>
<tr th:if="${#lists.isEmpty(employeeList)}">
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
<tr th:if="${not #lists.isEmpty(employeeList)}">
<td colspan="3">有数据!</td>
</tr>
<!-- 或者 -->
<tr th:unless="${#lists.isEmpty(employeeList)}">
<td colspan="3">有数据!</td>
其中,if 搭配 not 关键词使用,和 unless 表达式的效果是一样的,两者皆可用。此外,还可以使用 switch 搭配 case 语句进行选择。
<h3>测试 switch</h3>
<div th:switch="${#lists.size(employeeList)}">
<p th:case="1">一个和尚挑水喝</p>
<p th:case="2">两个和尚抬水喝</p>
<p th:case="3">三个和尚没水喝</p>
</div>
使用 th:each 遍历时,th:each 所在标签满足每次遍历出来一条数据就会添加一次该标签,语法格式如下。
th:each="遍历出来的数据,status:要遍历的数据"
其中,status 表示遍历的状态,它包含 index 和 count 属性。index 表示遍历出来的每个元素的下标;count 表示遍历出来的每个元素的计数。
例如,遍历显示请求域中的 employeeList 集合的值,示例代码如下。
<!--遍历显示请求域中的 teacherList-->
<table border="1" cellspacing="0" width="500">
<tr>
<th>编号</th>
<th>姓名</th>
<th>工资</th>
</tr>
<tbody th:if="${#lists.isEmpty(employeeList)}">
<tr>
<td colspan="2">员工的集合是空的!!!</td>
</tr>
</tbody>
<!--集合不为空,遍历展示数据-->
<tbody th:unless="${#lists.isEmpty(employeeList)}">
<tr th:each="employee,status : ${employeeList}">
<td th:text="${status.count}">这里显示编号</td>
<td th:text="${employee.name}">这里显示员工的名字</td>
<td th:text="${employee.salary}">这里显示员工的工资</td>
</tr>
</tbody>
</table>
8.4.7 模板文件¶
对于不同页面包含的一些重复的代码片段,我们可以抽取各个页面的公共部分作为模板文件,需要的时候直接调用即可,既减少了代码冗余,又提高了开发效率。
Thymeleaf 提供了 “th:fragment” 属性,给抽取的公共代码片段命名。示例代码如下,创建页面的公共代码片段 header。
在需要的页面中引入公共代码片段时,一般需要搭配 th:insert、th:replace 或者 th:include 属性。三者的作用及特点如表 8-6 所示。
表 8-6 th:insert、th:replace 或者 th:include 的作用及特点
| 作用 | 特点 | |
| th:insert | 把目标的代码片段整个插入到当前标签内部 | 保留页面原有的标签 |
| th:replace | 用目标的代码替换当前标签 | 不会保留页面原有的标签 |
| th:include | 把目标的代码片段去除最外层标签,然后再插入到当前标签内部 | 去掉片段外层标签,同时保留页面原有标签 |
th:insert、th:replace 和 th:include 三者的用法如下。
<div id="badBoy" th:insert="segment :: header">
div 标签的原始内容
</div>
<div id="worseBoy" th:replace="segment :: header">
div 标签的原始内容
</div>
<div id="worstBoy" th:include="segment :: header">
div 标签的原始内容
</div>
其中,以 “th:insert="segment :: header"” 为例,segment 表示代码片段所在页面的逻辑视图,header 表示公共代码片段的名称。
下面通过一个案例来演示模板文件的使用。
例如,现在有 A、B 两个页面,其中两个页面包含同样的头部信息,那么我们可以把这部分同样的信息封装到另一个页面 common.html 作为模板文件,然后分别在 A、B 中引入该文件,从而减少代码冗余。
首先,在 views 目录下,分别创建 common.html、a.html 和 b.html 文件。
common.html 文件示例代码如下,通过 th:fragment 属性为抽取的公共部分命名为 “headHtml”。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--为抽取出来的 html 内容自定义一个名字-->
<div id="commonHead" th:fragment="headHtml">
头部信息
</div>
</body>
</html>
A 页面对应 a.html 文件,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>A页面</title>
</head>
<body>
<!--引入head.html中的头部信息内容-->
<!--1.将内容进行包含-->
<div id="myHead01" th:include="common::headHtml"></div>
<!--2.将整体内容进行替换-->
<div id="myHead02" th:replace="common::headHtml"></div>
<!--3.将整体内容进行插入添加-->
<div id="myHead03" th:insert="common::headHtml"></div>
<div>A页面的主题内容</div>
</body>
</html>
B 页面对应 b.html 文件,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>B页面</title>
</head>
<body>
<!--引入head.html中的头部信息内容-->
<!--1.将内容进行包含-->
<div id="myHead01" th:include="common::headHtml"></div>
<!--2.将整体内容进行替换-->
<div id="myHead02" th:replace="common::headHtml"></div>
<!--3.将整体内容进行插入添加-->
<div id="myHead03" th:insert="common::headHtml"></div>
<div>B页面的主题内容</div>
</body>
</html>
A 和 B 页面中分别演示了 th:insert、th:replace 和 th:include 属性的使用。
创建 ToAPageServlet 类,继承 ViewBaseServlet 基类,实现从首页跳转 A 页面,以及 Thymeleaf 渲染该页面,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebServlet("/toAPageServlet")
public class ToAPageServlet extends++,ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processTemplate("a",request,response);
}
}
创建 ToBPageServlet 类,继承 ViewBaseServlet 基类,实现从首页跳转 B 页面,以及 Thymeleaf 渲染该页面,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebServlet("/toBPageServlet")
public class ToBPageServlet extends++,ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processTemplate("b",request,response);
}
}
在 index.html 文件中编写代码,创建超链接,单击后分别跳转 ToAPageServlet、ToBPageServlet 类进行
处理,示例代码如下。
运行代码查看页面效果,如图 8-22 所示。

分别单击两个超链接,如图 8-23 和 8-24 所示。A 页面和 B 页面包含同样的头部信息,表明两页面成功引入公共代码片段。


以 A 页面为例,打开 F12 页面,我们可以清晰地看到 th:insert、th:replace 和 th:include 属性的不同用法,如图 8-25 所示。

8.5 案例:水果库存后台管理系统的实现¶
下面借助 Thymeleaf 相关知识,完成水果库存后台管理系统的实现,主要包括对于库存信息的修改、添加、删除,以及查询等操作。
8.5.1 展示所有库存信息¶
首先创建 fruitdb 数据库,并创建水果库存表 t_fruit,提前录入水果信息。该表信息可以通过前言提示获取。
t_fruit 表的结构如表 8-7 所示。
表 8-7 t_fruit 表的结构
| 字段名称 | 数据类型 | Key | |
| 编号 | fid | INT | PRI |
| 名称 | fname | VARCHAR(20) | |
| 价格 | price | INT | |
| 数量 | fcount | INT | |
| 描述 | remark | VARCHAR(50) |
下面创建 chapter08_fruit 模块,引入 Thymeleaf 依赖包,前端页面等静态资源,以及公共的工具类 BaseDAO、ViewBaseServlet。同样可以直接获取该资料。
创建与表 t_fruit 字段一一对应的实体类 Fruit,示例代码如下。
package com.atguigu.fruit.bean;
public class Fruit {
private Integer fid; // 水果 id
private String fname; // 名称
private Integer price; // 价格
private Integer fcount; // 数量
private String remark; // 描述
// 省略部分代码
}
分别创建 Dao 层、Service 层,实现 Service 调用 Dao,Dao 再调用数据库查询数据。
Dao 层包括 FruitDAO 接口和该接口的实现类 FruitDAOImpl,示例代码如下。
package com.atguigu.fruit.dao;
//省略 import 语句
public class FruitDAO {
List<Fruit> getFruitList();
Fruit getFruitByFid(Integer fid);
}
package com.atguigu.fruit.dao.impl;
//省略 import 语句
public class FruitDAOImpl extends BaseDao<Fruit> implements FruitDAO {
//获取所有数据
@Override
public List<Fruit> getFruitList() {
return super.getList("select * from t_fruit");
}
//根据 id 获取数据
@Override
public Fruit getFruitByFid(Integer fid) {
return super.getBean("select * from t_fruit where fid = ? ", fid);
}
}
Service 层包括 FruitService 接口和该接口实现类 FruitServiceImpl,示例代码如下。
public class FruitServiceImpl {
List<Fruit> getFruitList();
Fruit getFruitByFid(Integer fid);
}
package com.atguigu.fruit.service.impl;
//省略 import 语句
public class FruitServiceImpl implements FruitService {
private FruitDAO fruitDAO = new FruitDAOImpl();
@Override
public List<Fruit> getFruitList() {
return fruitDAO.getFruitList();
}
@Override
public Fruit getFruitByFid(Integer fid) {
return fruitDAO.getFruitByFid(fid);
}
}
然后创建 IndexServlet,实现将数据展示到页面,示例代码如下。
package com.atguigu.fruit.servlets;
//省略 import 语句
Ross
@Servlet("/index.html")
public class IndexServlet extends MyBaseServlet {
private FruitService fruitService=new FruitServiceImpl();
@Override
public void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException {
//获取所有水果信息
List<Fruit> fruitList = fruitService.getFruitList();
System.out.println("fruitList = " + fruitList);
//将水果信息保存至请求域
request.setAttribute("fruitList",fruitList);
//跳转页面
this.processTemplate("index",request,response);
}
@Override
protected void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req,resp);
}
}
修改 index.html 页面,从请求域中获取数据,借助 Thymeleaf 渲染到页面,示例代码如下。
</head>
<body>
<div id="div_container">
<div id="div_fruit_list">
<p class="center f30">欢迎使用水果库存后台管理系统</p>
<table id="tbl_fruit">
<tr>
<th class="w20">名称1</th>
<th class="w20">单价</th>
<th class="w20">库存</th>
<th>操作</th>
</tr>
<tr th:if="${#lists.isEmpty(fruitList)}">
<td colspan="4">对不起,库存为空!</td>
</tr>
<tr th:unless="${#lists.isEmpty(fruitList)}"
th:each="fruit : ${fruitList}">
<td>th:text="${fruit.fname}">苹果</td>
<td th:text="${fruit.price}">5</td>
<td th:text="${fruit.fcount}">20</td>
<td><img th:src="@{/imgs/del.jpg}"/></td>
</tr>
</table>
</div>
</div>
</body>
</html>
启动项目查看页面效果,如图 8-26 所示。

8.5.2 编辑和修改特定库存信息¶
展示全部水果库存信息后,接下来实现对特定水果信息的编辑与修改。
修改 index.html 页面,在水果名称栏添加超链接,实现单击水果名称跳转至编辑修改页面。
edit.html 页面示例代码如下,根据水果 ID 将对应水果信息回显,然后修改后提交表单,保存新数据至数据库。
<body>
<div id="div_container">
<div id="div_fruit_list">
<p class="center f30">编辑库存信息 3</p>
<form th:action="@{/update}" method="post" th:object="${fruit}">
<!-- 隐藏域:功能类似于文本框,它的值会随着表单的提交发送给服务器端,但是界面上用户看不到 -->
<input type="hidden" name="fid" th:value="*{fid}"/>
<table id="tbl_fruit">
<tr>
<th class="w20">名称:</th>
<td><input type="text" name="fname" th:value="*{fname}"/></td>
</tr>
<tr>
<th class="w20">单价:</th>
<td><input type="text" name="price" th:value="*{price}"/></td>
</tr>
<tr>
<th class="w20">库存:</th>
<td><input type="text" name="fcount" th:value="*{fcount}"/></td>
</tr>
<tr>
<th class="w20">备注:</th>
<td><input type="text" name="remark" th:value="*{remark}"/></td>
</tr>
<tr>
<th colspan="2">
<input type="submit" value="修改" />
</th>
</tr>
</table>
</form>
</div>
</body>
</html>
修改 Dao 层和 Service 层添加修改数据库信息的代码,FruitDAOImpl 示例代码如下。
package com.atguigu.dao.impl;
// 省略 import 语句
public class FruitDAOImpl extends BaseDao<Fruit> implements FruitDAO {
//根据 id 修改水果信息
@Override
public void updateFruit(Fruit fruit) {
String sql = "update t_fruit set fname = ?, price = ?, fcount = ?, remark = ? where fid = ?";
super.update(sql, fruit.getFname(), fruit.getPrice(),
fruit.getFcount(), fruit.getRemark(), fruit.getFid());
}
}
FruitServiceImpl 类的示例代码如下。
package com.atguigu.service.impl; // 省略 import 语句
public class FruitServiceImpl implements FruitService {
创建 ToEditPageServlet 类实现携带水果 id,跳转修改页面功能,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@Servlet("/edit.html")
public class ToEditPageServlet extends++,ViewBaseServlet {
private FruitService fruitService=new FruitServiceImpl();
@Override
public void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException,ServletException {
//获取水果 id
String fidStr = request.getParameter("fid");
if (StringUtils.isEmpty(fidStr)) {
int fid = Integer.parseInt(fidStr);
//根据 id 获取水果信息
Fruit fruit = fruitService.getFruitByFid(fid);
request.setAttribute("fruit",fruit);
//跳转页面
this.processTemplate("edit",request,response);
}
}
}
创建 UpdateServlet 类获取新的参数,将其保存至数据库,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@Servlet("/update")
public class UpdateServlet extends MyBaseServlet {
private FruitService fruitService=new FruitServiceImpl();
@Override
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
//1.设置编码
request.setCharacterEncoding("utf-8");
//2.获取参数
String fidStr = request.getParameter("fid");
Integer fid = Integer.parseInt(fidStr);
String fname = request.getParameter("fname");
String-priced = request.getParameter("price");
int price = Integer.parseInt(priceStr);
String fcountStr = request.getParameter("fcount");
Integer fcount = Integer.parseInt(fcountStr);
String remark = request.getParameter("remark");
//3.执行更新
fruitService.updateFruit(new Fruit(fid,fname, price, fcount, remark));
//4.资源跳转
response.sendRedirect("index.html");
}
启动项目,在首页中单击第一行 “红富士”,发现跳转至编辑页面,如图 8-27 所示。然后修改单价为 10 元,库存为 100 个,单击 “修改”,如图 8-28 所示,修改成功。


8.5.3 删除库存信息¶
继续实现单击首页操作栏图标,删除对应水果信息。
修改 index.html 页面,在操作栏图标处绑定单击事件,示例代码如下。
<td>
<a th:href="@{/del.do(fid=${fruit.fid})}" onclick="delFruit()">
<img src="imgs/del.jpg" class="delImg" />
</a>
</td>
创建 index.js 文件,单击图标后弹出提示框,携带 id 参数发送请求,示例代码如下。
在 index.html 文件中引入 index.js 文件。
修改 Dao 层和 Service 层,添加修改数据库信息的代码,FruitDAOImpl 示例代码如下。
package com.atguigu.dao.impl;
//省略 import 语句
public class FruitDAOImpl extends BaseDao<Fruit> implements FruitDAO {
//根据 id 删除水果信息
public void delFruit(Integer fid) {
super.update("delete from t_fruit where fid = ? ", fid);
}
}
FruitServiceImpl 类的示例代码如下。
package com.atguigu.service.impl;
//省略 import 语句
public class FruitServiceImpl implements FruitService {
//根据 id 删除水果信息
@Override
public void delFruit(Integer fid) {
fruitDAO.delFruit(fid);
}
}
创建 DelServlet 类,实现根据 id 删除水果信息,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@Servlet("/del")
public class DelServlet extends++,ViewBaseServlet {
private FruitService fruitService=new FruitServiceImpl();
@Override
public void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException {
//获取水果 id
String fidStr = request.getParameter("fid");
if (StringUtil.isEmpty(fidStr)) {
int fid = Integer.parseInt(fidStr);
//根据 id 删除水果
fruitService.delFruit(fid);
//重定向到首页
response.sendRedirect("index.html");
}
}
}
启动项目,然后单击第一行 “红富士” 对应的删除图标,如图 8-29 所示。
单击 “确定” 按钮后再次查看页面,如图 8-30 所示。


| 名称 | 单价(元) | 库存(个) | 操作 |
| 大瓜 | 5 | 66 | × |
| 南瓜 | 4 | 48 | × |
| 苦瓜 | 3 | 50 | × |
| 鸭蓝 | 6 | 200 | × |
| 柚子 | 6 | 38 | × |
| 橙子 | 10 | 80 | × |

从图 8-30 中可知,“红富士” 信息被删除。
8.5.4 添加库存信息¶
最后来实现如何添加水果信息。
首先在首页,表单的右上方添加超链接,单击跳转添加水果库存页面。
<div style="border:0px solid red;width:60%;margin-left:20%;text-align:right;"> <a th:href="@{/add.html}" style="border:0px solid blue;margin-bottom:4px;">添加新库存记录</a> </div>
添加水果库存页面 add.html,示例代码如下。
<html xmlns:th="http://www.thymeleaf.org">
<head><!--省略部分代码--></head>
<body>
<div id="div_container">
<div id="div_fruit_list">
<p class="center f30">新增库存信息</p>
<form th:action="@{/add}" method="post">
<table id="tbl_fruit">
<tr>
<th class="w20">名称:</th>
<td><input type="text" name="fname" /></td>
</tr>
<tr>
<th class="w20">单价:</th>
<td><input type="text" name="price" /></td>
</tr>
<tr>
<th class="w20">库存:</th>
<td><input type="text" name="fcount" /></td>
</tr>
<tr>
<th class="w20">备注:</th>
<td><input type="text" name="remark" /></td>
</tr>
<tr>
<th colspan="2">
<input type="submit" value="添加" />
</th>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>
修改 Dao 层和 Service 层,添加修改数据库信息的代码,FruitDAOImpl 示例代码如下。
package com.atguigu.dao.impl;
//省略 import 语句
public class FruitDAOImpl extends BaseDao<Fruit> implements FruitDAO {
//添加新的水果信息
@Override
public void addFruit(Fruit fruit) {
String sql = "insert into t_fruit values(0,?,?,?,?)";
super.update(sql, fruit.getFname(), fruit.getPrice(), fruit.getFcount(), fruit.getRemark());
}
FruitServiceImpl 类的示例代码如下。
package com.atguigu.service.impl;
//省略 import 语句
public class FruitServiceImpl implements FruitService {
//调用 Dao 层添加数据
@Override
public void addFruit(Fruit fruit) {
fruitDAO.addFruit(fruit);
}
}
创建 ToAddPageServlet,跳转 add.html 页面,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@WebServlet(name = "ToAddPageServlet", value = "/add.html")
public class ToAddPageServlet extends++,viewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//跳转页面
this.processTemplate("add",request,response);
}
}
创建 AddServlet,获取参数,并将其添加至数据库,示例代码如下。
package com.atguigu.servlet;
//省略 import 语句
@Servlet("/add")
public class AddServlet extends++,ViewBaseServlet {
private FruitService fruitService=new FruitServiceImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置编码
request.setCharacterEncoding("UTF-8");
//获取参数
String fname = request.getParameter("fname");
Integer price = Integer.parseInt(request.getParameter("price"));
Integer fcount = Integer.parseInt(request.getParameter("fcount"));
String remark = request.getParameter("remark");
//保存数据至 fruit 对象
Fruit fruit = new Fruit(0,fname, price, fcount, remark);
//调用 service 层
fruitService.addFruit(fruit);
//跳转首页
response.sendRedirect("index.html");
}
启动项目,如图 8-31 所示。
单击 “添加新库存记录”,跳转添加数据页面,如图 8-32 所示。然后输入信息,单击 “添加” 后再次查看首页,如图 8-33 所示,添加成功。

图 8-33 再次添加 “红富士”
| 名称 | 单价(元) | 库存(个) | 操作 |
| 大瓜 | 5 | 66 | × |
| 南瓜 | 4 | 48 | × |
| 苦瓜 | 3 | 50 | × |
| 鸭梨 | 6 | 200 | × |
| 柚子 | 6 | 38 | × |
| 橙子 | 10 | 80 | × |
| 红富士 | 10 | 20 | × |
8.6 本章小结¶
本章主要介绍 Thymeleaf 页面渲染技术,了解 MVC 和三层架构的概念,从宏观上把握 Thymeleaf 所处位置。Thymeleaf 对视图层进行了封装,在静态页面上渲染显示动态数据,简化视图层的操作,降低组件间耦合度,便于维护。本章介绍了 Thymeleaf 基本语法,包括表达式语法、域对象的使用,以及如何获取请求参数、分支与迭代等。最后通过案例综合应用 Thymeleaf 相关知识,带领大家动手操作,加深理解。