Skip to content

Thymeleaf:现代化的服务端模板引擎 🎨

什么是 Thymeleaf?

Thymeleaf 是一个现代化的服务端 Java 模板引擎,它的核心理念是让 HTML 模板保持"自然"状态——即使没有服务器运行,你也可以直接在浏览器中双击打开 HTML 文件进行预览。

NOTE

想象一下,前端设计师可以独立工作,无需启动整个 Spring Boot 应用就能看到页面效果,这就是 Thymeleaf 的魅力所在!

为什么需要 Thymeleaf?🤔

传统 JSP 的痛点

在 Thymeleaf 出现之前,我们主要使用 JSP 来处理服务端渲染:

jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>用户列表</title>
</head>
<body>
    <h1>用户列表</h1>
    <c:forEach var="user" items="${users}">
        <div>
            <p>姓名: ${user.name}</p>
            <p>邮箱: ${user.email}</p>
        </div>
    </c:forEach>
</body>
</html>
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>用户列表</title>
</head>
<body>
    <h1>用户列表</h1>
    <div th:each="user : ${users}">
        <p>姓名: <span th:text="${user.name}">示例用户</span></p>
        <p>邮箱: <span th:text="${user.email}">[email protected]</span></p>
    </div>
</body>
</html>

TIP

注意看 Thymeleaf 版本中的 "示例用户" 和 "[email protected]",这些是静态预览时显示的内容,而在服务器渲染时会被实际数据替换。

Thymeleaf 解决的核心问题

Spring Boot 中集成 Thymeleaf

1. 添加依赖

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 
    implementation("org.springframework.boot:spring-boot-starter-web")
}

NOTE

Spring Boot 的自动配置会自动处理 Thymeleaf 的核心组件:ServletContextTemplateResolverSpringTemplateEngineThymeleafViewResolver

2. 配置文件设置

yaml
# application.yml
spring:
  thymeleaf:
    prefix: classpath:/templates/     # 模板文件路径
    suffix: .html                     # 模板文件后缀
    mode: HTML                        # 模板模式
    encoding: UTF-8                   # 字符编码
    cache: false                      # 开发时关闭缓存

WARNING

生产环境中应该将 cache 设置为 true 以提高性能!

3. 控制器示例

kotlin
@Controller
class UserController {

    @GetMapping("/users")
    fun listUsers(model: Model): String {
        // 模拟用户数据
        val users = listOf(
            User("张三", "[email protected]", 25),
            User("李四", "[email protected]", 30),
            User("王五", "[email protected]", 28)
        )
        
        model.addAttribute("users", users) 
        model.addAttribute("title", "用户管理系统")
        
        return "user-list" // 返回模板名称,对应 templates/user-list.html
    }
    
    @GetMapping("/user/{id}")
    fun userDetail(@PathVariable id: Long, model: Model): String {
        val user = User("张三", "[email protected]", 25)
        model.addAttribute("user", user)
        return "user-detail"
    }
}

data class User(
    val name: String,
    val email: String,
    val age: Int
)

4. Thymeleaf 模板

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${title}">用户列表</title>
    <style>
        .user-card { 
            border: 1px solid #ccc; 
            margin: 10px; 
            padding: 15px; 
            border-radius: 5px; 
        }
    </style>
</head>
<body>
    <h1 th:text="${title}">用户管理系统</h1>
    
    <!-- 条件渲染 -->
    <div th:if="${#lists.isEmpty(users)}">
        <p>暂无用户数据</p>
    </div>
    
    <!-- 循环渲染 -->
    <div th:unless="${#lists.isEmpty(users)}">
        <div class="user-card" th:each="user, iterStat : ${users}">
            <h3>用户 #<span th:text="${iterStat.count}">1</span></h3>
            <p>姓名: <strong th:text="${user.name}">示例姓名</strong></p>
            <p>邮箱: <span th:text="${user.email}">[email protected]</span></p>
            <p>年龄: <span th:text="${user.age}">0</span> 岁</p>
            
            <!-- 条件样式 -->
            <span th:if="${user.age >= 30}" 
                  th:class="'badge-senior'" 
                  style="color: red;">资深用户</span>
            <span th:unless="${user.age >= 30}" 
                  style="color: green;">年轻用户</span>
        </div>
    </div>
</body>
</html>
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户详情</title>
</head>
<body>
    <div th:object="${user}">
        <h1>用户详情</h1>
        <p>姓名: <span th:text="*{name}">姓名</span></p>
        <p>邮箱: <a th:href="'mailto:' + *{email}" th:text="*{email}">邮箱</a></p>
        <p>年龄: <span th:text="*{age}">年龄</span></p>
        
        <!-- URL 生成 -->
        <a th:href="@{/users}">返回用户列表</a>
    </div>
</body>
</html>

Thymeleaf 核心语法速览 📚

常用表达式

表达式类型语法说明示例
变量表达式${...}获取上下文变量${user.name}
选择表达式*{...}在选定对象上执行*{name} (需配合 th:object)
URL 表达式@{...}生成 URL@{/users/{id}(id=${user.id})}
消息表达式#{...}国际化消息#{welcome.message}

常用属性

html
<!-- 文本内容 -->
<span th:text="${message}">默认文本</span>
<div th:utext="${htmlContent}">HTML内容</div>

