一个服务端模板引擎:把 Controller 放进 Model 的数据,套进 HTML 模板,渲染出完整的页面再发给浏览器。
| JSP(老方案) | Thymeleaf(本课程方案) | |
|---|---|---|
| 页面里写什么 | Java 代码嵌在页面里(<% %>、<%= %>) | HTML 标签 + th: 属性,页面本身是合法 HTML |
| 浏览器直接打开 | 打不开(必须服务器编译) | 可以,未渲染的属性被忽略,页面不报错 |
| 前后端分离 | Java 逻辑与页面混杂 | 数据用 ${} 占位,职责清晰 |
| 引入方式 | 容器内置 | pom.xml 加 spring-boot-starter-thymeleaf |
src/main/resources/templates/ 目录;② 静态资源(css/js/图片)放 src/main/resources/static/ 目录。点击"下一步"逐步看(以访问 /book/list 为例):
/book/list,携带登录后的 sessionBookController.findBook() 方法books 图书列表"books"(不是页面内容,只是一个名字)templates/books.htmlth: 属性。表达式是 Thymeleaf 的"取数语法",写在 th: 属性或 ${} 里。点每个演示的按钮,看模板怎么变成最终页面。
<td th:text="${book.name}">图书名称</td> <td th:text="${#session.getAttribute('user').name}">用户名</td>
<td>楚辞</td> <td>zhangsan</td>
<link rel="stylesheet" th:href="@{/css/bootstrap.css}"> <form th:action="@{/book/search}" method="post">...</form>
<link rel="stylesheet" href="/css/bootstrap.css"> <form action="/book/search" method="post">...</form>
<div th:object="${book}"> <p th:text="*{name}">书名</p> <p th:text="*{author}">作者</p> </div>
<div> <p>楚辞</p> <p>屈原</p> </div>
// 配置文件 application.yml:spring.messages.basename: msg/home // 资源文件 msg/home.properties 里:welcome=欢迎使用图书管理系统 <h1 th:text="#{welcome}">标题</h1>
<h1>欢迎使用图书管理系统</h1>
th: 属性就是"告诉 Thymeleaf 怎么处理这个标签"的指令。先看总表,再看两个重点演示。
| 属性 | 作用 | 案例中的用法 |
|---|---|---|
th:text | 输出文本,替换标签内容 | th:text="${book.name}" |
th:href / th:src | 生成链接 / 资源地址(配 @{}) | th:href="@{/css/bootstrap.css}" |
th:action | 表单提交地址(配 @{}) | th:action="@{/book/search}" |
th:each | 循环遍历集合,重复渲染当前标签 | th:each="book : ${books}" |
th:if / th:unless | 条件成立 / 不成立才渲染 | th:if="${book.status == '0'}" |
th:switch / th:case | 多分支判断(等价 if-else if) | th:switch="${book.status}" |
th:onclick | 绑定点击事件(常配内联表达式) | th:onclick="findBookById([[${book.id}]])" |
th:block | 隐形容器:只控制渲染,不产生 HTML 标签 | <th:block th:each="..."> |
<th:block th:each="book : ${books}"> <tr> <td th:text="${book.name}"></td> <td> <th:block th:if="${book.status == '0'}">可借阅</th:block> <th:block th:if="${book.status == '1'}">借阅中</th:block> </td> </tr> </th:block>
<tr>
<td>楚辞</td>
<td>可借阅</td>
</tr>
<tr>
<td>纳兰词</td>
<td>借阅中</td>
</tr>
<button th:if="${book.status == '0'}" th:onclick="findBookById([[${book.id}]]);">借阅</button> <button th:if="${book.status == '1' || book.status == '2'}" disabled="true">借阅</button>
<button onclick="findBookById(1);">借阅</button> (楚辞,状态0 → 可点) <button disabled="true">借阅</button> (纳兰词,状态1 → 禁用)
<div th:if="${#session.getAttribute('user').role == 'ADMIN'}"> <button>新增</button> </div>
<div>
<button>新增</button>
</div>
除了在 th: 属性里写表达式,Thymeleaf 还允许把表达式"内联"进普通文本或 JS 代码里。
| 写法 | 含义 | 区别 |
|---|---|---|
[[${...}]] | 转义内联 | HTML 特殊字符会被转义(更安全,推荐) |
[(${...})] | 不转义内联 | 原样输出,可渲染 HTML 标签(有注入风险,慎用) |
<button th:onclick="findBookById([[${book.id}]]);">借阅</button> <script> function findBookById(id) { $.get("/book/find/" + id); // 请求后端,带上图书编号 } </script>
<button onclick="findBookById(1);">借阅</button>
<script>
function findBookById(id) {
$.get("/book/find/" + id);
}
</script>
下面是本案例 templates/books.html 的真实代码骨架。对照左侧模板、右侧渲染结果,逐段看懂。
<html xmlns:th="http://www.thymeleaf.org"> <link th:href="@{/css/bootstrap.css}" rel="stylesheet"> <link th:href="@{/css/AdminLTE.css}" rel="stylesheet"> <script th:src="@{/js/jquery.min.js}"></script>
<link href="/css/bootstrap.css" rel="stylesheet"> <link href="/css/AdminLTE.css" rel="stylesheet"> <script src="/js/jquery.min.js"></script>
<form th:action="@{/book/search}" method="post"> 图书名称:<input name="name"> 图书作者:<input name="author"> <input type="submit" value="查询"> </form>
<form action="/book/search" method="post">
图书名称:<input name="name">
图书作者:<input name="author">
<input type="submit" value="查询">
</form>
<th:block th:each="book : ${books}"> <tr> <td th:text="${book.name}"></td> <td th:text="${book.author}"></td> <td th:text="${book.press}"></td> </tr> </th:block>
<tr><td>楚辞</td><td>屈原</td><td>中国文联出版社</td></tr>
<tr><td>纳兰词</td><td>纳兰性德</td><td>中国文联出版社</td></tr>
<tr><td>西游记</td><td>吴承恩</td><td>中国文联出版社</td></tr>
<th:block th:if="${book.status == '0'}">可借阅</th:block> <th:block th:if="${book.status == '1'}">借阅中</th:block> <th:block th:if="${book.status == '2'}">归还中</th:block> <button th:if="${book.status == '0'}" th:onclick="findBookById([[${book.id}]]);">借阅</button>
楚辞(status=0)→ 可借阅 + <button onclick="findBookById(1);">借阅</button>
纳兰词(status=1)→ 借阅中 + <button disabled="true">借阅</button>
西游记(status=2)→ 归还中 + <button disabled="true">借阅</button>
把过滤器(上一章)、Controller、Thymeleaf 串成一条完整链路。点击"下一步"逐步走:
/doRegister 存用户,/dologin 把用户名存进 session(loginUser)LoginFilter(上一章的登录过滤器)templates/books.html,用 model 数据渲染/book/find/{id},后端打印借阅编号@Controller
@RequestMapping("book")
public class BookController {
ArrayList<Book> books = Data.getData(); // 3 本静态图书
@RequestMapping("list") // GET /book/list
public String findBook(Model model, HttpSession session){
session.setAttribute("user", new User("zhangsan","ADMIN"));
model.addAttribute("books", books); // 数据放进 Model
return "books"; // 返回视图名 → templates/books.html
}
@RequestMapping("search") // POST /book/search
public String searchBook(Book book, Model model, HttpSession session){
// 按 name / author 过滤后,重新放进 model 返回同一模板
...
return "books";
}
@RequestMapping("find/{id}") // GET /book/find/1
public String findBook(@PathVariable("id") Integer id){
System.out.println("申请借阅图书的id:" + id);
return "books";
}
}
三个接口的职责: · /book/list → 查全部图书,进列表页 · /book/search → 按名称/作者模糊查询,结果仍渲染同一张 books.html · /book/find/1 → 借阅接口:路径上的 1 是图书编号,由 th:onclick 里的内联表达式传过来 为什么都是 return "books"? 因为三个接口共用 templates/books.html 这一张模板, 只是 model 里的 books 集合内容不同(全量 or 过滤后的结果)。
Exception evaluating SpringEL expression。原则:模板用到的每个变量,Controller 都必须先放进去。