Thymeleaf模板引擎:从自然模板到PDF生成的全栈实践指南 1. 项目概述为什么我们需要Thymeleaf如果你做过Java Web开发尤其是用过Spring Boot那你肯定对JSP、Freemarker这些模板引擎不陌生。但不知道你有没有这种感觉用JSP写页面HTML里混着一堆% ... %的脚本片段前端设计师一看就头疼想改个样式都无从下手用Freemarker呢语法是强大了但那些#if #list标签对不熟悉后端的前端同学来说又像在看天书。项目前后端联调时经常因为一个数据没渲染出来就得后端开发盯着浏览器的“一片空白”去调试效率很低。我自己在带团队做项目时就深受其扰。直到遇到了Thymeleaf情况才彻底改变。简单来说Thymeleaf是一个用于Web和独立环境的现代服务器端Java模板引擎。它的核心理念是“自然模板”——模板文件本身就是有效的HTML5可以直接在浏览器里打开和预览。那些动态逻辑通过一些以th:开头的属性比如th:text,th:if来声明。当模板在浏览器中静态打开时这些属性会被浏览器忽略显示的是你预先写好的静态原型数据当模板被后端引擎处理时这些属性才会生效替换成真实的动态数据。举个例子一个显示用户名的标签在Thymeleaf里你会这样写p th:text${user.name}静态的示例用户名如张三/p前端同学做样式时直接在浏览器打开这个HTML文件看到的就是“张三”布局、样式一目了然。后端开发运行时th:text属性会从模型里取出真实的user.name替换进去。这种“双向可读”的特性极大地改善了前后端协作的体验。结合最新的网络热词来看Thymeleaf的应用早已不局限于简单的页面渲染像利用flying saucer库将Thymeleaf模板转换为PDF并处理复杂的生成pdf页码需求、实现优雅的多页面布局、乃至成为许多初学者在菜鸟教程上的首选学习技术都证明了它的强大和流行。这篇文章我就从一个老开发的角度带你彻底搞懂Thymeleaf从核心概念到实际踩坑让你不仅能“会用”更能“用好”。2. Thymeleaf核心设计思想与架构解析2.1 “自然模板”哲学不仅仅是语法糖很多教程会把Thymeleaf的“自然模板”特性一笔带过认为这不过是个方便预览的小技巧。但在我看来这是Thymeleaf最革命性的设计它背后是对Web开发流程的深刻理解。在传统模板引擎中模板文件是一个“中间产物”。它既不是纯粹的前端文件因为包含了后端语法也不是纯粹的后端文件主体是HTML。这导致了一个尴尬的局面前端工具链如ESLint、样式预处理器难以直接处理它而后端工具又只关心其中的逻辑片段。Thymeleaf通过将动态逻辑全部封装在HTML标准属性以th:为命名空间里完美解决了这个问题。模板文件首先是一个合法的HTML文件可以享受所有前端生态工具的支持。th:*属性遵循HTML5的自定义数据属性规范不会破坏其有效性。这种设计带来了几个实实在在的好处原型设计独立UI/UX设计师可以使用纯HTML/CSS/JS工具如Figma、Sketch产出高保真原型开发人员直接在此基础上添加th:属性即可无需重写结构。静态测试与调试你可以在不启动后端服务器的情况下在浏览器中完整地浏览整个应用的“静态版本”验证所有交互流程和样式。这对于复杂表单、多步骤向导的UI验证至关重要。优雅降级th:*属性中提供的静态值如上例中的“张三”可以视为一种优雅降级或默认内容。在某些极端情况下即使模板引擎处理出错用户也能看到一些有意义的提示而不是一片空白或满屏的错误代码。2.2 模块化架构方言、处理器与模板缓存Thymeleaf的架构非常清晰理解它有助于你应对更复杂的需求和进行定制化开发。其核心是方言Dialect和处理器Processor。方言这是一组特性的集合包括处理器、表达式对象、表达式工具对象等。我们最常用的是标准方言Standard Dialect它提供了th:text、th:each、th:if等我们熟知的所有属性。但Thymeleaf的强大之处在于它的可扩展性。你可以创建自己的方言或者集成第三方方言。例如Spring Security就提供了Thymeleaf的集成方言让你能在模板里使用sec:authorize这样的属性来做权限控制。处理器这是真正干活的组件。每个th:*属性都对应一个处理器。当模板引擎解析到th:text时就会调用对应的TextTagProcessor来处理。处理器负责计算属性值中的表达式如${user.name}然后将结果写入到输出中。模板解析器与模板缓存这是性能的关键。TemplateResolver负责根据模板名称如index找到对应的模板文件如/templates/index.html。为了提高性能Thymeleaf默认会缓存解析后的模板。在开发阶段这很烦人因为每次修改都要重启。所以Spring Boot的Thymeleaf自动配置在开发模式下spring.thymeleaf.cachefalse会关闭缓存。但在生产环境务必开启缓存这对性能有数量级的提升。注意自定义处理器或方言是一个高级话题大多数业务场景用标准方言完全足够。但在构建公司内部的基础组件或与特定框架深度集成时这个能力会非常有用。2.3 表达式语法OGNL与Spring EL的威力Thymeleaf的表达式语言是其灵活性的源泉它主要支持两种OGNLObject-Graph Navigation Language和Spring ELExpression Language。在Spring Boot项目中默认使用的是更强大的Spring EL。表达式主要用在几种场景里变量表达式${...}用于访问模型Model中的属性或请求属性、会话属性等。这是最常用的表达式。span th:text${session.user.name}用户名/span选择变量表达式*{...}用于在由th:object绑定的表单对象内部进行选择可以简化表单字段的书写。form th:object${user} input typetext th:field*{name} / !-- 等价于 th:field${user.name} -- /form消息表达式#{...}用于国际化i18n从消息源如.properties文件中获取文本。h1 th:text#{welcome.message}Welcome!/h1链接表达式{...}用于生成符合上下文路径的URL绝对避免硬编码。这是Thymeleaf处理URL的推荐方式它能自动帮你处理应用上下文Context Path。a th:href{/user/list}用户列表/a !-- 如果应用部署在 /myapp 下会生成 a href/myapp/user/list --片段表达式~{...}用于引入模板片段Fragment是实现页面布局和组件复用的关键我们会在布局章节详细讲。理解这些表达式的计算上下文很重要。${}不仅可以访问Model里的数据还可以访问一系列内置的工具对象我们称之为表达式工具对象Expression Utility Objects。例如#dates用于格式化日期。${#dates.format(user.birthday, yyyy-MM-dd)}#strings用于字符串操作。${#strings.toUpperCase(user.name)}#lists,#sets,#maps用于集合操作。#ctx模板执行的上下文信息。这些工具对象极大地减少了我们在后端Controller里做数据格式预处理的工作让模板能处理更复杂的展示逻辑。3. 从零开始Spring Boot集成与基础使用详解3.1 环境搭建与自动配置奥秘现在几乎所有的Thymeleaf项目都基于Spring Boot它的自动配置让集成变得极其简单。在你的pom.xml中只需要引入一个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency引入之后Spring Boot就会为你自动配置好一个SpringTemplateEngine、一个TemplateResolver以及相关的视图解析器。默认的配置约定如下模板位置classpath:/templates/模板后缀.html编码UTF-8模式HTML5缓存生产环境开启true开发环境关闭false通过spring.thymeleaf.cache属性控制你可以在application.properties或application.yml中覆盖这些配置# 开发时关闭缓存修改模板后立即生效 spring.thymeleaf.cachefalse # 修改模板文件的前缀和后缀一般不推荐改 # spring.thymeleaf.prefixclasspath:/views/ # spring.thymeleaf.suffix.htm # 设置模板模式为严格的HTML5 spring.thymeleaf.modeHTML5这里有个实操心得我强烈建议在团队项目中保持默认的/templates/目录和.html后缀。这是一种约定优于配置的实践能让新成员快速上手也避免了因配置不一致导致的部署问题。3.2 第一个控制器与模板数据传递的细节让我们创建一个最简单的例子。首先是一个ControllerController public class HelloController { GetMapping(/hello) public String hello(Model model) { // 向模型中添加数据 model.addAttribute(message, Hello, Thymeleaf!); model.addAttribute(currentTime, LocalDateTime.now()); model.addAttribute(userList, Arrays.asList(Alice, Bob, Charlie)); // 返回逻辑视图名对应 /templates/hello.html return hello; } }然后是模板文件/templates/hello.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 titleThymeleaf入门/title /head body !-- 使用变量表达式显示文本 -- h1 th:text${message}这里是静态的标题/h1 !-- 使用工具对象格式化日期 -- p当前时间span th:text${#dates.format(currentTime, yyyy-MM-dd HH:mm:ss)}2023-10-01 12:00:00/span/p !-- 使用 th:each 进行迭代 -- ul li th:eachuser : ${userList} th:text${user}静态用户项/li /ul !-- 条件判断 th:if / th:unless -- div th:if${not #lists.isEmpty(userList)} p用户列表不为空。/p /div div th:unless${#lists.isEmpty(userList)} p这段文字也会显示因为条件与th:if相反。/p /div !-- 使用链接表达式生成URL -- a th:href{/home}返回首页/a /body /html访问http://localhost:8080/hello你就会看到动态渲染的页面。这里有几个关键点命名空间声明html xmlns:thhttp://www.thymeleaf.org这行声明了th:命名空间让IDE能提供语法高亮和自动补全如IntelliJ IDEA的Thymeleaf插件。th:text与转义th:text属性会对其值进行HTML转义。这意味着如果message的值是scriptalert(xss)/script它会被转义成普通文本显示从而防止XSS攻击。如果你确信内容是安全的HTML并需要渲染请使用th:utextu代表unescaped。th:each的状态变量迭代时Thymeleaf会提供一个状态变量默认名称为迭代变量名 Stat例如userStat。它包含了一些有用的属性li th:eachuser, iterStat : ${userList} 索引span th:text${iterStat.index}0/span, 序号span th:text${iterStat.count}1/span, 用户span th:text${user}Alice/span /liindex从0开始count从1开始这在生成表格行号时非常方便。3.3 表单处理与数据绑定th:object与th:field的最佳实践表单是Web应用中最常见的交互。Thymeleaf与Spring MVC的数据绑定结合得天衣无缝。后端ControllerController RequestMapping(/user) public class UserController { GetMapping(/edit) public String editForm(RequestParam Long id, Model model) { // 模拟从数据库获取用户 User user userService.findById(id); model.addAttribute(user, user); return user/edit; } PostMapping(/save) public String saveUser(ModelAttribute User user, BindingResult result) { if (result.hasErrors()) { return user/edit; // 返回表单页显示错误 } userService.save(user); return redirect:/user/list; // 重定向到列表页防止重复提交 } }前端模板/templates/user/edit.htmlform th:action{/user/save} th:object${user} methodpost !-- 隐藏域存放ID -- input typehidden th:field*{id} / div label forname姓名/label !-- th:field 会自动绑定 id, name, value 属性 -- input typetext idname th:field*{name} / !-- 显示字段错误信息 -- span th:if${#fields.hasErrors(name)} th:errors*{name} classerror/span /div div label foremail邮箱/label input typeemail idemail th:field*{email} / span th:if${#fields.hasErrors(email)} th:errors*{email} classerror/span /div div label性别/label !-- 单选框th:field 同样适用 -- input typeradio th:field*{gender} valueMALE / 男 input typeradio th:field*{gender} valueFEMALE / 女 /div div label forhobbies爱好多选/label !-- 多选框需要绑定到一个集合字段上 -- input typecheckbox th:field*{hobbies} valueREADING / 阅读 input typecheckbox th:field*{hobbies} valueMUSIC / 音乐 input typecheckbox th:field*{hobbies} valueSPORT / 运动 /div button typesubmit保存/button /form这里面的门道很多th:object与*{...}th:object${user}将表单绑定到模型中的user对象。之后在表单内部就可以使用选择表达式*{...}来访问该对象的属性它等价于${user.xxx}但写起来更简洁且与绑定上下文紧密关联。th:field的魔法这是Thymeleaf表单处理的核心。th:field*{name}会做三件事设置name属性为name对应后端接收的参数名。设置id属性为name如果未显式指定id。设置value属性为user.getName()的当前值。对于单选框和复选框它还会根据绑定值自动添加checked属性。错误显示#fields.hasErrors()和th:errors用于显示Spring MVC验证框架如Valid产生的错误信息。*{name}表示显示user.name字段的错误。防止重复提交处理POST请求后务必使用redirect:进行重定向即PRG模式Post-Redirect-Get。这能避免用户刷新页面时重复提交表单。踩坑记录th:field在绑定复选框checkbox到集合时要求后端对象的对应字段必须是Collection类型如ListString并且需要正确初始化如new ArrayList()。如果字段是String[]或者未初始化可能会绑定失败。这是新手常踩的一个坑。4. 高级特性与工程化实践4.1 布局与模板片段告别重复的HTML代码当网站有多个页面时页头、页脚、导航栏这些公共部分如果每个页面都复制一遍维护起来将是噩梦。Thymeleaf提供了两种强大的代码复用机制片段Fragment和布局Layout。1. 使用th:fragment和th:replace/th:insert这是最基础、最灵活的片段复用方式。首先我们创建一个公共的片段文件比如/templates/fragments/header.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org body !-- 定义一个名为 “main-header” 的片段 -- header th:fragmentmain-header nav a th:href{/}首页/a a th:href{/about}关于/a span th:text${#authentication.name} th:if${#authorization.expression(isAuthenticated())}用户名/span /nav /header !-- 可以定义多个片段 -- footer th:fragmentmain-footer pcopy; 2023 我的公司/p /footer /body /html然后在其他页面中引入这些片段!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head.../head body !-- 用 th:replace 替换当前标签为引入的片段 -- div th:replace~{fragments/header :: main-header} 这里的内容会被完全替换成 header 片段 /div main h1这是主页内容/h1 /main !-- 用 th:insert 将片段插入到当前标签内部 -- footer th:insert~{fragments/header :: main-footer} !-- 片段会插入到这里面 -- /footer /body /htmlth:replace和th:insert的区别在于replace会用片段替换掉宿主标签本身而insert会将片段插入到宿主标签的内部。根据W3C标准footer标签通常期望内部有内容所以这里用insert更合适。2. 使用布局方言实现真正的页面继承片段引入解决了代码复用但对于整个页面的骨架布局如html结构、head中的公共CSS/JS我们希望能有一个“父模板”。这需要借助Thymeleaf的布局方言。它不是标准方言的一部分需要额外引入依赖dependency groupIdnz.net.ultraq.thymeleaf/groupId artifactIdthymeleaf-layout-dialect/artifactId /dependencySpring Boot的Thymeleaf starter会自动配置它。使用起来非常直观布局文件/templates/layout/base.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org xmlns:layouthttp://www.ultraq.net.nz/thymeleaf/layout head title layout:title-pattern$CONTENT_TITLE - $LAYOUT_TITLE默认标题/title link relstylesheet th:href{/css/main.css} script layout:fragmenthead-scripts/script /head body header h1我的网站/h1 /header div classcontainer !-- 这里将被具体页面的内容替换 -- section layout:fragmentcontent p这是布局中的默认内容如果页面没有覆盖则显示这个。/p /section /div footer p页脚信息/p /footer !-- 公共JS放在这里 -- script th:src{/js/common.js}/script script layout:fragmentfooter-scripts/script /body /html具体页面/templates/home/index.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org xmlns:layouthttp://www.ultraq.net.nz/thymeleaf/layout layout:decorate~{layout/base} !-- 声明继承自哪个布局文件 -- head title主页/title !-- 可以往布局的 head-scripts 片段中添加本页特定的CSS或JS -- script layout:fragmenthead-scripts console.log(主页特定的脚本); /script /head body !-- 覆盖布局中名为 content 的片段 -- section layout:fragmentcontent h1欢迎来到主页/h1 p这是主页的独特内容。/p /section !-- 覆盖布局中名为 footer-scripts 的片段 -- script layout:fragmentfooter-scripts console.log(主页页脚特定的脚本); /script /body /html最终渲染时index.html中的content片段会替换掉base.html中定义的content片段其他部分则继承自布局。这种方式实现了真正的模板继承结构清晰维护方便是构建中大型项目的首选。4.2 国际化与消息处理对于多语言应用Thymeleaf与Spring的MessageSource无缝集成。首先在src/main/resources下创建消息属性文件messages.properties(默认如英文)messages_zh_CN.properties(简体中文)在中文属性文件中welcome.message欢迎{0} page.title用户管理 button.submit提交在模板中使用#{...}表达式和th:text属性来引用h1 th:text#{welcome.message(${user.name})}Welcome!/h1 title th:text#{page.title}Default Title/title button typesubmit th:text#{button.submit}Submit/button#{welcome.message(${user.name})}中的{0}是一个占位符会被表达式中的参数这里是user.name替换。Spring Boot会自动根据当前请求的Locale通常通过Accept-Language请求头或会话设置来选择合适的消息文件。4.3 生成PDF与Flying Saucer集成这是近期的一个热门需求将Thymeleaf渲染的HTML页面直接转换为PDF用于生成报告、合同、账单等。Flying Saucer是一个基于iText的Java库专门用于将CSS格式的XHTML渲染成PDF。结合Thymeleaf流程非常清晰引入依赖dependency groupIdorg.xhtmlrenderer/groupId artifactIdflying-saucer-pdf/artifactId version9.1.22/version !-- 使用最新稳定版 -- /dependency dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version !-- Flying Saucer 通常与 iText 5.x 兼容 -- /dependency编写一个专用于PDF的Thymeleaf模板(/templates/pdf/report.html)!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8/ style /* PDF需要内联样式或base64嵌入的字体不支持外部CSS链接 */ body { font-family: SimSun; font-size: 12pt; } h1 { color: #333; } .header { text-align: center; } .footer { position: fixed; bottom: 0; width: 100%; text-align: center; font-size: 10pt; } /* 处理分页和页码 */ .page-break { page-break-after: always; } /style /head body div classheader h1 th:text${reportTitle}报告标题/h1 /div div th:eachitem, stat : ${items} p th:text|${stat.count}. ${item.name}|项目内容/p !-- 在特定位置分页 -- div th:if${stat.count % 20 0} classpage-break/div /div !-- 使用 flying saucer 的 CSS 属性生成页码 -- div classfooter 第 span classpageNumber/span 页 / 共 span classtotalPages/span 页 /div /body /html注意PDF渲染对CSS支持有限尤其是布局和定位。position: fixed用于页眉页脚page-break-after: always用于强制分页。页码通过Flying Saucer的特定CSS伪元素生成但更复杂的页码逻辑如“第X页共Y页”通常需要在Java代码中通过iText API后处理。编写服务类将模板渲染为HTML再转换为PDFService public class PdfGenerationService { Autowired private TemplateEngine templateEngine; public byte[] generatePdfReport(String templateName, MapString, Object data) throws IOException, DocumentException { // 1. 使用Thymeleaf渲染HTML字符串 Context context new Context(); context.setVariables(data); String htmlContent templateEngine.process(templateName, context); // 2. 使用Flying Saucer将HTML转换为PDF ByteArrayOutputStream outputStream new ByteArrayOutputStream(); ITextRenderer renderer new ITextRenderer(); // 设置字体解决中文不显示问题关键 ITextFontResolver fontResolver renderer.getFontResolver(); // 添加字体文件需要将字体文件如simsun.ttc放在resources/fonts下 fontResolver.addFont(classpath:/fonts/simsun.ttc, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); renderer.setDocumentFromString(htmlContent); renderer.layout(); renderer.createPDF(outputStream); renderer.finishPDF(); return outputStream.toByteArray(); } }在Controller中调用并返回PDF响应GetMapping(/report/pdf) public void generatePdf(HttpServletResponse response) throws IOException, DocumentException { MapString, Object data new HashMap(); data.put(reportTitle, 销售报告); data.put(items, ...); // 填充数据 byte[] pdfBytes pdfGenerationService.generatePdfReport(pdf/report, data); response.setContentType(application/pdf); response.setHeader(Content-Disposition, inline; filename\report.pdf\); // inline 表示在浏览器中打开 response.setContentLength(pdfBytes.length); response.getOutputStream().write(pdfBytes); response.getOutputStream().flush(); }处理页码的进阶技巧上面模板中使用.pageNumber和.totalPages是Flying Saucer支持的基本CSS内容生成。但对于复杂的页码格式如“第X页/共Y页”它可能不够灵活。一个更可靠的方法是使用iText的PdfPageEventHelper在生成PDF时动态添加页眉页脚。这需要更深入地操作iText API在createPDF前后添加事件监听器来绘制页码。重大避坑指南中文字体这是生成中文PDF最大的坑必须显式添加中文字体文件如宋体simsun.ttc、黑体simhei.ttf并正确配置BaseFont.IDENTITY_H编码。否则PDF中的中文会显示为空白。CSS支持Flying Saucer支持的CSS是CSS 2.1的一个子集且是“打印”媒体类型。Flexbox、Grid等现代布局、外部CSS文件、Web字体可能不被支持或支持很差。尽量使用内联样式和简单布局。资源路径模板中的图片如果使用相对路径或th:src{...}在PDF渲染上下文中可能无法解析。建议将图片转换为Base64编码内联或使用绝对路径classpath:或file:。性能渲染复杂HTML为PDF是CPU密集型操作。在高并发场景下要考虑缓存生成的PDF或使用异步任务避免阻塞请求线程。5. 性能调优、问题排查与最佳实践5.1 模板缓存与开发模式配置如前所述模板缓存是Thymeleaf生产环境性能的基石。但开发时需要关闭。Spring Boot的配置非常智能# application-dev.properties (开发环境) spring.thymeleaf.cachefalse spring.thymeleaf.prefixclasspath:/templates/ # 开启模板文件修改的实时监听需要spring-boot-devtools spring.devtools.restart.enabledtrue # application-prod.properties (生产环境) spring.thymeleaf.cachetrue # 可以配置缓存TTL但通常不需要 # spring.thymeleaf.cache-ttl-ms3600000强烈建议使用Spring Boot DevTools它提供了热重启和静态资源缓存禁用等功能能极大提升开发体验。当spring.thymeleaf.cachefalse时DevTools能确保模板修改后立即生效无需手动重启应用。5.2 常见问题排查实录模板解析错误模板找不到症状返回TemplateInputException: Error resolving template [...]。排查检查Controller返回的视图名是否与模板文件路径匹配默认在/templates/下后缀.html。检查spring.thymeleaf.prefix和suffix配置是否被意外覆盖。确认模板文件是否真的在classpath下打包后位于JAR包的BOOT-INF/classes/templates/。表达式求值错误变量为null或属性不存在症状页面渲染空白或抛出SpelEvaluationException。排查使用th:text${variable}时如果variable为nullThymeleaf默认会渲染空字符串不会报错。但如果表达式更复杂如${user.profile.address.city}中间任何一环为null都会导致异常。使用安全导航操作符?.将表达式改为${user?.profile?.address?.city}。如果中间某部分为null整个表达式会安静地返回null而不是抛出异常。使用th:if或th:unless先判断对象是否存在div th:if${user ! null}...。静态资源无法加载CSS/JS/图片症状页面样式错乱浏览器控制台报404错误。排查Thymeleaf的{}表达式会正确处理上下文路径。确保你的资源放在src/main/resources/static/或src/main/resources/public/目录下。在模板中引用时使用link th:href{/css/style.css} relstylesheet。开头的/表示相对于应用上下文根目录。检查Spring Boot的静态资源处理配置spring.mvc.static-path-pattern,spring.web.resources.static-locations通常不需要改动。表单绑定失败数据回显为空症状提交表单后验证失败返回原页面但之前输入的数据没了。排查Controller方法参数必须用ModelAttribute注解且属性名与表单th:field匹配。确保表单字段的name属性与对象属性名一致th:field会自动处理。如果使用了Valid验证且验证失败必须将BindingResult参数紧跟在ModelAttribute参数后面并且不能重定向必须返回表单视图这样模型中的命令对象包含错误信息和用户输入才会被保留并传递回视图。5.3 最佳实践总结保持模板简洁模板的主要职责是展示。复杂的业务逻辑、数据转换、计算应该放在后端的Service层或Controller中或者使用工具方法。避免在模板中编写过长的表达式或复杂的Java代码片段。充分利用片段和布局这是提高可维护性的关键。将公共的导航、页脚、侧边栏、模态框等抽离成片段。使用布局方言定义页面的整体骨架。始终使用{}生成链接绝对不要硬编码URL。{}能自动处理应用上下文Context Path无论是在根目录部署还是子路径部署都能正常工作。为迭代和条件判断添加注释当模板中有多层嵌套的th:each和th:if时在结束标签处添加HTML注释说明是哪个循环或条件的结束能极大提高可读性。!--/* 结束用户列表循环 */-- /div谨慎使用th:utext除非你完全信任内容的来源例如来自你自己的系统而非用户输入否则不要使用th:utext来渲染未转义的HTML这会导致XSS攻击漏洞。考虑模板的可测试性虽然Thymeleaf模板主要是为了渲染但你可以编写单元测试来验证模板的解析是否成功或者使用TemplateEngine的process方法在测试中渲染模板并断言关键内容。