产生背景

  1. JavaServerPages 的 .jsp 文件,代码中既有页面布局的静态元素,又有交互后端数据的动态元素,代码维护不方便
  2. 浏览器解释 html 时会忽略未定义的标签属性,因此某些标签可以根据后端返回值动态变换
  3. Spring 提供了 Model 对象的 addAttribute(String attributeName, Object attributeValue) 方法,在控制器(Controller)方法中传递数据到视图
  4. Springboot 使用 “习惯优于配置” ,集成了 ThymeLeaf 引擎,可以通过 spring-boot-starter-thymeleaf 实现自动配置

交互方式

在后端依赖文件引入

使用Maven的项目,在pom.xml文件加入

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>

使用Gradle的项目,在 gradle.build 文件加入

1
2
3
dependencies{
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

在html文件中加入标签

对于使用 Thymeleaf 的模板文件,Thymeleaf 的语法是基于 XML 的,因此需要在文件的根元素 <html> 中加上 xmlns 声明

1
2
3
4
5
6
7
8
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Thymeleaf Example</title>
</head>
<body>
<h1 th:text="${message}">Default message</h1>
</body>
</html>

对于标准的 HTML5 文件(即 <!DOCTYPE html>),它本身并不需要严格遵循 XHTML 的语法要求,因此可以省略命名空间声明,直接使用 Thymeleaf 标签和功能。

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"> <!-- 设置字符编码 -->
<title>Thymeleaf Example</title>
</head>
<body>
<h1 th:text="${message}">Default message</h1>
</body>
</html>

可以看出获取变量值用 $ 符号,对于javaBean的话使用 变量名.属性名 方式获取,这点和 EL 表达式一样。另外 $ 表达式只能写在th标签内部,否则不生效。上面例子就是使用 th:text 标签的值替换 h1 标签里面的值,原有的 Default message 仅用于前端开发时展示用

Thymeleaf语法

1
th:属性名="标准表达式"

代表性属性名

属性名 功能
text 普通字符串
utext 转义文本
value 设置文本框的值
if/each 条件表达式
th:with 定义常量
action 指定表单的提交地址
object 设置绑定到表单元素的 Java 对象
field 设置元素的 id 属性或扮演 id 属性角色的属性
th:attr 使用标签内的属性替代原标签的配置

标准表达式

Thymeleaf 使用的标准表达式有四种类型:

类型名 书写方法 说明
变量表达式 ${…} 可拼接到 `
选择变量表达式 *{…} 用于绑定模型对象属性
消息表达式 #{…} 用于i18n
链接 URL 表达式 @{…} 类似的标签有 th:hrefth:src

此外,可进行算数、比较、条件、真假运算

例如 th:with="isEven=(${prodStat.count} % 2 == 0)"

分类 实例
if-then (if) ? (then)
if-then-else (if) ? (then) : (else)
Default (value) ?: (defaultvalue)
1
2
3
4
5
6
7
8
9
10
11
12
条件表达式if/unless
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<a href="comments.html" th:href="@{/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a>

<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p>
</div>

<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p> <p th:case="*">User is some other thing</p>
</div>

消息表达式

1
2
3
4
5
6
7
8
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

<!-- 更简单的表达式,但是|…|中只能包含变量表达式${…},不能包含其他常量、条件表达式等 -->
<span th:text="|Welcome to our application, ${user.name}!|">


<p th:utext="#{home.welcome(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper!</p>
<p th:utext="#{${welcomeMsgKey}(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper!</p>

选择变量表达式

1
2
3
4
5
6
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text=*{nationality}">Saturn</span>.</p>
</div>

等价于

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

<!-- 也可用选择变量表达式 -->
<div>
<p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
</div>

在标签中加入 th:block 使编译前不可见

1
2
3
4
5
6
7
8
9
<table>
<tr>
<td th:text="${user.login}">...</td>
<td th:text="${user.name}">...</td> </tr>
<tr>
<td colspan="2" th:text="${user.address}">...</td>
</tr>
<!--/*/ </th:block> /*/-->
</table>

引入外部链接

1
2
3
<a th:href="@{http://www.baidu.com}">绝对路径</a>
<a th:href="@{/}">相对路径</a>
<a th:href="@{css/bootstrap.min.css}">Content路径,默认访问static下的css文件夹</a>

Thymeleaf 前后端交互

模板元素的前端跨文件传递

通常应该根据控制器方法返回的视图名称来命名模板文件,也可以在application.propertiesapplication.yml 文件中配置模板前缀和后缀。

模板文件应该位于 src/main/resources/templates 目录下,引擎会从会在该目录下加载对应的文件

例如,首先定义一个 footer.html 文件:

1
2
3
4
5
6
7
8
9
10
11
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
&copy; 2025 Copyright
</div>
<div id="copy-section">
&copy; 2025 Copyright
</div>

</body>
</html>

上面的代码定义了一个片段称为 copy,我们可以很容易地使用 th:include 或者 th:replace 属性包含在我们的主页上

1
2
3
4
<body>
<div th:include="footer :: copy"></div><!-- 在div标签内引入模板元素 -->
<div th:replace="footer :: copy"></div><!-- 用模板元素替代当前标签 -->
</body>

这里有三种写法:

  • 模板文件名::DOM标签名 或者 模板文件名::[DOM标签名] 引入模板页面中的某个模块
  • 模板文件名 引入模板页面
  • ::DOM标签名 或者 this::DOM标签名 引入自身模板的模块
1
2
3
4
5
上面所有的 `模板文件名` 和 `DOM标签名` 的写法都支持表达式写法:
<div th:include="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>

对于没有定义th:fragment的元素,也可以用 CSS 的选择器写法来引入
<div th:include="footer :: #copy-section"></div>

数据模型的前后端传递

在Thymeleaf页面中,利用表格元素和后端页面进行交互,例如 JavaBean 对象 User 类含有 name、age 和 id 属性

1
2
3
4
5
<form th:action="${user.id != null ? '/update/' + user.id : '/create'}" method="post">
<input type="text" th:field="*{name}"/>
<input type="text" th:field="*{age}"/>
<input type="submit" value="submit"/>
</form>

th:action 指定提交表单的方式。th:object 指定要绑定的对象,th:field 则映射到绑定对象的字段。因此,th:object 和 th:field 通常作为一个集合使用。

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
public class UserController {
@RequestMapping(value="/create", method=RequestMethod.POST)
public String createUser(@ModelAttribute("User") User user) {
// 这里user对象会自动包含表单提交的数据
userService.createUser(user);
return "userlist"; // 返回到视图页面
}

@PostMapping("/update/{id}")
public String updateUser(@PathVariable("id") Long id, @ModelAttribute User user, Model model) {
userService.updateUser(id, user);

// 将用户信息添加到模型中以便在视图中显示
model.addAttribute("user", user);

// 返回视图页面
return "result"; // 假设返回到 result.html 视图
}
}
  1. 表单数据绑定 (@ModelAttribute):用于将表单中的数据绑定到 Java 对象(User)中。
  2. 路径参数 (@PathVariable):用于获取 URL 中的动态参数,如 id
  3. Model:用于在控制器方法中传递数据到视图,方法体接收Model 参数,返回的String由视图解析器处理
  4. 方法返回 String 为视图的名称,Spring 会根据视图解析器来找到对应的 Thymeleaf 模板(例如 result.html),并渲染该页面。

传递数据和视图

ModelAndView 对象封装了模型(数据)和视图(模板)信息。

拦截器(Interceptor)自定义视图处理器(ViewResolver) 中,ModelAndView 可以作为参数,进行视图和模型的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class UserController {

@PostMapping("/submit")
public String handleFormSubmission(@ModelAttribute User user, ModelAndView modelAndView) {
// 设置模型数据
modelAndView.addObject("user", user);

// 根据条件动态返回不同的视图
if (user.getAge() > 18) {
modelAndView.setViewName("adultUser");
} else {
modelAndView.setViewName("minorUser");
}

return modelAndView.getViewName(); // 返回视图名称(String)
}
}

在上面的例子中,虽然 ModelAndView 被用作方法参数,但最终返回的是视图名称(String)。

分页显示查询结果的案例实现

案例要求:

  • ModelAndView 适合用来处理 查询操作,尤其是当你需要渲染多个结果时。通过 th:each 和分页信息,你可以灵活地展示大量数据。
  • 权限控制和用户习惯:使用 session 来判断用户是否有权限访问某个页面(也可以通过Spring Security实现),尤其是查询操作,防止未授权的用户访问。
  • 分页处理:通过 Pageable 接口和 PageRequest 实现分页查询,避免一次性加载大量数据,从而提高性能并减少服务器负载。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Controller
public class UserController {
@Autowired
private UserMapper userMapper; // 使用 MyBatis 的 Mapper

@GetMapping("/search/{page}")
public ModelAndView searchUsers(
@PathVariable(value = "page", required = false) Integer page, // 设置默认值为 null
HttpSession session) {
// 如果未提供页码参数,默认为第1页
if (page == null) {
page = 1;
}

// 权限验证
if (session.getAttribute("user") == null) {
return new ModelAndView("redirect:/login");
}

// 从 session 获取每页显示的条数,如果没有则使用默认的 10 条
Integer pageSize = (Integer) session.getAttribute("pageSize");
if (pageSize == null) {
pageSize = 10; // 默认每页显示10条数据
}

// 启动分页,PageHelper 会自动拦截 SQL 执行分页
PageHelper.startPage(page, pageSize);

// 执行查询:自动在查询时添加 LIMIT 和分页条件
List<User> users = userMapper.selectAll(); // 使用 MyBatis Mapper 执行查询

// 使用 PageInfo 包装查询结果,获取分页信息
PageInfo<User> pageInfo = new PageInfo<>(users);

// 创建 ModelAndView 对象并返回
ModelAndView modelAndView = new ModelAndView("userList");
modelAndView.addObject("pageInfo", pageInfo); // 将分页结果传递给视图

return modelAndView;
}

// 设置每页显示条数
@PostMapping("/setPageSize")
public String setPageSize(@RequestParam("size") int size, HttpSession session) {
session.setAttribute("pageSize", size); // 将页面设置的条数保存在 session 中
return "redirect:/search/1"; // 设置后重定向到第1页
}
}

使用MyBatis的PageHelper,通过拦截器的方式处理,分页逻辑 是通过 PageHelper 来控制的,而不是通过 SQL 查询语句本身。Spring Data JPA 也提供了 Pageable 接口自动处理分页的底层逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper
public interface UserMapper {

// 查询所有用户
@Select("SELECT * FROM users")
List<User> selectAll();

// 也可以根据条件查询
@Select("SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')")
List<User> searchByName(@Param("name") String name);
}

Thymeleaf 模板文件

在模板文件 userPage.html中,利用 th:each 来渲染查询结果,并通过分页信息来生成分页控件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User List</title>
</head>
<body>
<h1>User List</h1>

<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${pageInfo.list}">
<td th:text="${user.name}"></td>
<td th:text="${user.age}"></td>
</tr>
</tbody>
</table>

<div>
<p>Total Items: ${pageInfo.total}</p>
<span th:if="${pageInfo.pageNum > 1}">
<a th:href="@{/search/{page}(page=${pageInfo.pageNum - 1})}">Previous</a>
</span>
<!-- 显示当前页码 -->
<span th:text="'Page ' + ${pageInfo.pageNum} + ' of ' + ${pageInfo.pages}"></span>

<span th:if="${pageInfo.pageNum < pageInfo.pages}">
<a th:href="@{/search/{page}(page=${pageInfo.pageNum + 1})}">Next</a>
</span>

<span th:if="${pageInfo.pageNum < pageInfo.pages}">
<a th:href="@{/search/{page}(page=${pageInfo.pages})}">End</a>
</span>
</div>


</body>
</html>

总结

  1. 视图解析模型的对比
特性 Model ModelAndView
主要用途 主要用于传递数据到视图 同时传递数据和视图
数据设置方式 使用 model.addAttribute(...) 使用 modelAndView.addObject(...)
视图设置方式 通过返回视图名称,视图由解析器决定 通过构造 ModelAndView 时指定视图
常见场景 简单的视图数据传递 需要明确指定视图和数据时
灵活性 相对灵活,适合大多数简单场景 在需要更多控制时更为直接

在大多数应用场景中,Model 是更常用的方式,尤其是在配合 Thymeleaf 时,它能保持控制器代码的简洁。而 ModelAndView 更多用于需要在一个地方集中控制数据和视图时。

  1. Spring 框架用于简化处理 HTTP 请求参数与 Java 方法参数之间的绑定的工具
优点 缺点
@ModelAttribute 自动绑定参数到对象 需要通过 getter 方法来访问对象的属性
@RequestParam 对象数值可以直接 需要使用 setter 方法将多个参数赋给一个对象

3.更先进的前后端交互方式

随着前后端分离架构的流行,使用 AJAXRESTful API 的方式逐渐取代了传统的服务器渲染方式(如 Thymeleaf)。这种方式将 前端和后端 解耦,前端使用 JavaScript 框架(如 Vue, React, Angular 等)构建用户界面,通过 API 获取数据并动态渲染页面。

  • AJAX + RESTful API:前端通过 AJAX 动态请求数据,后端返回 JSON 数据,前端根据数据更新页面。这种方式在现代 Web 应用中变得越来越流行。
  • Thymeleaf:仍然适用于传统的 服务器渲染 页面,但它的局限性在于需要重新加载整个页面,且不适合动态更新页面内容。随着单页面应用(SPA)技术的崛起,Thymeleaf 更多地用于静态页面渲染。

补充资料: