跳转至

第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 所示。

e0b440bee84a395e3c1388f4e3fe7b069154a466aaf528cbf72dfc2dd5461921.jpg

MVC 的处理过程如下,首先控制器接受用户的请求,并决定应该调用哪个模型来进行处理,然后模型用业务逻辑来处理用户的请求并返回数据,最后控制器用相应的视图对该模型返回的数据进行格式化,并通过表示层呈现给用户。

使用 MVC 模型,能够将视图分离出来,进一步实现各个组件之间的解耦,让各个组件可以单独维护,同时让后端工程师和前端工程师的对接变得更方便。

MVC 可以是三层框架中的一个表现层框架,属于表现层。也就是说,MVC 把三层架构中的表现层再度进行了分化,分成了 Controller、View、Model 三部分,只不过实体的应用贯穿整个三层架构。三层架构

74bf819b6c0a36132b82eca8d7f3d395cddf362acf424ecb2f19784be606b737.jpg

和 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 所示。

381af86c3eb57775493052cdede46937572d34be72a3da4d8f6b0199f568b1e5.jpg

如果我们把所有的 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 值,必须根据实际路径确定。

1217baeae1f858c12ee5d29ad3fd129b607ce608702c2703fbbd38e32282608c.jpg

<?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());
    }
}
templateEngine.process(templateName, webContext, resp.getWriter());
} 

上述代码中 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 所示。

b15a70fa7b161604bf26baa2143fbfcb406d03e49be2df36e8ebf167c082d943.jpg

d4d61a2c1cd8256d5a02249305b3a2d569ba8d1115357fd55bc0606901afce4f.jpg

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

886a9306d92513cc8229e6f6f9ae6e67c77b6142b374cd0d101df3f19caa2a22.jpg

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 对象。只要浏览器不关闭,不管发送多少次请求,获取的都是同一个

582d9d4d9111141333fa357a40c4f5bfd74be3424028e91149ead600cf587bb9.jpg

Session 对象。会话域的作用范围如图 8-9 所示。

当关闭浏览器或者退出浏览器时,Session 会失效,当前会话也会失效,在第 9 章会详细介绍。

(3) 应用域。

应用域(Application Scope)指的是整个项目全局。有效作用于整个服务器端启动期间,关闭浏览器或退出并不会失效,当在服务器端关闭时失效,如图 8-10 所示。

4bc2a0fb46ecadb3e1afad80260bbe9390bcb42c58a9d43ed7314fb2e4736f50.jpg

7bb0a71e54db54bed42d5219fc0d9e57188d75a1dc3bf94a21e8e3620de90710.jpg

应用域的数据存放在 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 域对象中获取,示例代码如下。

<p th:text="${name}">request field value</p> 

另外,通过 Thymeleaf 表达式获取客户端传递的请求参数,语法格式如下。

${param.参数名} 

例如,根据一个参数名获取一个参数值,示例代码如下。

<p th:text="${param.username}">这里替换为请求参数的值</p>

页面显示效果,如图 8-11 所示。

例如,根据一个参数名获取多个参数值,示例代码如下。

<p th:text="${param.team}">这里替换为请求参数的值</p> 

页面显示效果,如图 8-12 所示。

2697a9ba92eb1d9bc5f104252b4e5824fa409177e3cfdab0bb01dbb25e46b22b.jpg

页面显示:tom

bbaf446594bf190a659fbcf14c8ca9c382932a5712668c06a6da7953f47a18a2.jpg

页面显示:[German, France]

图 8-11 根据一个参数名获取一个参数值

图 8-12 根据 1 个参数名获取多个参数值

如果想要精确获取某一个值,可以使用数组下标。示例代码如下。

<p th:text="${param.team[0]}">这里替换为请求参数的值</p>
<p th:text="${param.team[1]}">这里替换为请求参数的值</p> 

页面显示效果,如图 8-13 所示。

5503984eca3cb285ad6b673aabe7f12a0a1ae7371e657b06c86b3aad20e9eed2.jpg

下面通过一个案例来演示请求域对象的使用。

首先,创建 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 类进行处理,示例代码如下。

<a href="testViewServlet01
    ?name=jack&birthday=2000-06-06&hobby=java&hobby=game">请求域对象测试</a> 

在 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 所示。

ef1d9a4de27c9798e9baa785cd608cfb199f0e6e62936f15ad5e4e8a567b0ed6.jpg

55049384590995dc2305ece637bdb429d958aaa74fad23d020bde59c0222fe4d.jpg

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> 

而公共内置对象,包括以下几种。

a32fa09a7ffed13fb3386804d019ca461f84992098df212d2d7bbbaef62d7f0d.jpg

一般情况下,公共内置对象对应的源码位置,如图 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 所示。

b76e7fa9a37095f955db1f3514c85616960c9cbdfef3206378b382f04c5fccd0.jpg

bbb287b1ca9bb9532d509c55fd08107e1b604d59bbd1de382940a9bd215edadb.jpg

8.4.5 OGNL 语言

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

