ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot集成Thymeleaf实战:模板引擎核心语法与性能优化指南

Spring Boot集成Thymeleaf实战:模板引擎核心语法与性能优化指南 1. 从零认识 Thymeleaf为什么是它而不是 JSP 或者 Freemarker很多刚接触 Spring Boot 的朋友第一次看到 Thymeleaf 这个词会觉得有点陌生。它其实是一门模板引擎简单说就是负责把后端的数据和前端页面“拼”到一起的工具。Spring Boot 官方文档里推荐的默认模板引擎就是 Thymeleaf可见它在 Spring 生态里的地位。如果你正在学 Spring Boot迟早要跟它打交道。1.1 模板引擎在生产环境中到底干了什么事拿电商系统举例商品详情页里商品名称、价格、库存这些数据都存在数据库里每次用户打开页面后端都要把这些数据从数据库捞出来再填进 HTML 页面里返回给浏览器。这个“填数据”的过程就是模板引擎的活。在没有模板引擎的年代大家用 JSP 做这件事页面里写一堆% %Java 代码页面和逻辑混在一起改起来相当痛苦。后来出现了 Freemarker、Velocity 这些模板引擎各有各的语法。Thymeleaf 则另辟蹊径它主张“HTML 优先”模板文件本身就是一个合法、可以直接用浏览器打开的 HTML 页面只是在原有 HTML 标签的基础上通过th:开头的属性把动态数据嵌进去。这意味着前端设计师拿着 Thymeleaf 模板不需要启动后端服务直接用浏览器打开就能看到静态效果。后端开发拿到模板把th:属性填上数据动态渲染就完成了。这种“静态原型”和“动态渲染”二合一的特性是 Thymeleaf 跟 JSP 最大的区别也是它在 Spring Boot 时代被官方首选推荐的根本原因。1.2 和 JSP、Freemarker 对比Thymeleaf 的核心优势我用一个表格把这几个模板引擎的差异摆出来这样更直观。对比维度ThymeleafJSPFreemarker页面是否需要容器支持不需要浏览器直接打开即可预览必须运行在 Servlet 容器里比如 Tomcat不需要但模板文件不是标准 HTML语法风格基于 HTML 属性扩展对前端友好嵌入 Java 代码前后端耦合严重自定义指令语法如#if与 Spring Boot 集成度官方默认推荐脚手架直接引入Boot 里基本已被放弃维护支持集成可行但不如 Thymeleaf 顺手学习曲线平缓会 HTML 就能上手早年熟悉但现在新项目很少有人用中等需额外掌握新指令JSP 最大的问题是需要依赖容器才能运行Spring Boot 内置 Tomcat 虽然能跑但打包成可执行 Jar 后对 JSP 的支持是受限的很多同学把项目打成 Jar 包后发现 JSP 页面 404排查半天最后才发现是 JSP 在可执行 Jar 场景下天然不友好。这个坑在 Thymeleaf 上不存在Thymeleaf 的模板文件是放在src/main/resources/templates目录下的资源文件Jar 包一样能加载。配合 Spring Boot 的spring-boot-starter-thymeleaf你只需要在pom.xml里加一个依赖所有配置基本都自动完成不用像 Freemarker 那样手动配置模板路径、编码之类的东西。这也是我推荐新项目直接上 Thymeleaf 的原因。1.3 什么场景适合用 Thymeleaf什么场景不适合现在前后端分离成了主流很多团队前端用 Vue 或 React后端只出接口。这种情况下 Thymeleaf 似乎没有用武之地了但在实际项目中下面这几类场景 Thymeleaf 依然是效率很高的选择。服务端渲染的后台管理系统比如企业内部的工单系统、数据报表系统页面交互没那么复杂用 Thymeleaf 加上 Bootstrap一个人后端开发就能全部搞定不需要额外维护前端工程。需要 SEO 的落地页、门户官网搜索引擎爬虫抓取的是服务端返回的完整 HTML 内容Thymeleaf 这种服务端渲染天然满足要求而 Vue 这类前后端分离的页面对部分搜索引擎不够友好。不想引入 Node.js 构建链路的单体应用很多传统企业项目没条件搭建复杂的前端工程Thymeleaf 直接嵌入 Spring Boot 项目一条链路跑到底部署也省事。当然如果是交互特别复杂的单页应用比如类似在线表格编辑、复杂的拖拽工作台这种建议还是前后端分离Thymeleaf 不适合硬塞进去。选技术方案要分场景没有银弹Thymeleaf 在服务端渲染这个领域里确实是最省心的选择之一。2. Spring Boot 集成 Thymeleaf 的完整实操这块直接带你走一遍集成过程从依赖到配置把每一步的细节和原理都讲清楚。网上很多教程只告诉你“加个依赖就能用”但实际开发中你会遇到版本选择、编码问题、热部署等一系列情况这些我都会展开说。2.1 引入依赖与版本选择Boot 版本决定了 Thymeleaf 版本用 Maven 项目举例你只需要在pom.xml里加这一段dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency这里有个细节Spring Boot 3.x 之前的版本对应的 Thymeleaf 主力版本是 3.0.x而 Spring Boot 3.x 内置的 Thymeleaf 是 3.1.x。3.1 版本最明显的变化是支持了布局组件的新写法还有对 HTML 标准语法的兼容性更好。实际项目里不需要手动指定 Thymeleaf 的版本号Spring Boot 的spring-boot-dependencies会自动管理版本。除非你有特殊需求比如要使用 Thymeleaf 的新特性但没有对应的 Boot 版本支持这时候你才需要手动覆盖版本号。我见过有人强行把 Thymeleaf 升到 3.1 但 Boot 还是 2.7 的运行起来各种兼容性问题完全没有必要。2.2 application.yml 配置参数详解不止是前缀和后缀Spring Boot 的自动配置已经默认了 Thymeleaf 的模板前缀是classpath:/templates/后缀是.html也就是说你放在resources/templates下的index.htmlController 直接返回index字符串就能被解析。但实际项目中往往还需要调整一些其他配置。spring: thymeleaf: prefix: classpath:/templates/ suffix: .html mode: HTML encoding: UTF-8 cache: false servlet: content-type: text/html逐个解释下每个配置项的用意。mode: HTML指定模板解析的格式默认就是 HTML2.x 时代曾经有个 LEGACYHTML5 用来解析非严格 HTML但 Thymeleaf 3.0 之后对 HTML5 支持已经很好不用再额外配置。encoding: UTF-8必须显式声明否则页面中文乱码会让人怀疑人生。cache: false是开发环境下最重要的配置关掉模板缓存才能让修改后的页面即时生效但是生产环境务必改回true否则性能会受到影响。还有一个容易踩坑的点servlet.content-type: text/html控制响应头里的 Content-Type如果页面出现乱码除了改这里的编码还要确认模板文件本身保存时就是 UTF-8 编码。我遇到过一个同事代码和配置都是 UTF-8但页面在 IDE 里默认用 GBK 保存结果渲染出来的中文全是乱码。这种问题从配置层面排查不出来最后把文件重新用 UTF-8 保存才解决。2.3 开发环境热更新配置改完页面不用重启热更新这个问题网上讨论得很多但很多方案配置起来挺麻烦。我用下来最顺手的方案是spring-boot-devtools搭配cache: false不需要额外的 JRebel 或者其他插件。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scoperuntime/scope optionaltrue/optional /dependencydevtools的原理是监听 classpath 下的文件变化一旦有文件被修改就自动重启应用。注意“重启”不是“热加载”它是重新启动容器但比手动重启快很多因为 devtools 用了两个类加载器基础依赖的类不会重新加载只有项目自身的代码变化。实际开发中修改 Thymeleaf 模板文件后只要 IDE 触发文件保存devtools 会检测到变化。但有时修改模板后没有自动触发原因可能是 devtools 默认只监控 classpath 下的变化而模板文件在resources/templates下需要把模板目录加入编译路径。IDEA 里可以用Ctrl Shift F9手动编译当前文件或者按Ctrl F10重新加载应用一般都能解决。注意spring-boot-devtools在生产环境的 jar 包里不会生效它的 Maven 依赖 scope 是 runtime打包时默认会被排除。不过为了保险生产环境构建时还是建议通过配置把 devtools 排除干净避免不必要的干扰。3. Thymeleaf 核心语法实战从文本输出到流程控制语法是 Thymeleaf 的自留地。这一节我会把平时项目里最常用的语法梳理一遍配合具体代码示例每个都给出能直接跑通的用法。掌握了这些常见页面的渲染需求基本都能覆盖。3.1 表达式基础看懂变量、选择、URL 三种表达式的区别Thymeleaf 的表达式主要在th:属性里使用常见的表达方式有三种。变量表达式${...}是最常用的它从上下文中取值。Controller 里往 Model 塞了一个username页面里就写span th:text${username}默认值/span。用户没登录时没有这个值页面会显示标签体里写的“默认值”这就体现了 Thymeleaf 静态原型的能力。选择表达式*{...}跟变量表达式很像区别是它不直接去上下文取值而是从当前对象里取。配合th:object使用div th:object${user} p姓名span th:text*{name}张三/span/p p年龄span th:text*{age}25/span/p /div这里的*{name}等价于${user.name}。选择表达式在表单回填场景下特别方便后面讲到表单的时会详细展开。URL 表达式{...}用来生成链接它最大的好处是自动拼接项目的 Context Path。比如你的应用部署后根路径是/myapp写th:href{/user/list}渲染出来的地址是/myapp/user/list不用在代码里手动拼接。带参数的写法是{/user/detail(id${userId})}它会生成/user/detail?id123。3.2 常用属性标签th:text、th:if、th:each、th:with 一把梭这几组标签是使用频率最高的我把它们的用法和细节整理一下。th:text用于输出纯文本HTML 特殊字符会被自动转义。比如后台存了一段script标签的字符串用th:text输出时会被转成lt;scriptgt;浏览器显示成普通文本不会执行脚本。这个特性对于防止 XSS 注入很有用。如果你确实想输出原始 HTML可以用th:utext但前提是你完全信任这个数据源否则容易踩到安全漏洞。th:if做条件判断条件成立才渲染当前标签div th:if${user.age 18} 你已经成年 /div div th:unless${user.age 18} 你还没成年 /div注意th:unless是th:if的相反判断。还有一种细节th:if判断空字符串时有一定特殊性比如${user.name }在用户名为 null 时结果是 false在字符串为空时结果是 true别把 null 和空字符串混为一谈。th:each是循环遍历支持 List、Map 和数组。遍历集合时可以通过status变量获取下标tr th:eachitem, stat : ${itemList} td th:text${stat.count}1/td td th:text${item.name}默认商品名/td /trstat.count从 1 开始计数stat.index从 0 开始还有stat.first、stat.last来判断首尾元素。用得比较多的是在表格里显示序号或者在最后一个元素后面加分隔线。th:with用来定义局部变量适合在循环体内部复用计算结果的场景div th:eachitem : ${itemList} th:withdiscountPrice${item.price * 0.8} p原价span th:text${item.price}100/span/p p折后价span th:text${discountPrice}80/span/p /divth:with定义的变量只在当前标签及其子标签内有效不会污染外层作用域。3.3 表单绑定与数据回填th:object 搭配 th:field 的魔力表单是后台系统里最常写的页面Thymeleaf 的表单回填机制能帮你省掉大量手动赋值代码。核心是th:object指定绑定的对象th:field自动匹配表单控件的 name 和 value。form th:action{/user/save} th:object${user} methodpost input typehidden th:field*{id} / div label用户名/label input typetext th:field*{username} / /div div label邮箱/label input typetext th:field*{email} / /div div label角色/label select th:field*{role} option th:eachr : ${roles} th:value${r} th:text${r}ADMIN/option /select /div button typesubmit保存/button /form新增用户和编辑用户走同一个表单Controller 里如果传的是一个空的User对象表单就是空白状态如果传的是从数据库查出来的user表单控件会自动回显对应字段值包括单选和下拉框也会自动选中匹配项。这一步看似简单但很多没接触过的同学会手动写各种selectedselected判断又容易漏用th:field就没有这些问题。有一个细节需要注意表单提交后如果校验失败需要回显用户的输入而不是数据库里的值。Spring MVC 的BindingResult可以保留用户提交的值Controller 里把错误信息和对象返回页面即可th:field会自动优先取BindingResult里包装的值这个特性能帮你省不少事。3.4 代码片段复用th:fragment 让公共部分只写一次后台管理系统很常见的情况是每个页面的头部、侧边栏、底部都长一样如果复制粘贴那每次改个菜单要动几十个页面。Thymeleaf 的片段复用能根治这个问题。在common.html里定义公共片段!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org body nav th:fragmentsidebar ul lia th:href{/user/list}用户管理/a/li lia th:href{/order/list}订单管理/a/li /ul /nav /body /html其他页面引入这个片段div th:replace~{common :: sidebar}/div~{common :: sidebar}的意思是“取 common.html 中名为 sidebar 的片段”。还有一种更灵活的做法片段支持参数传递比如菜单的高亮状态可以在片段的表达式里使用传入的参数nav th:fragmentsidebar(currentMenu) a th:class${currentMenu user} ? active : th:href{/user/list}用户管理/a /nav引入时传入参数div th:replace~{common :: sidebar(user)}/div这样每个页面传入自己的菜单标识公共布局里就能根据参数控制高亮不需要每个页面都复制一份修改版代码布局管理非常高效。3.5 内置工具类金额、日期格式化不再手动拼字符串项目里金额和日期格式化是高频需求。Thymeleaf 内置了#numbers、#dates、#temporals等工具类在模板里直接调用就行。p th:text${#numbers.formatDecimal(price, 1, 2)}1,000.00/p这里price是BigDecimal类型1表示整数部分最少位数2是小数位数。最常见的格式化金额参数是#numbers.formatDecimal(price, 1, 2)。日期格式化在 Java 8 之后推荐用#temporalsp th:text${#temporals.format(createTime, yyyy-MM-dd HH:mm:ss)}2024-01-01 12:00:00/pJava 8 的LocalDateTime不能用#dates很多同学踩过这个坑。#dates只支持java.util.Date#temporals才是给LocalDateTime、LocalDate用的。写模板的时候先确认 Controller 塞进来的是哪种类型选错了工具类在渲染时会直接抛异常页面刷出来一串报错排查起来浪费时间。4. Controller 与模板的数据对接从 return 到 Model 的完整链路模板语法只是半边天另外半边是后端怎么把数据送到模板里。这一节专门讲 Controller 层的写法包括视图返回方式、数据传递方式、局部刷新和登录用户的获取。4.1 Model、ModelAndView 到底怎么选Spring MVC 提供了好几种给模板传数据的方式最常用的是在方法参数上加Model或者ModelMap然后在方法里addAttribute。Controller public class UserController { GetMapping(/user/list) public String list(Model model) { ListUser users userService.listAll(); model.addAttribute(userList, users); return user/list; } }这里用Controller而不是RestController因为RestController默认把所有方法返回值当 JSON 处理不会走视图解析器。如果你用RestController想要返回页面得手动返回ModelAndView对象挺别扭的。我见过有人误用了RestController后页面返回的是 JSON 字符串而不是 HTML排查半天才发现是注解用错了。还有ModelAndView的写法GetMapping(/user/detail) public ModelAndView detail(RequestParam Long id) { ModelAndView mv new ModelAndView(user/detail); mv.addObject(user, userService.getById(id)); return mv; }两种写法效果一样ModelAndView是“视图和模型一起返回”Model是“往模型里塞数据后返回视图名”。实际项目中我更习惯用Model方式代码读起来更加直观方法返回字符串也更方便统一处理。4.2 重定向传递参数RedirectAttributes 的用武之地表单提交后最常见操作是重定向到列表页。如果用return redirect:/user/listController 里直接model.addAttribute(message, 保存成功)是传不过去的因为重定向是一次新的请求原来的 Model 数据会丢失。这时候要用RedirectAttributesPostMapping(/user/save) public String save(User user, RedirectAttributes ra) { userService.save(user); ra.addFlashAttribute(message, 保存成功); return redirect:/user/list; }addFlashAttribute存的数据会在重定向后的下一次请求里生效然后自动清除。页面里通过th:text${message}就能取到提示信息。这种机制的原理是 Spring 把数据存到了 Session 里重定向完成后立即移除所以只能在重定向后的第一页拿到不会污染后续请求。4.3 局部刷新与 AjaxThymeleaf 模板片段也能当接口用很多场景下面需要局部刷新比如点击按钮后只更新某一块区域。Thymeleaf 的模板片段不仅能用于页面复用还能配合 AJAX 请求返回局部 HTML。后端 Controller 可以这样写GetMapping(/user/statistics) public String statistics(Model model) { model.addAttribute(userCount, userService.count()); return user/dashboard :: statistics; }注意返回值里带了:: statistics这告诉 Thymeleaf “渲染 user/dashboard 页面里的 statistics 片段而不是整个页面”。前端用 jQuery 或原生 fetch 请求这个地址拿到的就是一段 HTML直接插入到页面的某个容器里就能完成局部刷新。这种做法的好处是不需要引入 JSON 和前端模板渲染的双重维护成本后端生成好 HTML 直接给前端用代码量少对于不复杂的小模块来说效率很高。缺点是后端承担了前端渲染的逻辑适合后台管理这类对交互要求不高的系统SaaS 官网这类对首屏加载速度要求高的场景就不推荐这种模式了。4.4 获取登录用户信息Session 与 Spring Security 结合后台系统在模板里显示当前登录人是很常见的需求。如果用了 Spring Security你可以直接通过#authentication或#authorization表达式访问。p th:text${#authentication.name}未登录/p p th:text${#authentication.principal.realName}张三/p如果没集成安全框架通常的做法是在登录成功后把用户对象塞进 Session然后通过HttpServletRequest或Session属性在模板中获取model.addAttribute(loginUser, session.getAttribute(loginUser));p th:text${session.loginUser.realName}张三/p更简单的写法是直接用${session.loginUser.realName}Thymeleaf 可以直接访问 Session 属性。不过我还是更推荐在 Controller 层显式把需要的字段放进 Model模板里面直接取不要动不动就session.xxx这样代码耦合并低以后如果从 Session 改成 Redis 存储登录态页面不用动。5. 实战中高频踩坑记录与性能优化建议这一节是真正的干货部分。我把项目里遇到过的各种诡异问题、排查思路和优化经验整理出来希望能帮你省下不少排查耗时。这些问题网上零散地有人提过但很少有一篇把它们串起来。5.1 页面 404 或整页下载最经典的三个坑第一个坑Controller 返回的视图名没问题模板文件也在templates目录下但访问时页面 404。排查第一件事是看返回的字符串是否写对了层级路径。比如模板文件在templates/user/list.htmlController 返回的是user/list少写一层目录就找不到。另外看下项目是不是打成 Jar 包运行的如果模板文件没被编译进 Jar 里也会 404。Maven 打包时默认会把resources下的文件打进 Jar如果改了配置排除资源文件就会出问题。第二个坑页面没有渲染浏览器直接下载了一个 HTML 文件。这个问题的原因大概率是RestController用在了本该用Controller的类上。RestController是Controller加ResponseBody的组合所有返回值都会以 JSON 或字符串形式写在响应体里不会走视图解析器。解决方法是把类上的注解改成Controller或者在需要返回 JSON 的接口方法上单独加ResponseBody。第三个坑URL 表达式生成的链接少项目根路径。如果应用部署在 Tomcat 的某个子路径下比如http://ip:8080/myapp页面写的a th:href/user/list生成的是/user/list点击后直接 404正确写法是a th:href{/user/list}{}会自动带上 Context Path。5.2 热更新失效的常见原因与排查路径开发环境热更新失效我总结了三个主要原因。第一spring.thymeleaf.cache没有设为false。Thymeleaf 默认缓存模板解析结果如果你没改缓存配置改了页面后刷新浏览器看到的永远是旧页面。这个问题在配置上加了cache: false就能解决。第二IDEA 没有开启自动编译。devtools 监听的是 classpath 的变化但 idea 默认不会在文件保存后自动编译需要按Ctrl Shift F9手动编译当前文件或者勾选Build project automatically选项。Eclipse 用户一般没这个问题因为 Eclipse 默认保存即编译。第三修改了模板文件但没触发 devtools 重启。这种情况可以按Ctrl F10手动触发应用重启或者检查 devtools 的依赖是否被optional排除掉了。有的人在打包时引了 devtools 到生产环境又在 application.yml 里关闭了 devtools 的自动重启排查时以为 devtools 还在其实已经被关停了。5.3 生产环境缓存配置与渲染性能实测生产环境必须把模板缓存打开也就是spring.thymeleaf.cache: true。一旦开启Thymeleaf 会缓存解析后的模板 AST后续请求无需重新解析模板文件渲染性能会有明显提升。我这边的经验是在并发量不高的后台系统上这个优化感觉不出来但如果页面模板复杂、访问量大模板缓存的收益还是实打实的。更基本的优化是把模板里的复杂逻辑尽量往前端或 Controller 移动。比如在模板里写嵌套循环做金额累加每次渲染都要执行 Java 逻辑性能肯定比在 Service 层算好一个总数再塞给模板差。模板引擎的定位是“展示层”不是“逻辑处理层”这一点务必记住。还有一个容易忽略的优化点模板文件本身的大小和静态资源的体积。Thymeleaf 输出的是完整 HTML如果引入了很多未压缩的 JS、CSS 文件页面首屏加载会慢。开发环境可以忽略生产环境建议用 Web 服务器开启静态资源压缩。Spring Boot 里可以配置server.compression.enabled: true启用响应压缩HTML、CSS、JS 这类文本资源会显著瘦身这对 Thymeleaf 渲染的页面同样有效。5.4 常见问题速查表我把实际开发中遇到的一些问题整理成表格方便你快速检索。现象最可能的原因排查建议页面中文乱码模板文件编码不是 UTF-8或spring.thymeleaf.encoding未设置检查文件编码显式配置encoding: UTF-8修改模板后不生效缓存未关闭或 devtools 未生效开发环境设置cache: false确认 devtools 正常返回 JSON 而不是页面类上误用RestController改为Controller页面 404视图路径写错或 Jar 包未包含模板目录核对模板路径确认打包资源正常URL 链接少了项目路径手动拼了路径而不是用{}统一使用{/xxx}LocalDateTime格式化报错用了#dates而不是#temporals换成#temporals.format下拉框不回填没用th:field而是手动判断使用th:field绑定表单对象AJAX 请求拿到整页 HTML返回的字符串没有带片段名返回页面名 :: 片段名格式6. 一些项目沉淀下来的实操心得文章写到这儿按照惯例分享一下我在多个项目里积累的一些体会算不上方法论但都是实际操作换来的经验。6.1 模板目录结构从一开始就要规划好很多新项目刚起步时模板文件不多大家随手往templates下丢过一阵子页面多了就会一团乱麻。我强烈建议按模块分目录templates/user/list.html、templates/order/list.html、templates/common/header.html、templates/common/footer.html。只放文件名而不建目录的做法后期维护成本很高。开发效率和可维护性取决于规范技术栈再好也拯救不了混沌的目录结构。6.2 页面里尽量少写内联 Java 代码Thymeleaf 支持在script里写th:inlinejavascript这种内联模式可以在 JS 代码里直接使用模板表达式。这种方式功能很强大但我建议谨慎使用因为页面里的模板表达式和真正的 JavaScript 代码混在一起出了问题很难排查而且不熟悉 Thymeleaf 的前端同事接手时理解成本很高。如果要用建议把数据通过>
返回列表