一、Thymeleaf入门

当我们用servlet实现动态页面展示的时候,我们需要用writer.write()写出整个页面,以展示动态信息。但是代码实在是太冗长,根本无法维护,所以我们需要将视图展示抽取出来,单独作为一个View视图层。
但是我们如果只使用HTML作为视图的话,它是无法展示动态数据的,所以我们对HTML的新的期待:既能够正常显示页面,又能在页面中包含动态数据部分。而动态数据单靠HTML本身是无法做到的,所以此时我们需要引入服务器端动态视图模板技术。
Thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎。类似JSP,Velocity,FreeMaker等,它也可以轻易的与Spring MVC等Web框架进行集成作为Web应用的模板引擎。它的主要作用是在静态页面上渲染显示动态数据。
Thymeleaf的优势:
SpringBoot官方推荐使用的视图模板技术,和SpringBoot完美整合。
不经过服务器运算仍然可以直接查看原始值,对前端工程师更友好。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
th:text="${username}"表示在当前p标签的标签体里面显示服务器中的一个username的数据
-->
<p th:text="${username}">Original Value</p>
</body>
</html>

在Servlet中,将请求转发到一个HTML页面文件时,使用的完整的转发路径(绝对路径)就是物理视图。
/pages/user/login_success.html
如果我们把所有的HTML页面都放在某个统一的目录下,那么转发地址就会呈现出明显的规律:
/pages/user/login.html
/pages/user/login_success.html
/pages/user/regist.html
/pages/user/regist_success.html……
路径的开头都是:/pages/user/
路径的结尾都是:.html
所以,路径开头的部分我们称之为视图前缀,路径结尾的部分我们称之为视图后缀。
物理视图=视图前缀+逻辑视图+视图后缀
上面的例子中:
| 视图前缀 | 逻辑视图 | 视图后缀 | 物理视图 |
|---|---|---|---|
| /pages/user/ | login | .html | /pages/user/login.html |
| /pages/user/ | login_success | .html | /pages/user/login_success.html |
第一步,加入jar包:

第二步,配置上下文参数:

物理视图=视图前缀+逻辑视图+视图后缀
<!-- 在上下文参数中配置视图前缀和视图后缀 -->
<context-param>
<param-name>view-prefix</param-name>
<param-value>/WEB-INF/view/</param-value>
</context-param>
<context-param>
<param-name>view-suffix</param-name>
<param-value>.html</param-value>
</context-param>
说明:param-value中设置的前缀、后缀的值不是必须叫这个名字,可以根据实际情况和需求进行修改。
为什么要放在WEB-INF目录下?
原因:WEB-INF目录不允许浏览器直接访问,所以我们的视图模板文件放在这个目录下,是一种保护。以免外界可以随意访问视图模板文件。
访问WEB-INF目录下的页面,都必须通过Servlet转发过来,简单说就是:不经过Servlet访问不了。
这样就方便我们在Servlet中检查当前用户是否有权限访问。
那放在WEB-INF目录下之后,重定向进不去怎么办?
重定向到Servlet,再通过Servlet转发到WEB-INF下。
第三步,创建Servlet基类:
这个类大家直接复制粘贴即可,将来使用框架后,这些代码都将被取代。
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ViewBaseServlet extends HttpServlet {
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());
// 3.处理模板数据
templateEngine.process(templateName, webContext, resp.getWriter());
}
}
第四步,创建index.html文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试Thymeleaf</title>
</head>
<body>
<a href="/class13_thymeleaf/testThymeleaf">初步测试Thymeleaf</a>
</body>
</html>
第五步,创建Servlet:

<servlet>
<servlet-name>TestThymeleafServlet</servlet-name>
<servlet-class>com.drimwai.servlet.model.TestThymeleafServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestThymeleafServlet</servlet-name>
<url-pattern>/testThymeleaf</url-pattern>
</servlet-mapping>
第六步,修改Servlet让其继承ViewBaseServlet:

第七步,在doPost()方法中跳转到Thymeleaf页面:
package com.drimwai.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class TestThymeleafServlet extends ViewBaseServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("username","奥巴马");
//请求转发跳转到/WEB-INF/view/target.html
processTemplate("target",request,response);
}
}
第八步,编写Thymeleaf页面代码:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>目标页面</title>
</head>
<body>
<h1 th:text="${username}">这里要显示一个动态的username</h1>
</body>
</html>