b2415145429f413277f7b4a0bd11e8aa406188dcf793e230c3254750d3a44472.jpg

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 所示。

07e6f971d79f197b7bf1496c427d91b64ff6cc103f1ef6c77c197caa2b71c249.jpg

1900a08b7ba9c9357c8c35d44be310bb4964d5a2c1ad091c95969d955f9d3c7c.jpg

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> 
</tr>
</table> 

其中,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。

<div th:fragment="header">
    <p>被抽取出来的头部内容</p>
</div>

在需要的页面中引入公共代码片段时,一般需要搭配 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 类进行

处理,示例代码如下。

<a href="toAPageServlet">去A页面</a>
<a href="toBPageServlet">去B页面</a> 

运行代码查看页面效果,如图 8-22 所示。

d3ea91334f5129f45f5adfc9d3e72eacd8dc22dec8866badee48b518f1c73405.jpg

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

509f42f58c021cc0502eba471801153ccb4980a8b299bfc692e32b2215948ca9.jpg

e99daa09ff629277c84ff7113840553b0e4940be9c1f39946252139a71b58da4.jpg

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

bc576e6e0f548211f781d11bada6de4c24f35c67dc9f153e829fa1bfc188251c.jpg

8.5 案例:水果库存后台管理系统的实现

下面借助 Thymeleaf 相关知识,完成水果库存后台管理系统的实现,主要包括对于库存信息的修改、添加、删除,以及查询等操作。

8.5.1 展示所有库存信息

首先创建 fruitdb 数据库,并创建水果库存表 t_fruit,提前录入水果信息。该表信息可以通过前言提示获取。

t_fruit 表的结构如表 8-7 所示。

表 8-7 t_fruit 表的结构

字段名称数据类型Key
编号fidINTPRI
名称fnameVARCHAR(20)
价格priceINT
数量fcountINT
描述remarkVARCHAR(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,示例代码如下。

package com.atguigu.fruit.service; //省略 import 语句 
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 渲染到页面,示例代码如下。

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <!--省略部分代码-->
</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 所示。

27d6587bc12a171f602ab3d88521d92f2cbe675990b29fb6de9e8886c5d668b5.jpg

8.5.2 编辑和修改特定库存信息

展示全部水果库存信息后,接下来实现对特定水果信息的编辑与修改。

修改 index.html 页面,在水果名称栏添加超链接,实现单击水果名称跳转至编辑修改页面。

<td>
    <a th:text="${fruit.fname}" th:href="@{/edit.html(fid=${fruit.fid})}">
    苹果
</a>
</td> 

edit.html 页面示例代码如下,根据水果 ID 将对应水果信息回显,然后修改后提交表单,保存新数据至数据库。

<html xmlns:th="http://www.thymeleaf.org">
<head><!--省略部分代码--></head>
<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 {

//调用 Dao 层修改数据库信息
@Override
public void updateFruit(Fruit fruit) {
    fruitDAO.updateFruit(fruit);
}

创建 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 所示,修改成功。

9c2a6ec7a60cb1a1069df8bccf54eb6686b4a56aafee017c1bf999461c00177d.jpg

2851cb706c796ba15ca771d87bf7c70373bbc68479bf4774e439843081109e7b.jpg

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 参数发送请求,示例代码如下。

function delFruit() {
    if (!confirm(
    '是否确认删除?') ) {
    event.preventDefault();
    }
}

在 index.html 文件中引入 index.js 文件。

<head>
    <script language="JavaScript" th:src="@{/js/index.js}"></script>
</head> 

修改 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 所示。

7825b903e03291e48098a77e675c36bcf82fcac9bcebc421ca944ee9d9dd18a5.jpg

12c0ae2f35c32b7aa517c5b14c68bd29741375d9f84f74041cf45f06c5b2ad12.jpg

名称单价(元)库存(个)操作
大瓜566×
南瓜448×
苦瓜350×
鸭蓝6200×
柚子638×
橙子1080×

702711d51805c2efd569bf60de8680848a1744b0d44c605de6d0aafc530ca08d.jpg

从图 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 所示,添加成功。

77f1551c3ccd821f5ecf9ba9e6ed2dc54e30634aaf7c275df52d2d65eda04888.jpg

图 8-33 再次添加 “红富士”

名称单价(元)库存(个)操作
大瓜566×
南瓜448×
苦瓜350×
鸭梨6200×
柚子638×
橙子1080×
红富士1020×

8.6 本章小结

本章主要介绍 Thymeleaf 页面渲染技术,了解 MVC 和三层架构的概念,从宏观上把握 Thymeleaf 所处位置。Thymeleaf 对视图层进行了封装,在静态页面上渲染显示动态数据,简化视图层的操作,降低组件间耦合度,便于维护。本章介绍了 Thymeleaf 基本语法,包括表达式语法、域对象的使用,以及如何获取请求参数、分支与迭代等。最后通过案例综合应用 Thymeleaf 相关知识,带领大家动手操作,加深理解。