一、Thymeleaf 表达式概览
Thymeleaf 表达式以 ${...}、*{...}、#{...}、@{...}、~{...} 等形式存在,它们可以出现在 th:* 属性值中,也可以在内联脚本或文本中使用。每种表达式都有特定用途:
| 表达式类型 | 语法 | 主要用途 |
|---|---|---|
| 变量表达式 | ${...} |
获取后端传递的变量值,支持 OGNL/SpringEL 表达式 |
| 选择表达式 | *{...} |
在 th:object 指定的对象上执行表达式,用于对象属性导航 |
| 消息表达式 | #{...} |
访问国际化消息资源文件(如 messages.properties) |
| 链接表达式 | `@{...}`` | 生成 URL,自动处理上下文路径和参数 | | |
| 片段表达式 | ~{...} |
引用模板片段,用于页面布局复用 |
| 预处理表达式 | __${...}__ |
在表达式执行前进行预处理,用于动态选择表达式 |
二、表达式详解与案例
1. 变量表达式 ${...}
这是最常用的表达式,它获取 Controller 放入 Model 的属性值,并可以执行属性访问、方法调用、算术运算等操作。
基本用法:
<p th:text="${user.name}">姓名</p>
支持的操作:
属性访问 :
${user.name}、${user['name']}。方法调用 :
${user.getFullName()}、${list.size()}。集合访问 :
${list[0]}、${map['key']}。算术运算 :
${totalPrice * 0.8}。比较运算 :
${age >= 18}。逻辑运算 :
${isAdmin and isActive}。三元运算符 :
${sex == 'M' ? '先生' : '女士'}。Elvis 操作符 :
${user.name ?: '匿名'}(如果 user.name 为 null,显示"匿名")
2. 选择表达式 *{...}
选择表达式需要配合 th:object 使用,它直接在绑定的对象上执行表达式,简化代码。
<form th:object="${user}">
<input type="text" th:field="*{username}" />
<input type="password" th:field="*{password}" />
</form>
*{username}等价于${user.username}。选择表达式内部可以嵌套变量表达式:*{user.createdAt > ${yesterday}}。
3. 消息表达式 #{...}
用于国际化,根据当前语言环境从资源文件中获取消息。
资源文件 (messages.properties):
home.welcome=欢迎来到我的网站
user.greeting=你好,{0}!
模板中使用:
<h1 th:text="#{home.welcome}">默认欢迎语</h1> <p th:text="#{user.greeting(${user.name})}">你好,用户!</p>参数占位符
{0}会被传入的值替换。
4. 链接表达式 @{...}
生成 URL,自动包含应用的上下文路径(context path)。可以附加查询参数或路径变量。
基本用法:
<a th:href="@{/user/profile}">个人资料</a>
<!-- 生成:/context/user/profile -->
<a th:href="@{/user/details(id=${user.id}, action='view')}">详情</a>
<!-- 生成:/context/user/details?id=123&action=view -->
<a th:href="@{/user/{id}/edit(id=${user.id})}">编辑</a>
<!-- 生成:/context/user/123/edit -->
<img th:src="@{/images/logo.png}" alt="Logo">
链接表达式还可以指定协议(如 http://),或引用相对路径。
5. 片段表达式 ~{...}
用于引用模板片段,通常与 th:insert 或 th:replace 配合。
定义片段 (fragments/common.html):
<div th:fragment="footer">
<p>© 2023 公司名称</p>
</div>
引用片段:
<div th:replace="~{fragments/common :: footer}"></div>
6. 预处理表达式 __${...}__
在表达式执行前先解析内部表达式,常用于动态决定变量名。
<!-- 假设有一个变量 type = 'admin' -->
<div th:text="${__${type}.name__}"></div>
<!-- 预处理后变成 ${admin.name},然后执行 -->
7. 字面量与操作符
Thymeleaf 支持多种字面量:
文本 :用单引号括起来,如
'这是一个字符串'。数字 :
100、3.14。布尔值 :
true、false。null :
null。操作符:
算术:
+、-、*、/、%比较:
>、<、>=、<=、==、!=(也可以用gt、lt等别名)逻辑:
and、or、not、!条件:
(if) ? (then) : (else)、?:(Elvis,若左侧为 null 则返回右侧)
示例:
<span th:text="${age} > 18 ? '成年' : '未成年'"></span>
<span th:text="${user.name} ?: '游客'"></span>
8. 内置对象与工具类
Thymeleaf 提供一系列以 # 开头的内置对象,方便处理常见任务。
| 内置对象 | 用途 | 示例 |
|---|---|---|
#dates |
日期格式化与处理 | ${#dates.format(date, 'yyyy-MM-dd')} |
#calendars |
日历对象操作 | ${#calendars.createNow()} |
#numbers |
数字格式化 | ${#numbers.formatDecimal(price, 1, 2)} |
#strings |
字符串操作 | ${#strings.toUpperCase(name)}、${#strings.isEmpty(text)} |
#objects |
对象工具 | ${#objects.nullSafe(obj, '默认')} |
#arrays |
数组工具 | ${#arrays.length(array)} |
#lists |
列表工具 | ${#lists.size(list)}、${#lists.isEmpty(list)} |
#sets |
集合工具 | 类似 lists |
#maps |
Map 工具 | ${#maps.size(map)}、${#maps.containsKey(map, 'key')} |
#aggregates |
聚合函数 | ${#aggregates.sum(numbers)}、${#aggregates.avg(numbers)} |
#messages |
消息工具 | ${#messages.msg('key')} |
#uris |
URI 编码/解码 | ${#uris.escapePath(uri)} |
#conversions |
类型转换 | ${#conversions.convert(obj, 'java.lang.String')} |
#ctx |
上下文对象 | ${#ctx.variables} 获取所有模型变量 |
#locale |
当前语言环境 | ${#locale.language} |
示例:
<p th:text="${#dates.format(user.birthday, 'yyyy年MM月dd日')}">生日</p>
<p th:text="${#strings.capitalizeWords(user.fullName)}">姓名</p>
<p th:text="${#lists.size(products)}">产品数量</p>
三、综合案例分析:构建一个用户信息页面
下面通过一个完整的案例展示多种表达式的组合使用。假设我们有一个 Spring Boot 应用,包含用户信息展示、产品列表和片段布局。
1. 实体类
public class User {
private String name;
private String email;
private Date birthday;
private boolean vip;
// getter/setter 省略
}
public class Product {
private String name;
private BigDecimal price;
private int stock;
// getter/setter
}
2. Controller
@Controller
public class HomeController {
@GetMapping("/profile")
public String profile(Model model) {
User user = new User();
user.setName("张三");
user.setEmail("zhangsan@example.com");
user.setBirthday(new Date());
user.setVip(true);
model.addAttribute("user", user);
List<Product> products = Arrays.asList(
new Product("笔记本电脑", new BigDecimal("5999.00"), 10),
new Product("鼠标", new BigDecimal("99.00"), 50),
new Product("键盘", new BigDecimal("299.00"), 0)
);
model.addAttribute("products", products);
return "profile";
}
}
3. 片段文件 fragments/common.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!-- 头部片段,带标题参数 -->
<header th:fragment="header(title)">
<h1 th:text="${title}">默认标题</h1>
<nav>
<a th:href="@{/}">首页</a> |
<a th:href="@{/profile}">个人资料</a> |
<a th:href="@{/products}">产品</a>
</nav>
</header>
<!-- 页脚片段 -->
<footer th:fragment="footer">
<p th:text="#{footer.copyright}">© 2023</p>
</footer>
</body>
</html>
4. 国际化消息 messages.properties
footer.copyright=© 2023 我的公司。保留所有权利。
user.vip=VIP 会员
user.regular=普通会员
product.stock.available=有货
product.stock.out=缺货
5. 主页面 profile.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户资料</title>
<style>
.vip { color: gold; }
.stock { font-weight: bold; }
.out { color: red; }
</style>
</head>
<body>
<!-- 插入头部,传递标题参数 -->
<div th:replace="~{fragments/common :: header('个人资料')}"></div>
<main>
<h2>用户信息</h2>
<!-- 使用 th:object 绑定 user 对象 -->
<div th:object="${user}">
<p>姓名:<span th:text="*{name}">张三</span></p>
<p>邮箱:<span th:text="*{email}">email@example.com</span></p>
<p>生日:<span th:text="${#dates.format(*{birthday}, 'yyyy-MM-dd')}">1990-01-01</span></p>
<p>
会员类型:
<!-- 条件判断 + 消息表达式 -->
<span th:classappend="*{vip} ? 'vip' : ''"
th:text="*{vip} ? #{user.vip} : #{user.regular}">普通会员</span>
</p>
</div>
<h2>产品列表</h2>
<table border="1">
<thead>
<tr>
<th>产品名</th>
<th>价格</th>
<th>库存</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${products}" th:object="${product}">
<td th:text="*{name}">产品</td>
<td th:text="${#numbers.formatDecimal(*{price}, 1, 2)} + '元'">0.00元</td>
<td th:text="*{stock}">0</td>
<td>
<span th:if="*{stock > 0}" class="stock" th:text="#{product.stock.available}">有货</span>
<span th:unless="*{stock > 0}" class="stock out" th:text="#{product.stock.out}">缺货</span>
</td>
</tr>
</tbody>
</table>
<!-- 链接表达式示例 -->
<p>
<a th:href="@{/profile/edit}">编辑资料</a> |
<a th:href="@{/products/category(id=1, page=${currentPage})}">查看分类产品</a>
</p>
</main>
<!-- 插入页脚 -->
<div th:replace="~{fragments/common :: footer}"></div>
</body>
</html>
6. 页面渲染效果
访问
/profile后:
头部显示"个人资料",导航链接正确生成。
用户信息区域展示姓名、邮箱、格式化后的生日,并根据
vip值显示对应消息(VIP会员/普通会员),VIP 会员名称为金色。产品列表循环展示,库存状态通过
th:if判断,并显示国际化文本"有货"/"缺货",缺货时显示红色字体。底部显示版权消息,通过
#{footer.copyright}从国际化文件获取。链接表达式生成了带上下文路径的 URL,并附加了参数。