代码:
<p th:text="标签体新值">标签体原始值</p>
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>目标页面</title>
</head>
<body>
<p th:text="${username}">username</p>
</body>
</html>
th:text作用:
代码:
<input type="text" name="username" th:value="文本框新值" value="文本框旧值" />
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>目标页面</title>
</head>
<body>
<input type="text" name="username" th:value="${username}" value="username" />
</body>
</html>
语法:任何HTML标签原有的属性,前面加上『th:』就都可以通过Thymeleaf来设定新值。
代码:
<!--
使用Thymeleaf解析url地址
-->
<a th:href="@{/index.html}">访问index.html</a>
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>目标页面</title>
</head>
<body>
<a th:href="@{/index.html}">访问index.html</a>
</body>
</html>
经过解析后得到:
/class13_thymeleaf/index.html
所以@{/}的作用是在字符串前附加『上下文路径』
这个语法的好处是:实际开发过程中,项目在不同环境部署时,Web应用的名字有可能发生变化。所以上下文路径不能写死。而通过@{/}动态获取上下文路径后,不管怎么变都不怕啦!
代码:
<!--
使用Thymeleaf解析url地址
-->
<a th:href="@{/index.html(参数名=${动态数据},参数名='静态数据')}">访问index.html</a>
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>目标页面</title>
</head>
<body>
<a th:href="@{/index.html(username=${username},password='123456')}">访问index.html</a>
</body>
</html>

如果我们直接访问index.html本身,那么index.html是不需要通过Servlet,当然也不经过模板引擎,所以index.html上的Thymeleaf的任何表达式都不会被解析。
解决办法:通过Servlet访问index.html,这样就可以让模板引擎渲染页面了。

进一步的好处:
通过上面的例子我们看到,所有和业务功能相关的请求都能够确保它们通过Servlet来处理,这样就方便我们统一对这些请求进行特定规则的限定。
第一步,创建一个动态首页hello.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>动态index页面</title>
</head>
<body>
<p th:text="${value}">value</p>
</body>
</html>
第二步,创建一个IndexServlet:
package com.drimwai.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class IndexThymeleafServlet 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 {
request.setAttribute("value","这是一个动态的欢迎页面");
//请求转发跳转到/WEB-INF/view/hello.html
processTemplate("hello",request,response);
}
}
第三步,修改web.xml把默认的首页指向servlet:
<!-- 指向首页的Servlet -->
<servlet>
<servlet-name>indexThymeleafServlet</servlet-name>
<servlet-class>com.drimwai.servlet.IndexThymeleafServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>indexThymeleafServlet</servlet-name>
<url-pattern>/indexThymeleaf</url-pattern>
</servlet-mapping>
<!-- 把首页指向IndexServlet -->
<welcome-file-list>
<welcome-file>indexThymeleaf</welcome-file>
</welcome-file-list>
域对象是在服务器中有一定作用域范围的对象,在这个范围内的所有动态资源都能够共享域对象中保存的数据。
在请求转发的场景下,我们可以借助HttpServletRequest对象内部给我们提供的存储空间,帮助我们携带数据,把数据发送给转发的目标资源。
请求域:HttpServletRequest对象内部给我们提供的存储空间
会话域的范围是一次会话

应用域的范围是整个项目全局

我们通常的做法是,在Servlet中将数据存储到域对象中,而在使用了Thymeleaf的前端页面中取出域对象中的数据并展示
Servlet中代码:
String requestAttrName = "helloRequestAttr";
String requestAttrValue = "helloRequestAttr-VALUE";
request.setAttribute(requestAttrName, requestAttrValue);
Thymeleaf表达式:
<p th:text="${helloRequestAttr}">request field value</p>
Servlet中代码:
// ①通过request对象获取session对象
HttpSession session = request.getSession();
// ②存入数据
session.setAttribute("helloSessionAttr", "helloSessionAttr-VALUE");
Thymeleaf表达式:
<p th:text="${session.helloSessionAttr}">这里显示会话域数据</p>
Servlet中代码:
// ①通过调用父类的方法获取ServletContext对象
ServletContext servletContext = getServletContext();
// ②存入数据
servletContext.setAttribute("helloAppAttr", "helloAppAttr-VALUE");
Thymeleaf表达式:
<p th:text="${application.helloAppAttr}">这里显示应用域数据</p>
当一个带参数的链接指向一个Servlet,那么该Servlet该如何获取参数,并显示到Thymeleaf的页面中呢?
代码:
<p th:text="${param.username}">这里替换为请求参数的值</p>
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>目标页面</title>
</head>
<body>
<p th:text="${param.username}">这里替换为请求参数的值</p>
</body>
</html>
页面显示效果:

代码:
<p th:text="${param.team}">这里替换为请求参数的值</p>
页面显示效果:

如果想要精确获取某一个值,可以使用数组下标。页面代码:
<p th:text="${param.team[0]}">这里替换为请求参数的值</p>
<p th:text="${param.team[1]}">这里替换为请求参数的值</p>
页面显示效果:

所谓内置对象其实就是在Thymeleaf的表达式中可以直接使用的对象。

