Thymeleaf模板引擎:从自然模板到服务端渲染的Java Web开发实践

发布时间:2026/8/1 13:40:00

Thymeleaf模板引擎:从自然模板到服务端渲染的Java Web开发实践 1. 从JSP到Thymeleaf一个模板引擎的演进与选择如果你是从Java Web开发的“上古时代”一路走过来的肯定对JSPJavaServer Pages又爱又恨。爱它简单直接在HTML里写点% %就能嵌入Java代码快速出活恨它维护起来简直是灾难前后端逻辑搅在一起稍微复杂点的页面就难以阅读更别提单元测试了。后来虽然有了FreeMarker、Velocity这些更清晰的模板引擎但它们本质上还是“服务器端渲染”的思维模板文件离开了后端服务器就是一堆无法直接预览的、带有特殊标签的“残次品”。Thymeleaf的出现很大程度上就是为了解决这个痛点。我第一次接触Thymeleaf是在一个需要前端设计师高度参与的项目里设计师习惯用浏览器直接打开HTML文件看效果而我们后端开发者又需要动态数据。用JSP设计师打不开。用纯HTMLAJAX初期原型和简单页面又显得杀鸡用牛刀。Thymeleaf的“自然模板”理念正中下怀——它允许你写标准的、语法良好的HTML文件那些用于动态替换的属性比如th:text在不经过服务器渲染时会被浏览器当作普通属性忽略页面依然能显示静态的默认值。这意味着同一个.html文件既是设计师眼里可预览的静态原型也是我们后端眼里的动态模板。这几年虽然前后端分离架构大行其道Vue、React成了前端主流但Thymeleaf并没有消失反而在一些特定场景下更加稳固。比如需要快速开发的后台管理系统、对SEO有要求的服务端渲染页面、邮件模板、PDF报告生成或者就是一些不那么复杂、不希望引入重型前端框架的内部应用。最近社区里讨论的“thymeleaf flying saucer”生成PDF以及“thymeleaf多页面布局”恰恰说明了它在报表输出和视图复用这些传统强项上依然有着旺盛的生命力。所以无论你是维护一个老项目还是开启一个适合服务端渲染的新项目花点时间了解Thymeleaf都是一笔不错的投资。2. Thymeleaf核心设计哲学与工作原理拆解2.1 “自然模板”是如何实现的Thymeleaf的核心卖点是“自然模板”Natural Templates。这听起来有点玄乎但原理其实很直观。我们来看一段代码!-- 这是一个标准的Thymeleaf模板片段 -- p欢迎您span th:text${user.name}访客/span/p当这个文件被设计师用浏览器直接打开时浏览器不认识th:text这个属性它会将其忽略并显示标签内的静态文本“访客”。于是设计师看到的是“欢迎您访客”。而当这个文件通过Thymeleaf模板引擎在服务器端处理时引擎会识别th:text属性用模型Model中user.name变量的值比如“张三”替换掉整个span标签的内容。最终发送给浏览器的是“欢迎您张三”。这种“优雅降级”的能力实现了视图原型和最终成品的高度统一极大地提升了前后端协作效率。它所有的属性都以前缀开头默认是th:所以不会污染HTML标准。这种设计使得模板文件本身就是合法的HTML5文件可以被编辑器校验、被浏览器渲染符合现代开发工具链的习惯。2.2 模板引擎的三大核心要素理解任何一个模板引擎都可以从三个核心要素入手模板、数据模型和引擎处理器。Thymeleaf也不例外。模板Template就是那些包含th:*属性的HTML文件。Thymeleaf支持多种模板模式最常用的是HTML模式。它不仅仅是简单的变量替换而是包含了一整套完整的语法能处理条件判断th:if、循环th:each、片段包含th:replace、链接处理{}等复杂逻辑。数据模型Context在Spring MVC中这通常就是我们放在Model、ModelMap或ModelAndView里的那些键值对。在Thymeleaf的语境里它被封装成一个IContext对象常用实现是WebContext或Context。模板中所有${...}表达式要获取的变量都来自于这个上下文Context。例如控制器中model.addAttribute(user, userObj)模板中就能用${user.name}来访问。引擎处理器TemplateEngine这是大脑。SpringTemplateEngine是Spring生态中的标配。它的工作流程可以简化为解析读取模板文件根据模板模式如HTML创建对应的解析器将模板解析成一棵抽象语法树AST。处理遍历这棵树识别所有th:*属性处理器。每个处理器如TextTagProcessor对应th:text负责执行自己的逻辑计算表达式、访问数据模型、操作DOM等。渲染将处理后的、纯净的HTML DOM树序列化为字符串也就是最终的HTML响应输出。这个过程是完全在服务器端同步完成的所以Thymeleaf天生适合服务端渲染SSR。对于“thymeleaf生成pdf页码”这类需求通常的路径是先用Thymeleaf渲染出完整的HTML字符串再使用像Flying Saucer这类基于iText的HTML转PDF库将HTML转换为带页码、页眉页脚的PDF文档。Thymeleaf在这里扮演了生成高质量、带样式的HTML内容的角色。3. 基础环境搭建与核心语法精讲3.1 在Spring Boot中快速集成现在几乎所有的Java Web项目都基于Spring Boot集成Thymeleaf简单到令人发指。在你的pom.xml中只需要引入一个starter依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency引入之后Spring Boot的自动配置就已经为你做好了一切默认模板位置classpath:/templates/默认模板后缀.html自动配置好了SpringTemplateEngine、ThymeleafViewResolver等组件。你唯一需要做的就是创建控制器和模板文件。创建一个控制器Controller public class HelloController { GetMapping(/hello) public String hello(Model model) { model.addAttribute(message, Hello, Thymeleaf!); model.addAttribute(currentTime, LocalDateTime.now()); return hello; // 对应 templates/hello.html } }然后在src/main/resources/templates/下创建hello.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title入门示例/title /head body h1 th:text${message}默认标题/h1 p当前时间是span th:text${#temporals.format(currentTime, yyyy-MM-dd HH:mm:ss)}2023-01-01 12:00:00/span/p /body /html启动应用访问/hello你就会看到动态渲染的页面。注意文件开头的xmlns:th声明它虽然不是HTML5必须的但能让IDE更好地提供语法高亮和提示建议加上。3.2 表达式语法不仅仅是${...}Thymeleaf的表达式语言Thymeleaf Standard Expression Language非常强大主要有五种类型变量表达式${...}最常用用于访问上下文中的变量和属性。它支持OGNLObject-Graph Navigation Language和Spring EL因此可以嵌套访问。p用户名${user.name}/p p公司地址${user.company.address.city}/p !-- 调用方法 -- p姓名大写${user.name.toUpperCase()}/p选择变量表达式*{...}通常与th:object绑定使用用于简化对选定对象的访问。div th:object${user} p姓名*{name}/p !-- 等同于 ${user.name} -- p邮箱*{email}/p /div注意*{...}的作用域仅限于被th:object包裹的标签及其子标签。在这个区域外使用会报错。这在表单回显时特别有用。消息表达式#{...}用于国际化i18n。它会从消息源如.properties文件中根据key获取对应的文本。h1 th:text#{page.home.title}首页/h1链接表达式{...}用于构建URL是Thymeleaf的一大亮点。它能自动处理上下文路径context path并且与th:href、th:src、th:action等属性完美配合。!-- 生成 /app/user/list -- a th:href{/user/list}用户列表/a !-- 生成 /app/user/profile?id1 -- a th:href{/user/profile(id${userId})}用户档案/a !-- 生成 /app/static/css/style.css -- link th:href{/static/css/style.css} relstylesheet使用{...}后你再也不用担心应用部署路径改变导致的链接失效问题。片段表达式~{...}用于引入模板片段是实现“thymeleaf多页面布局”和代码复用的关键我们会在后面详细讲解。3.3 常用属性处理器实战属性处理器是th:*属性的执行者。掌握以下几个就能应对80%的场景。th:text与th:utext文本替换。th:text会对内容进行HTML转义防止XSS攻击。th:utext“un-escaped text”则不会转义直接输出原始HTML除非你非常确定内容安全否则慎用。p th:text${htmlContent}默认文本/p !-- 输出strong加粗/strong -- p th:utext${htmlContent}默认文本/p !-- 输出strong加粗/strong --th:each循环迭代。状态变量stat提供了很多有用信息。ul li th:eachitem, stat : ${items} th:text|${stat.index 1}. ${item.name}| 项目示例 /li /ulstat对象包含index从0开始、count从1开始、size、current、even/odd等属性。th:if与th:unless条件渲染。判断依据是表达式的布尔值。Thymeleaf对“假”的判断很宽松null、false、0、false、off、no、空字符串、空集合、空数组等都被视为false。div th:if${user ! null}用户已登录/div div th:unless${user.isAdmin}非管理员视图/div div th:if${#lists.isEmpty(items)}列表为空/divth:switch与th:case多条件选择。div th:switch${user.role} p th:caseadmin管理员界面/p p th:caseuser普通用户界面/p p th:case*未知角色/p !-- * 是默认case -- /divth:href,th:src,th:action与链接表达式{...}结合动态设置资源路径。img th:src{/images/logo.png} altLogo form th:action{/user/save} methodpost ... /formth:object与th:field表单数据绑定和回显的黄金搭档。th:object指定表单绑定的对象th:field绑定对象的具体属性它能自动生成id、name、value并处理复选框、单选框的选中状态。form th:action{/user/save} th:object${user} methodpost input typetext th:field*{name} / input typeemail th:field*{email} / !-- 对于单选框 -- input typeradio th:field*{gender} valueM / 男 input typeradio th:field*{gender} valueF / 女 /form提交后如果验证失败控制器返回同一个视图th:field会自动将提交的值和错误信息回显到表单中这是开发CRUD功能时极大的便利。4. 高级特性与项目实战应用4.1 布局与模板复用告别重复代码当你的网站有统一的页头、导航栏、页脚时为每个页面复制粘贴这些代码是维护的噩梦。Thymeleaf提供了强大的布局功能主要通过th:fragment、th:replace、th:insert和th:include3.x版本已废弃th:include建议用replace/insert来实现。1. 定义片段Fragment 在/templates/layout目录下创建header.html、footer.html或者在一个layout.html中定义多个片段。!-- /templates/layout/common.html -- !DOCTYPE html html head th:fragmentcommon_head(title) meta charsetUTF-8 title th:text${title}默认标题/title link relstylesheet th:href{/css/main.css} /head body header th:fragmentcommon_header nav.../nav /header footer th:fragmentcommon_footer p© 2023 我的公司/p /footer /body /html2. 引入片段 在具体页面中使用th:replace或th:insert引入片段。replace会用片段完全替换当前标签insert则会将片段插入当前标签内部。!-- /templates/page/index.html -- html head th:replacelayout/common :: common_head(首页) !-- 这里的原始内容会被 common_head 片段完全替换 -- /head body div th:replacelayout/common :: common_header/div main h1首页内容/h1 /main div th:insertlayout/common :: common_footer !-- common_footer 片段会插入到这个div内部 -- /div /body /html3. 参数化片段 片段可以接收参数使其更加灵活。如上例中common_head(title)。!-- 在另一个页面 -- head th:replacelayout/common :: common_head(用户管理)/head这就是实现“thymeleaf多页面布局”的核心。通过合理的片段划分你可以像搭积木一样构建页面极大提升代码复用率和可维护性。4.2 内联与文本模板模式有时我们需要在JavaScript或CSS中使用Thymeleaf表达式但th:*属性在script或style标签内无效。这时就需要内联Inlining。JavaScript内联使用th:inlinejavascript。script th:inlinejavascript var userId [[${user.id}]]; var userName /*[[${user.name}]]*/ 默认用户名; console.log(用户${userName}, ID: ${userId}); /script[[...]]是转义的输出/*[[...]]*/的注释语法可以在静态打开时提供一个可读的默认值。CSS内联使用th:inlinetext。这在需要动态生成样式时有用。style th:inlinetext .user-avatar { background-image: url([[{/avatar/ user.avatarUrl}]]); } .priority-[[${task.priority}]] { color: red; } /style文本模板模式是另一个强大的特性。Thymeleaf不仅可以渲染HTML还可以渲染纯文本、JavaScript、CSS甚至XML。通过配置不同的TemplateMode你可以用Thymeleaf来生成电子邮件正文、配置文件、代码等。例如生成一封文本邮件Context context new Context(); context.setVariable(userName, 张三); String text templateEngine.process(email/welcome.txt, context);模板文件welcome.txt可以这样写亲爱的 [[${userName}]] 欢迎注册我们的服务这比用字符串拼接生成动态文本要优雅和强大得多。4.3 与Spring深度集成表单验证与国际化Thymeleaf与Spring的集成是天衣无缝的尤其是在处理表单和国际化方面。表单验证与错误显示 Spring MVC的BindingResult对象包含了表单验证的错误信息。Thymeleaf可以方便地访问并展示它们。form th:action{/user/save} th:object${user} methodpost input typetext th:field*{name} / !-- 显示name字段的错误 -- small th:if${#fields.hasErrors(name)} th:errors*{name} classerror错误信息/small input typeemail th:field*{email} / small th:if${#fields.hasErrors(email)} th:errors*{email}/small button typesubmit提交/button /form#fields.hasErrors(fieldName)用于判断特定字段是否有错th:errors*{fieldName}则直接输出该字段的所有错误信息默认会以br/分隔。国际化i18n Spring Boot默认会从classpath:/messages.properties及其语言变体如messages_zh_CN.properties加载消息源。Thymeleaf通过#{...}表达式直接使用。创建messages.propertieswelcome.messageHello, {0}! page.titleUser Profile在模板中使用h1 th:text#{page.title}Title/h1 p th:text#{welcome.message(${user.name})}Hello, User!/p通过#{}表达式Thymeleaf会自动根据当前请求的Locale通常通过Accept-Language头或Session设定选择对应的语言文件。5. 性能调优、常见问题与排查实录5.1 缓存策略与性能考量Thymeleaf默认会缓存已解析的模板这对于生产环境是至关重要的性能优化可以避免每次请求都重新解析模板文件。但在开发阶段这会导致你修改了模板文件后需要重启应用才能看到变化这显然是不可接受的。开发环境关闭缓存 在application.properties或application.yml中配置# application.properties spring.thymeleaf.cachefalse# application.yml spring: thymeleaf: cache: false我个人的习惯是在开发环境的配置文件中显式地设置为false在生产环境配置文件中设置为true或默认不写因为默认就是true。模板解析优化 对于非常复杂的页面模板解析本身可能成为瓶颈。虽然不常见但如果你遇到性能问题可以考虑检查模板中是否有多余的、复杂的表达式计算。避免在模板中进行大量的数据转换或格式化操作尽量在控制器或服务层处理好。使用th:block作为逻辑块容器而不是滥用div因为th:block不会渲染成实际的HTML标签可以减少输出体积。5.2 高频问题排查手册在实际开发中你肯定会遇到下面这些问题。这里我整理了一份速查表问题现象可能原因解决方案页面显示空白或th:*属性原样输出1. 模板文件不在默认的classpath:/templates/目录下。2. 控制器返回的视图名与模板文件名不匹配注意后缀。3. 没有引入Thymeleaf依赖或依赖冲突。1. 检查文件路径。Spring Boot默认找templates/下的.html文件。2. 控制器return viewName对应templates/viewName.html。3. 检查pom.xml运行mvn dependency:tree查看是否有其他模板引擎冲突。表达式${...}不生效显示为字符串1. 变量未放入Model。2. 变量名拼写错误。3. 在th:object块内错误使用了${}应使用*{}。1. 确认控制器中使用了model.addAttribute()。2. 仔细核对变量名大小写。3. 在th:object范围内访问该对象的属性应使用*{property}。静态资源CSS/JS/图片404链接没有使用Thymeleaf的{}表达式或者静态资源目录配置不对。1.始终使用th:href{/path/to/resource}或th:src{...}。2. Spring Boot默认静态资源目录是classpath:/static/、/public/等确保资源文件放在这些目录下。th:field回显失败或绑定错误1. 表单提交后返回的视图没有重新放入包含BindingResult的命令对象ModelAttribute。2. 对象属性没有正确的getter/setter方法。3.th:field的值表达式写错。1. POST处理方法处理完验证后无论是成功还是失败返回视图前都需要model.addAttribute(formObject, updatedObject)。2. 确认你的Java Bean是符合规范的POJO。3.th:field的值必须是*{...}表达式且指向th:object的属性。布局th:replace不生效1. 片段路径写错。2. 片段名称写错。3. 被引入的片段文件本身有语法错误。1. 路径是相对于模板解析器的通常是templates/。layout/common :: header表示templates/layout/common.html文件中的header片段。2. 检查th:fragment定义的名字。3. 先确保片段文件能独立渲染无误。中文乱码1. 模板文件本身保存的编码不是UTF-8。2. 没有设置正确的CharacterEncodingFilter。1. 将IDE和文件编码统一设置为UTF-8。2. Spring Boot通常自动配置好了。如果不行检查是否在application.properties中设置了spring.thymeleaf.encodingUTF-8和spring.http.encoding.charsetUTF-8。5.3 自定义方言与扩展虽然Thymeleaf内置的功能已经非常强大但有时你需要为特定项目创建一些自定义的处理器或表达式工具。这时就需要了解它的扩展机制——方言Dialect。例如公司内部有一个常用的工具类StringUtils你想在模板中直接调用它的方法。你可以创建一个自定义方言将工具类注册为表达式工具对象。public class MyUtilsDialect extends AbstractDialect { Override public String getName() { return MyUtils; } Override public SetIExpressionObjectFactory getExpressionObjectFactories() { SetIExpressionObjectFactory factories new HashSet(); factories.add(new IExpressionObjectFactory() { Override public SetString getAllExpressionObjectNames() { return Collections.singleton(myUtils); } Override public Object buildObject(IExpressionContext context, String expressionObjectName) { return new MyStringUtils(); // 你的工具类实例 } Override public boolean isCacheable(String expressionObjectName) { return true; } }); return factories; } }然后在模板中就可以这样使用${#myUtils.someMethod(...)}。不过在大多数情况下更简单的做法是直接将工具类实例作为变量放入Model或者使用Spring的Component注解将其注入然后在控制器中传给Model。自定义方言更适合封装一组紧密相关、且需要在多个模板中频繁使用的复杂功能。踩过几次坑之后我的体会是Thymeleaf的学习曲线前期平缓但想用得精深必须理解其“自然模板”的哲学和与Spring深度集成的特性。把th:*属性当作给静态HTML添加的“动态指令”而不是一门新的编程语言心态会平和很多。对于“thymeleaf生成pdf页码”这类需求记住Thymeleaf只负责生成完美的HTML剩下的交给专业的PDF渲染库如Flying Saucer各司其职才能高效可靠。

相关新闻