<!-- 属性设置 -->
<input th:value="${user.name}" />
<img th:src="@{/images/logo.png}" />
<a th:href="@{/user/{id}(id=${user.id})}">查看详情</a>

<!-- 条件判断 -->
<div th:if="${user.age >= 18}">成年用户</div>
<div th:unless="${user.age >= 18}">未成年用户</div>

<!-- 循环 -->
<li th:each="item : ${items}" th:text="${item}">项目</li>

<!-- 表单绑定 -->
<form th:object="${user}" th:action="@{/user/save}" method="post">
    <input th:field="*{name}" />
    <input th:field="*{email}" />
</form>

实际业务场景应用 🚀

场景:电商商品列表页面

kotlin
@Controller
class ProductController {

    @GetMapping("/products")
    fun listProducts(
        @RequestParam(defaultValue = "0") page: Int,
        @RequestParam(defaultValue = "") category: String,
        model: Model
    ): String {
        val products = productService.findProducts(page, category)
        val categories = categoryService.findAllCategories()
        
        model.addAttribute("products", products)
        model.addAttribute("categories", categories)
        model.addAttribute("currentCategory", category)
        model.addAttribute("currentPage", page)
        
        return "product-list"
    }
}
完整的商品列表模板示例
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品列表</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h1>商品列表</h1>
        
        <!-- 分类筛选 -->
        <div class="mb-3">
            <a th:href="@{/products}" 
               th:class="${#strings.isEmpty(currentCategory)} ? 'btn btn-primary' : 'btn btn-outline-primary'">
                全部
            </a>
            <a th:each="cat : ${categories}"
               th:href="@{/products(category=${cat.name})}"
               th:text="${cat.name}"
               th:class="${currentCategory == cat.name} ? 'btn btn-primary ms-2' : 'btn btn-outline-primary ms-2'">
                分类
            </a>
        </div>
        
        <!-- 商品网格 -->
        <div class="row">
            <div class="col-md-4 mb-4" th:each="product : ${products}">
                <div class="card">
                    <img th:src="@{/images/products/{img}(img=${product.image})}" 
                         class="card-img-top" alt="商品图片">
                    <div class="card-body">
                        <h5 class="card-title" th:text="${product.name}">商品名称</h5>
                        <p class="card-text" th:text="${product.description}">商品描述</p>
                        <p class="text-danger fw-bold">
                            ¥<span th:text="${#numbers.formatDecimal(product.price, 0, 2)}">0.00</span>
                        </p>
                        <a th:href="@{/product/{id}(id=${product.id})}" 
                           class="btn btn-primary">查看详情</a>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- 分页 -->
        <nav th:if="${totalPages > 1}">
            <ul class="pagination justify-content-center">
                <li th:class="${currentPage == 0} ? 'page-item disabled' : 'page-item'">
                    <a class="page-link" 
                       th:href="@{/products(page=${currentPage - 1}, category=${currentCategory})}">
                        上一页
                    </a>
                </li>
                <li th:each="i : ${#numbers.sequence(0, totalPages - 1)}"
                    th:class="${i == currentPage} ? 'page-item active' : 'page-item'">
                    <a class="page-link" 
                       th:href="@{/products(page=${i}, category=${currentCategory})}"
                       th:text="${i + 1}">1</a>
                </li>
                <li th:class="${currentPage == totalPages - 1} ? 'page-item disabled' : 'page-item'">
                    <a class="page-link" 
                       th:href="@{/products(page=${currentPage + 1}, category=${currentCategory})}">
                        下一页
                    </a>
                </li>
            </ul>
        </nav>
    </div>
</body>
</html>

Thymeleaf vs 其他模板引擎 ⚖️

特性ThymeleafJSPFreeMarker
自然模板✅ 支持❌ 不支持❌ 不支持
静态预览✅ 可以❌ 不可以❌ 不可以
Spring 集成✅ 原生支持✅ 传统支持✅ 良好支持
学习曲线📈 适中📈 较陡📈 较陡
性能🚀 良好🚀 优秀🚀 优秀

最佳实践建议 💡

开发建议

  1. 开发环境关闭缓存spring.thymeleaf.cache=false
  2. 使用片段复用:创建通用的 header、footer 模板片段
  3. 合理使用表达式:优先使用 *{...} 配合 th:object 简化代码
  4. 静态资源路径:使用 @{...} 确保路径正确

性能优化

  • 生产环境启用模板缓存
  • 避免在模板中进行复杂的业务逻辑处理
  • 合理使用条件渲染,避免不必要的 DOM 元素

常见陷阱

  • 忘记添加 Thymeleaf 命名空间声明
  • 在循环中使用复杂表达式影响性能
  • 混用不同类型的表达式语法

总结 🎯

Thymeleaf 作为现代化的模板引擎,完美解决了传统 JSP 开发中前后端协作困难的问题。通过其"自然模板"的设计理念,让 HTML 模板既能独立预览,又能与 Spring Boot 无缝集成,大大提升了开发效率和团队协作体验。

对于 Spring Boot 开发者来说,Thymeleaf 不仅是 JSP 的优秀替代品,更是构建现代 Web 应用的强大工具! 🎉