用法举例:
<h3>表达式的基本内置对象</h3>
<p th:text="${#request.getContextPath()}">调用#request对象的getContextPath()方法</p>
<p th:text="${#request.getAttribute('helloRequestAttr')}">调用#request对象的getAttribute()方法,读取属性域</p>
基本思路:

Servlet中将List集合数据存入请求域:
request.setAttribute("aNotEmptyList", Arrays.asList("aaa","bbb","ccc"));
request.setAttribute("anEmptyList", new ArrayList<>());
页面代码:
<p>#list对象isEmpty方法判断集合整体是否为空aNotEmptyList:<span th:text="${#lists.isEmpty(aNotEmptyList)}">测试#lists</span></p>
<p>#list对象isEmpty方法判断集合整体是否为空anEmptyList:<span th:text="${#lists.isEmpty(anEmptyList)}">测试#lists</span></p>
公共内置对象对应的源码位置:

让标记了th:if、th:unless的标签根据条件决定是否显示。
示例的Servlet代码:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//往request域对象中存储List集合
request.setAttribute("aNotEmptyList", Arrays.asList("aaa","bbb","ccc"));
request.setAttribute("anEmptyList", new ArrayList<>());
//调用方法渲染视图
processTemplate("list", request, response);
}
示例的HTML代码:
<!--
使用Thymeleaf的th:if和th:unless来控制标签的显示与否
-->
<p th:unless="${#lists.isEmpty(aNotEmptyList)}">anNotEmptyList中有数据</p>
<p th:if="${#lists.isEmpty(aNotEmptyList)}">anNotEmptyList中没有有数据</p>
<p th:unless="${#lists.isEmpty(anEmptyList)}">anEmptyList中有数据</p>
<p th:if="${#lists.isEmpty(anEmptyList)}">anEmptyList中没有有数据</p>
<h3>测试switch</h3>
<div th:switch="${level}">
<p th:case="level1">银牌会员</p>
<p th:case="level2">金牌会员</p>
<p th:case="level3">白金会员</p>
<p th:case="level4">钻石会员</p>
</div>
示例的Servlet代码:
List<String> userList = new ArrayList<>();
userList.add("张三");
userList.add("李四");
userList.add("王五");
userList.add("赵六");
userList.add("田七");
request.setAttribute("userList", userList);
示例的HTML代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户列表展示页面</title>
</head>
<body>
<!--
获取域对象中的集合,并且遍历展示
-->
<table border="1" cellspacing="0" width="400" align="center">
<thead>
<tr>
<th>下标</th>
<th>计数</th>
<th>用户名</th>
</tr>
</thead>
<tbody th:if="${#lists.isEmpty(userList)}">
<tr>
<td colspan="3">用户信息为空,没法遍历展示数据</td>
</tr>
</tbody>
<tbody th:unless="${#lists.isEmpty(userList)}">
<!--
使用th:each进行遍历迭代
1. th:each写在哪? 每遍历出一条数据就要添加一个什么标签就写在什么标签上
2. th:each="遍历出来的数据 : 要遍历的数据"
3. 我们怎么使用遍历出来的数据: ${遍历出来的数据}
我们怎么获取遍历出来的每一个数据的状态:th:each="遍历出来的数据,遍历出来的数据的状态 : 要遍历的数据"
遍历出来的数据的状态又包含:
index表示下标
count表示计数
even表示是否是计数
odd表示是否是偶数
current表示遍历出来的当前元素
first表示第一个元素
last表示最后一个元素
例如: 下标
-->
<tr th:each="user,status : ${userList}">
<td th:text="${status.index}"></td>
<td th:text="${status.even}"></td>
<td th:text="${user}"></td>
</tr>
</tbody>
</table>
</body>
</html>
抽取各个页面的公共部分:

第一步,创建segment.html页面存放公共代码片段:
使用th:fragment来给这个片段命名:
<div id="inner" th:fragment="header">
<p>被抽取出来的头部内容</p>
</div>
第二步,在需要的页面中进行包含:
| 语法 | 效果 | 特点 |
|---|---|---|
| th:insert | 把目标的代码片段整个插入到当前标签内部 | 它会保留页面自身的标签 |
| th:replace | 用目标的代码替换当前标签 | 它不会保留页面自身的标签 |
| th:include | 把目标的代码片段去除最外层标签,然后再插入到当前标签内部 | 它会去掉片段外层标记,同时保留页面自身标记 |
页面代码举例:
<!-- 代码片段所在页面的逻辑视图 :: 代码片段的名称 -->
<div id="outer" th:insert="segment :: header">
div标签的原始内容
</div>
<div id="outer" th:replace="segment :: header">
div标签的原始内容
</div>
<div id="outer" th:include="segment :: header">
div标签的原始内容
</div>