第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 渲染的前端页面,示例代码如下。
其中,username 是被放在请求域中的数据,通过上述代码的方式,可以将该属性对应的具体值渲染到页面中。
8.2.2 物理视图和逻辑视图¶
在 Servlet 中,将请求转发到一个 HTML 页面文件时,使用完整的转发路径,被称为物理视图。例如,对于该项目的 login_success.html 页面,对应的物理视图为 “/pages/user/login_success.html”,项目的目录结构如图 8-3 所示。

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

需要注意的是,Thymeleaf 只能渲染视图前缀和视图后缀中间的网页文件。不符合视图前缀和视图后缀的其他路径都无法被渲染。
(3)创建 Servlet 基类,命名为 KuwaitBaseServlet 示例代码如下。
该类暂时不用手动编写,直接复制过来使用即可,后续使用框架编程后,Servlet 类将被完全取代。
上述代码中 processTemplate () 方法的功能,就是对静态页面进行动态数据渲染。包含了三个参数,其中 templateName 表示逻辑视图;req 为请求域对象,携带请求数据;resp 为响应体对象,携带服务器端要返回给浏览器的内容。原理是先通过模板引擎渲染数据,然后转发到指定页面,由于采用转发方式,所以请求域中的共享数据也是可以传递到客户端的。
(4)编写 index.html 文件创建超链接,单击访问 ThymeleafTestServlet 类,示例代码如下。
(5)创建 ThymeleafTestServlet 类,继承 KuwaitBaseServlet 类,然后调用其 init () 方法进行初始化,调用 processTemplate () 方法实现页面渲染及转发操作。同样使用 @WebServlet 注解将该类注入容器,且映射路径和 index.html 文件中保持一致,为 “/thymeleafTest”,示例代码如下。
在 views 目录下创建 admin.html 文件,引入 Thymeleaf 名称空间,通过 Thymeleaf 的表达式从请求域对象中取值,示例代码如下。
(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 属性,首先需要声明名称空间。示例代码如下。
我们可以使用 Thymelea 属性对 HTML 网页内容做渲染,我们演示一下渲染标签体内容、标签属性值,以及如何获取上下文路径。代码如下。
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 类中的具体操作,如下代码所示。
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 将数据渲染到页面中,示例代码如下。
然后在 index.html 文件中编写代码创建超链接,提供请求参数 name、birthday 和 hobby,单击后跳转 TextViewServlet01 类进行处理,示例代码如下。
在 views 目录下,创建 info_01.html 文件编写代码,用来显示 TextViewServlet01 类处理后的结果,示例代码如下。
运行代码查看页面效果,如图 8-14 所示。单击此超链接,访问 TestViewServlet01,如图 8-15 所示。


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

一般情况下,公共内置对象对应的源码位置,如图 8-16 所示。
下面以 List 集合为例,演示 Thymeleaf 内置 #lists 对象的使用。
例如,首先在 Servlet 类中,实现将如下两个 List 集合数据存入 request
请求域,其中一个为空集合,另一个集合包含 “aaa”“bbb” 和 “ccc” 三个元素,示例代码如下。
在 HTML 页面中调用 #list 对象的 isEmpty 方法,分别判断两个集合是否为空,示例代码如下。
#list 对象 isEmpty 方法判断集合整体是否为空,List1: 下面通过一个案例来演示日期、字符串、List 集合等常见内置对象的使用。 首先,创建 TestViewServlet02 类,继承 ViewBaseServlet 基类,将日期、字符串、List 集合等类型的数据存储在请求域对象 HttpServletRequest 中,并通过 Thymeleaf 将数据渲染到页面中,示例代码如下。 在 index.html 文件中编写代码创建超链接,单击后跳转 TestViewServlet02 类进行处理,示例代码如下。 内置对象测试 在 views 目录下创建 info\_02.html 文件编写代码,用来显示 TextViewServlet02 类处理后的结果,示例代码如下。 运行代码查看页面效果,如图 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 表达式,示例代码如下。 在 Thymeleaf 环境下,\${} 中的表达式可以从下列元素开始,包括访问属性域的起点、param,以及内置对象。其中访问属性域的起点又包括请求域属性名、session 和 application。param 用来获取请求参数,常见的内置对象有 request、session、lists 和 strings 等。 下面演示借助 OGNL 语言实现将 Student 对象共享到请求域中,并通过 Thymeleaf 将数据渲染到页面。 首先创建 Student、Teacher、Subject、School 等实体类。 Student 类示例代码如下。 Teacher 类示例代码如下。 Subject 类示例代码如下。 School 类示例代码如下。 创建 TestOgnlServlet 类,继承 ViewBaseServlet 基类,将 Student 对象存储在请求域对象 HttpServletRequest 中,并通过 Thymeleaf 将数据渲染到页面中,示例代码如下。 在 index.html 文件中编写代码,创建超链接,单击后跳转 TestOgnlServlet 类进行处理,示例代码如下。 OGNL 测试 在 views 目录下,创建 info\_03.html 文件,编写代码,用来显示 TestOgnlServlet 类处理后的结果,示例代码如下。 运行代码查看页面效果,如图 8-20 所示。然后单击此超链接,访问 TestOgnlServlet,如图 8-21 所示。   # 8.4.6 分支与迭代 前面介绍了 th:if 和 th:unless 用于条件判断,根据条件决定对应内容是否需要显示,th:switch 和 th:case 属于选择语句,而 th:each 用来循环迭代。接下来演示其具体的应用。 首先,创建 Employee 实体类,示例代码如下。 创建 EmployeeServlet 类,实现将 Employee 对象数据存储至 ArrayList 集合中,然后将其存储至 request 请求域中。示例代码如下。 然后在 views 下创建 list.html 前端页面,在页面中借助 th:if 和 th:unless 来显示数据,示例代码如下。 其中,if 搭配 not 关键词使用,和 unless 表达式的效果是一样的,两者皆可用。此外,还可以使用 switch 搭配 case 语句进行选择。 使用 th:each 遍历时,th:each 所在标签满足每次遍历出来一条数据就会添加一次该标签,语法格式如下。 th:each="遍历出来的数据,status:要遍历的数据" 其中,status 表示遍历的状态,它包含 index 和 count 属性。index 表示遍历出来的每个元素的下标;count 表示遍历出来的每个元素的计数。 例如,遍历显示请求域中的 employeeList 集合的值,示例代码如下。 # 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 | 把目标的代码片段去除最外层标签,然后再插入到当前标签内部 | 去掉片段外层标签,同时保留页面原有标签 |
| 字段名称 | 数据类型 | Key | |
| 编号 | fid | INT | PRI |
| 名称 | fname | VARCHAR(20) | |
| 价格 | price | INT | |
| 数量 | fcount | INT | |
| 描述 | remark | VARCHAR(50) |
| 名称 | 单价(元) | 库存(个) | 操作 |
| 大瓜 | 5 | 66 | × |
| 南瓜 | 4 | 48 | × |
| 苦瓜 | 3 | 50 | × |
| 鸭蓝 | 6 | 200 | × |
| 柚子 | 6 | 38 | × |
| 橙子 | 10 | 80 | × |
| 名称 | 单价(元) | 库存(个) | 操作 |
| 大瓜 | 5 | 66 | × |
| 南瓜 | 4 | 48 | × |
| 苦瓜 | 3 | 50 | × |
| 鸭梨 | 6 | 200 | × |
| 柚子 | 6 | 38 | × |
| 橙子 | 10 | 80 | × |
| 红富士 | 10 | 20 | × |