产生背景
J avaS erverP ages 的 .jsp 文件,代码中既有页面布局的静态元素,又有交互后端数据的动态元素,代码维护不方便
浏览器解释 html 时会忽略未定义的标签属性,因此某些标签可以根据后端返回值动态变换
Spring 提供了 Model
对象的 addAttribute(String attributeName, Object attributeValue)
方法,在控制器(Controller)方法中传递数据到视图
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语法
代表性属性名
属性名
功能
text
普通字符串
utext
转义文本
value
设置文本框的值
if/each
条件表达式
th:with
定义常量
action
指定表单的提交地址
object
设置绑定到表单元素的 Java 对象
field
设置元素的 id 属性或扮演 id 属性角色的属性
th:attr
使用标签内的属性替代原标签的配置
标准表达式
Thymeleaf 使用的标准表达式有四种类型:
类型名
书写方法
说明
变量表达式
${…}
可拼接到 `
选择变量表达式
*{…}
用于绑定模型对象属性
消息表达式
#{…}
用于i18n
链接 URL 表达式
@{…}
类似的标签有 th:href
和 th: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 > </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.properties
或 application.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" > © 2025 Copyright </div > <div id ="copy-section" > © 2025 Copyright </div > </body > </html >
上面的代码定义了一个片段称为 copy
,我们可以很容易地使用 th:include
或者 th:replace
属性包含在我们的主页上
1 2 3 4 <body > <div th:include ="footer :: copy" > </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) { 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" ; } }
表单数据绑定 (@ModelAttribute
) :用于将表单中的数据绑定到 Java 对象(User
)中。
路径参数 (@PathVariable
) :用于获取 URL 中的动态参数,如 id
。
Model
:用于在控制器方法中传递数据到视图,方法体接收Model 参数,返回的String由视图解析器处理
方法返回 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(); } }
在上面的例子中,虽然 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; @GetMapping("/search/{page}") public ModelAndView searchUsers ( @PathVariable(value = "page", required = false) Integer page, // 设置默认值为 null HttpSession session) { if (page == null ) { page = 1 ; } if (session.getAttribute("user" ) == null ) { return new ModelAndView ("redirect:/login" ); } Integer pageSize = (Integer) session.getAttribute("pageSize" ); if (pageSize == null ) { pageSize = 10 ; } PageHelper.startPage(page, pageSize); List<User> users = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo <>(users); 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); return "redirect:/search/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 >
总结
视图解析模型的对比
特性
Model
ModelAndView
主要用途
主要用于传递数据到视图
同时传递数据和视图
数据设置方式
使用 model.addAttribute(...)
使用 modelAndView.addObject(...)
视图设置方式
通过返回视图名称,视图由解析器决定
通过构造 ModelAndView
时指定视图
常见场景
简单的视图数据传递
需要明确指定视图和数据时
灵活性
相对灵活,适合大多数简单场景
在需要更多控制时更为直接
在大多数应用场景中,Model
是更常用的方式,尤其是在配合 Thymeleaf 时,它能保持控制器代码的简洁。而 ModelAndView
更多用于需要在一个地方集中控制数据和视图时。
Spring 框架用于简化处理 HTTP 请求参数与 Java 方法参数之间的绑定的工具
优点
缺点
@ModelAttribute
自动绑定参数到对象
需要通过 getter 方法来访问对象的属性
@RequestParam
对象数值可以直接
需要使用 setter 方法将多个参数赋给一个对象
3.更先进的前后端交互方式
随着前后端分离架构的流行,使用 AJAX 和 RESTful API 的方式逐渐取代了传统的服务器渲染方式(如 Thymeleaf)。这种方式将 前端和后端 解耦,前端使用 JavaScript 框架(如 Vue, React, Angular 等)构建用户界面,通过 API 获取数据并动态渲染页面。
AJAX + RESTful API :前端通过 AJAX 动态请求数据,后端返回 JSON 数据,前端根据数据更新页面。这种方式在现代 Web 应用中变得越来越流行。
Thymeleaf :仍然适用于传统的 服务器渲染 页面,但它的局限性在于需要重新加载整个页面,且不适合动态更新页面内容。随着单页面应用(SPA)技术的崛起,Thymeleaf 更多地用于静态页面渲染。
补充资料: