1. 为什么还在用模板引擎:Thymeleaf的定位与选型逻辑
这几年前后端分离的风潮刮得很猛,Vue、React成了不少团队的标配,很多新人甚至没写过服务端渲染页面。但如果你做过管理后台、CMS系统、报表类页面或者对内业务系统,就会发现模板引擎不仅没死,反而在特定场景下依然是效率最高的方案。Spring Boot里标准的模板引擎是Thymeleaf,这篇文章就围绕它的配置、语法、静态资源处理和常见坑展开,把我实际踩过的、验证过的东西一次讲清楚。
先回答一个绕不开的问题:为什么不用JSP?JSP在传统SSM项目中确实是主力,但它有一个先天不足——Spring Boot以可执行JAR方式打包后,JSP的渲染支持一直很别扭,需要额外的依赖和配置,而且在嵌入式容器下对JSP的支持历史上有不少限制。Thymeleaf是纯模板文件,天然支持JAR包部署,这一点在微服务化、容器化部署的趋势下非常关键。
再说Thymeleaf和FreeMarker的区别。FreeMarker更偏程序员思维,模板里可以写大量逻辑表达式,自由度很高,但这也意味着你很难限制页面里别写乱七八糟的业务逻辑。Thymeleaf的设计理念是"原生HTML页面",模板文件本身就是一个合法的HTML文件,浏览器直接打开就能看到静态效果,后端渲染时才把动态内容填进去。前端和后端各看各的,不用依赖一个"中间格式"。这个特性在团队协作时非常有用——前端可以直接用浏览器预览页面,后端专注填充数据。
从我的实际体验来看,选Thymeleaf还有一个务实理由:它的标签语法和HTML属性很接近,学习成本低。团队里来一个新人,告诉他th:text替换文本、th:if条件判断、th:each循环,基本半天就能上手写页面。这对维护成本来说是很实在的收益。
Spring Boot对Thymeleaf的支持也足够"傻瓜化"——引入依赖后,几乎零配置就能跑起来。默认的模板目录、默认的文件后缀、默认的编码都给你定好了。但"零配置"不等于"不需要理解配置",尤其是上了点规模的项目,模板前缀、缓存策略、静态资源映射这些细节迟早会找上你。提前把原理弄清楚,后面排查问题会省很多事。
2. 依赖引入与工程结构:先把骨架搭对
2.1 引入依赖时的版本选择细节
先说最基础的一步。创建一个Spring Boot项目,用IDEA的Spring Initializr也好,自己手写Maven工程也好,核心依赖就一个:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>这个starter会把Thymeleaf核心库和Spring Boot的自动配置包一起拉进来,不需要再手动引入Thymeleaf本体和thymeleaf-spring5之类的适配层。我在早期版本见过有人手动配了四五个Thymeleaf相关依赖,版本冲突调了半天,其实一个starter就全搞定了。
注意一点:如果同时引入了spring-boot-starter-web和spring-boot-starter-thymeleaf,Spring Boot会优先使用Thymeleaf作为视图解析器。当然你还可以同时引入FreeMarker之类的依赖,Spring Boot会按照自动配置的顺序各自注册ViewResolver,但实际使用时建议只保留一种,避免视图解析器优先级混乱。
版本方面,Spring Boot的父POM会统一管理Thymeleaf的版本,一般情况下不需要手动指定版本号。只有当你需要用到Thymeleaf 3的新特性而Boot管理版本太老时,才需要覆盖属性:
<properties> <thymeleaf.version>3.0.15.RELEASE</thymeleaf.version> <thymeleaf-extras-springsecurity5.version>3.0.4.RELEASE</thymeleaf-extras-springsecurity5.version> </properties>能不动版本就别动,跟着Boot的依赖管理走,这是最省心的方案。
2.2 模板文件到底该放在哪里
Spring Boot的自动配置默认约定:模板文件放在src/main/resources/templates目录下,静态资源放在src/main/resources/static目录下。你的Controller返回逻辑视图名时,Thymeleaf会去templates目录下拼上后缀找文件。
比如Controller里这样写:
@Controller public class IndexController { @GetMapping("/") public String index(Model model) { model.addAttribute("appName", "订单管理后台"); return "index"; } }它找的就是src/main/resources/templates/index.html。
这里有一个容易忽略的坑:如果把页面文件误放到了static目录,Spring Boot会用静态资源处理器直接把它当普通文件返回,而不会经过Thymeleaf渲染引擎。页面里的th:text这些属性全部原样出现在HTML源码里,看起来就像"模板没生效"。排查这类问题时,第一步先确认文件路径是不是在templates下。
目录结构还可以用子目录组织,比如templates/admin/user-list.html,Controller返回"admin/user-list"即可。在Spring Boot中,模板名里的斜杠会被正确映射到子目录,不需要额外配置。
2.3 一个可运行的最小示例
为了让后面讲配置时你有一个可验证的基准,我把最小示例的完整结构列出来。
工程目录如下:
src/main/java/com/example/demo/ ├── DemoApplication.java ├── controller/ │ └── IndexController.java └── model/ └── User.java src/main/resources/ ├── static/ │ ├── css/ │ │ └── app.css │ └── js/ │ └── app.js └── templates/ └── index.htmlController代码如下:
@Controller public class IndexController { @GetMapping("/") public String index(Model model) { model.addAttribute("title", "Thymeleaf 配置实践"); model.addAttribute("users", Arrays.asList( new User("张三", "zhangsan@example.com", 26), new User("李四", "lisi@example.com", 31) )); return "index"; } }模板页面内容:
<!DOCTYPE html> <html lang="zh-CN" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${title}">首页</title> <link rel="stylesheet" th:href="@{/css/app.css}" href="/css/app.css"> </head> <body> <h1 th:text="${title}">标题</h1> <table> <thead> <tr> <th>姓名</th> <th>邮箱</th> <th>年龄</th> </tr> </thead> <tbody> <tr th:each="user : ${users}"> <td th:text="${user.name}">姓名</td> <td th:text="${user.email}">邮箱</td> <td th:text="${user.age}">年龄</td> </tr> </tbody> </table> </body> </html>运行项目后访问http://localhost:8080/,如果能看到表格里两行用户数据并且标题正确显示,说明基础链路已经通了。有了这个可运行的最小工程,后面改配置、做实验才有基准。
3. 核心配置项详解:每一个配置背后的原因
3.1 application.properties里的主角配置
Thymeleaf在Spring Boot中的配置项很多,但日常开发真正用得上的也就七八个。我按重要性从高到低说。
# 模板文件存放目录 spring.thymeleaf.prefix=classpath:/templates/ # 模板文件后缀 spring.thymeleaf.suffix=.html # 模板编码 spring.thymeleaf.encoding=UTF-8 # 模板模式(关键配置) spring.thymeleaf.mode=HTML # 是否开启模板缓存 spring.thymeleaf.cache=false # 模板解析器顺序 spring.thymeleaf.check-template-location=true先说prefix和suffix。默认值就是classpath:/templates/和.html,绝大多数场景不需要改。但我见过一种情况:有的项目要把模板文件放在外部独立目录,比如/opt/app/templates,方便运维直接改页面不用重新打包。这时候prefix就要改成file:/opt/app/templates/。注意classpath:和file:前缀的区别——前者从依赖JAR或classes目录加载,后者从文件系统加载。外部化模板是一次性配置、长期受益的功能,如果你们的部署有这个诉求,这个配置就非常值钱。
3.2 缓存配置是开发期和上线期的分水岭
spring.thymeleaf.cache是个很值得单独拎出来说的配置。开发阶段必须设为false,这样修改模板后刷新页面立刻生效,不用重启应用。生产环境必须设为true,让Thymeleaf把解析过的模板缓存起来,避免每次请求都重新解析模板文件,显著降低IO和解析开销。
这里有一个实战小技巧:如果你想保留缓存带来的性能,又想在某台机器上临时调试页面,可以在启动命令里加参数覆盖:
java -jar app.jar --spring.thymeleaf.cache=false开发时IDEA里还有一个容易混淆的问题——修改了html文件但浏览器里始终是旧页面。如果你确认spring.thymeleaf.cache=false也设置了,那多半是浏览器端缓存。按F12打开开发者工具,勾选Disable cache,同时注意看清楚请求是不是命中了304 Not Modified。
还有一个我没有直接在配置文件里设过、但知道很多人问的属性:spring.thymeleaf.render-hidden-markers-before-checkboxes。这是专门解决checkbox渲染时隐藏域处理问题的,属于进阶参数,碰到表单提交场景再说,日常配置不需要动它。
3.3 模板模式从HTML5到HTML的变迁
spring.thymeleaf.mode这个配置在Thymeleaf 3.0之前叫HTML5,3.0之后改成HTML。如果你在旧文档里看到spring.thymeleaf.mode=HTML5,直接改成HTML就好,写HTML5在Thymeleaf 3里反而不生效。
为什么要用HTML模式?因为HTML模式对标签的校验更宽松,允许HTML5新增的标签和属性(比如<video>、>spring.thymeleaf.mode=HTML
补充一个容易被忽略的:spring.thymeleaf.check-template-location默认true,它会检查prefix指向的目录是否存在。如果用classpath:/templates/并且确认目录存在,就没问题。但有些工程把模板目录建成了src/main/resources/template/(少了个s),启动时会直接报错——因为check-template-location会寻找classpath:/templates/。这种低级错误我在帮别人排查时见过不止一次。
3.4 加上一个内联配置让代码更干净
还有一个我习惯性加的配置:
spring.thymeleaf.servlet.content-type=text/html默认值就是text/html,不需要改。但如果你的页面需要返回application/xhtml+xml之类的特殊类型,这里得过一眼。
以上这些配置项,我的建议是开发展阶段全部丢在application-dev.properties里,生产用application-prod.properties,利用Spring Boot的多Profile配置机制把它们分开。开发配置里cache=false,生产配置里cache=true,这样团队协作时不会有人忘记改配置。
4. 静态资源与页面数据渲染:配置只是起点
4.1 静态资源映射规则与URL拼接
Thymeleaf模板页里引用CSS、JS、图片等静态资源时,要使用th:href和th:src,并且用@{}表达式。@{}的主要作用是自动拼接context-path,这样项目部署到带根路径的环境下页面资源也不会失效。
<!-- 错误的写法 --> <link rel="stylesheet" href="/css/app.css"> <!-- 正确的写法 --> <link rel="stylesheet" th:href="@{/css/app.css}" href="/css/app.css">实际上Thymeleaf渲染时是拿th:href覆盖掉普通href属性的值。所以模板里同时保留th:href和href,这样直接用浏览器打开模板文件时(注意是templates里的文件,不是渲染后的),还能看到基本样式;后端渲染后th:href生效。这就是Thymeleaf"原生HTML页面"设计理念的最直观体现。
静态资源的访问路径在Spring Boot默认规则下是这样:
- 浏览器访问
/css/app.css,实际找的是classpath:/static/css/app.css - 浏览器访问
/js/app.js,实际找的是classpath:/static/js/app.js
如果你想把静态资源放别的目录,比如resources/static-assets,需要自定义资源映射:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") .addResourceLocations("classpath:/static-assets/"); } }这里要注意:addResourceHandler的第一个参数是浏览器URL模式,第二个参数是实际文件系统或classpath路径。映射关系非常直接,把URL前缀想象成"门牌号",把实际路径想象成"实际地址"。配置完访问/resources/css/app.css就能命中static-assets/css/app.css。
提示:
addResourceLocations的末尾必须带/,否则路径拼接会出问题。这个细节不仔细看极易踩。
4.2 标准表达式语法精华速记
Thymeleaf表达式是整个模板引擎的使用核心,我挑几个最常用的讲。
${...}是变量表达式,用来读取Model中放入的属性,也支持OGNL对象导航。比如${user.name}调用的是getName()方法,不是直接访问字段。所以Model里放的对象必须有对应的getter方法。
@{...}是URL表达式,前面已经提过。它可以拼接路径参数和查询参数:
<!-- 路径参数 --> <a th:href="@{/user/detail/{id}(id=${user.id})}">查看详情</a> <!-- 查询参数 --> <a th:href="@{/user/list(page=1,size=10)}">下一页</a>这个语法比较灵活,括号里放在路径中的变量会被拼成路径参数,路径之外的变成查询字符串。
*{...}是选择变量表达式,通常配合th:object使用,用于减少重复的对象前缀。举个例子,没有th:object时你得这样写:
<p th:text="${user.name}">姓名</p> <p th:text="${user.email}">邮箱</p> <p th:text="${user.age}">年龄</p>用了th:object以后可以简化成:
<div th:object="${user}"> <p th:text="*{name}">姓名</p> <p th:text="*{email}">邮箱</p> <p th:text="*{age}">年龄</p> </div>这里有一个容易踩的坑:th:object只对所在标签内部的*{}表达式生效,如果外层没有th:object就使用*{...},会抛出NoValueFoundException之类的错误。
th:if和th:unless用于条件判断,注意Thymeleaf对空值、空字符串、0、false、空集合这些都视为假。判断一个集合是否为空,常用写法是:
<div th:if="${#lists.isEmpty(users)}">暂无用户</div>th:each循环时支持迭代状态。除了取每个元素,还能拿到当前下标、当前元素总数、该元素是否是第一个/最后一个:
<tr th:each="user, stat : ${users}"> <td th:text="${stat.index}">0</td> <td th:text="${user.name}">姓名</td> <td th:text="${stat.count}">1</td> </tr>其中stat是可选的,不写时默认变量名是stat,显式声明为user, stat更清晰。stat.index从0开始,stat.count从1开始,这个区别很容易混淆,写表格序号时我建议直接用stat.count,天然从1开始。
4.3 日期和时间格式化到底怎么写
日期格式化是模板开发里的高频需求,也是新手最容易卡壳的地方。
在Controller里传过来一个LocalDateTime对象,模板里直接输出会是一长串带T的ISO格式,不好看。正确做法是用#temporals工具类:
<p th:text="${#temporals.format(createTime, 'yyyy-MM-dd HH:mm:ss')}">2024-01-01 00:00:00</p>如果是普通的时间处理工具类,还有一个更简洁的写法:
<p th:text="${#dates.format(localDate, 'yyyy-MM-dd')}">2024-01-01</p>但要特别注意:#dates工具类只处理java.util.Date,如果用Java 8的LocalDateTime,#dates.format会直接抛异常。此时必须用#temporals。这两个工具类之间的区分是靠类型不对时异常信息提示的,我第一次用#dates格式化LocalDateTime时愣是没看明白报错。现在规范的做法是——项目中时间类型统一用java.time包,模板里日期格式化也就统一用#temporals。
如果你觉得模板里到处写格式化表达式太啰嗦,可以在Controller层先处理好,直接把格式化后的字符串放进Model。页面展示的职责交给模板,数据加工留在Controller,这个边界把握好了,模板文件会清爽很多。
4.4 模板布局之碎片复用
一个后台管理系统至少有几个公共区块:顶部导航、侧边栏、页脚。把它们在每个页面里复制粘贴显然是Bad Practice。Thymeleaf的th:fragment和th:insert就是干这个用的。
在templates/fragments/common.html中定义公共片段:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <nav th:fragment="sidebar" class="sidebar"> <ul> <li><a th:href="@{/dashboard}">仪表盘</a></li> <li><a th:href="@{/user/list}">用户管理</a></li> <li><a th:href="@{/order/list}">订单管理</a></li> </ul> </nav> <footer th:fragment="footer" class="footer"> <p>© 2024 管理系统</p> </footer> </body> </html>在页面中引用:
<div th:insert="~{fragments/common :: sidebar}"></div>或者如果不需要外层再包一层div,可以直接用th:replace:
<nav th:replace="~{fragments/common :: sidebar}"></nav>th:insert和th:replace的区别在于:th:insert会把片段插入到当前标签内部,th:replace会用片段替换整个当前标签。大多数布局场景我用th:replace,结构更干净。
~{fragments/common :: sidebar}这个表达式的意思是:找到fragments/common.html文件里名为sidebar的片段。注意文件名不带后缀、路径省略了templates前缀。
这个机制配合布局继承还能实现更精细的页面骨架统一。你用th:replace插入公共头尾,中间内容区域每页自己写,页面间就保持了高度一致的布局结构。实际项目中我把侧边栏、顶部导航、页脚、脚本区都做成了fragment,新建一个页面只需要引用四个碎片,代码量大幅下降,后期改侧边栏菜单也只需要改一个文件。
4.5 Controller返回逻辑视图名的几种姿势
现在把Controller返回类型的情况完整梳理一遍。
最常见的是方法返回String,代表逻辑视图名,比如返回"index"则拼接出classpath:/templates/index.html。注意:在类上必须使用@Controller而不是@RestController。如果你用了@RestController,方法返回字符串时Spring MVC会把它当成HTTP响应体直接交给页面输出,不会走视图解析器。
有的项目里我见过类上标注@RestController,页面请求和接口请求写在一个类里,结果页面请求返回了一段HTML代码而不是渲染后的页面。排查时先看Controller注解,这个问题的发生频率远高于其他配置问题。
如果不想让方法只绑定一个模板,可以返回ModelAndView:
@GetMapping("/index") public ModelAndView index() { ModelAndView mv = new ModelAndView("index"); mv.addObject("appName", "订单管理后台"); return mv; }还有一种更灵活的方式:通过RedirectAttributes做重定向传参,配合RedirectView实现POST后重定向到GET的经典PRG模式:
@PostMapping("/user/save") public String saveUser(User user, RedirectAttributes ra) { userService.save(user); ra.addFlashAttribute("message", "保存成功"); return "redirect:/user/list"; }Flash属性在重定向后的下一次请求里可以通过${message}直接拿到,刷新页面后自动消失。这个机制用来做表单提交后的提示信息非常合适,同时避免了刷新页面重复提交表单的问题。
5. 常见问题与排查技巧实录:全是真实踩过的坑
5.1 页面修改后不生效:是缓存还是路径问题?
这是问的最多的问题,而且发生场景五花八门。按顺序检查以下三个原因:
第一,spring.thymeleaf.cache是否确实为false。如果用了多个Profile,确认当前激活的是开发Profile。IDEA里有时候配置文件加载顺序会出幺蛾子,看下启动日志里Active profiles打印的是什么。
第二,浏览器缓存。Thymeleaf页面即使服务端已经更新,浏览器端缓存了旧HTML,刷新也是看的旧页面。无痕窗口打开一次能排除这个问题。
第三,是否真的改对了文件。src/main/resources/templates目录下的文件是热更新目标,但如果你改了static目录下的CSS或JS文件,由于静态资源没有经过Thymeleaf解析,Spring Boot默认开启的静态资源缓存策略也会让刷新不生效。开发时建议把spring.web.resources.cache.cachecontrol.max-age设小一点或直接设0,防止静态资源被浏览器缓存住。
5.2 模板解析异常:Whitelabel Error Page背后的日志
页面显示Whitelabel错误页时,第一件事去控制台看堆栈。Thymeleaf解析错误常见的有这么几类:
TemplatedException:模板文件本身有问题,比如标签未闭合、属性拼写错误、表达式语法错。报错信息里通常会精确到文件哪一行,直接去改就好。
SpelEvaluationException或OGNL表达式求值错误:表达式的对象为空,访问了不存在的属性或方法。比如${user.name}但user为null。排查思路是在Controller里给Model加一个当前用户,或者表达式里加空判断:
<p th:text="${user != null ? user.name : '未登录'}">未登录</p>NoSuchMethodError之类的运行时异常:Controller往Model里放的对象类型和模板里调用的方法不匹配。比如模板里写${user.createTime}但User类里压根没有getCreateTime()方法,那只能是方法名拼错或者数据类型不对。
5.3 中文乱码问题
模板页面出现中文乱码,绝大多数是文件编码和解析编码不一致导致。Spring Boot里配置了spring.thymeleaf.encoding=UTF-8,但HTML文件的真实编码可能不是UTF-8——IDE新建文件时默认编码如果不是UTF-8,或者从别处复制过来的文件带着GBK编码,都会渲染乱码。
还有一个隐蔽的乱码来源:HTML的<meta charset>声明和实际返回的Content-Type头里的charset不一致。Thymeleaf渲染时会以spring.thymeleaf.encoding为准设置响应编码,浏览器优先读HTTP头里的编码。所以配置文件统一设UTF-8后,HTML文件也保存为UTF-8编码,就不会再出现中文乱码。
我这里吃亏的经验是:从同事那收到一个模板文件,打开后IDE显示出中文正常,但浏览器渲染出来全乱码。排查了很久,最后发现那个HTML文件是GBK编码,IDE自动按检测出的编码打开了所以显示正常,但Thymeleaf按UTF-8读取就乱码。解决办法是用文本编辑器(比如VSCode或Notepad++)另存为UTF-8无BOM格式。
5.4 静态资源全部404
页面能渲染,但CSS、JS全部404。这类问题十有八九是静态资源映射被覆盖了。如果你在项目里实现了WebMvcConfigurer并重写了addResourceHandlers方法,那么Spring Boot默认的静态资源映射会被你的自定义配置覆盖掉。要保留默认映射,需要在方法里显式添加默认规则:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 显式保留Spring Boot默认的静态资源映射 registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); // 再添加自定义映射 registry.addResourceHandler("/resources/**") .addResourceLocations("classpath:/custom-assets/"); }另一个404原因是前置了DispatcherServlet的映射路径。比如个别项目把spring.mvc.servlet.path改成了/api,那@{/css/app.css}渲染出来的链接会多一段前缀。这种情况下th:href="@{/css/app.css}"生成的URL可能不符合你路由规则,需要检查链接的实际生成结果。
5.5 请求JSON却返回了HTML页面
这类问题常见于Controller同时提供页面和API接口的场景。比如一个类上标@RestController,其中一个方法返回一个Map对象,Spring Boot会尝试把它序列化成JSON——这是正常行为。但如果另一个方法返回String类型,且这个类同时被标注了@RestController,那这个String会被直接输出而不是渲染成模板页面。
我自己的习惯是——凡是返回视图的Controller用@Controller注解,同时这个类里不写任何返回JSON的接口。返回JSON的接口单独用@RestController写在别的类里,或者在@Controller类的方法上额外加@ResponseBody做区分。这样职责明确,不会出现"我明明想返回页面却输出了一段HTML"的迷惑现象。
5.6 Thymeleaf 3的HTML模式适配
Thymeleaf 3.0把默认模式从HTML5改成了HTML,但是很多旧项目是从Thymeleaf 2.x直接升级上来的,模板里可能还残留着旧版本才有的写法,比如th:inline="text"的用法在两个版本中有差异。升级后渲染报错时注意看报错信息里提示的是不是模板兼容性问题,不要急着去查配置。
如果团队里有多个项目,模板片段可能会在不同项目间复制迁移,这时候模板文件里的xmlns:th="http://www.thymeleaf.org"声明也需要同步复制,别漏了。有些编辑器(比如老版本的IDEA)在没有这个命名空间声明时,对th:属性会标红报错,虽然不是运行时错误,但看着不舒服,还是加上为好。
5.7 表单提交表单回显的隐藏坑
Thymeleaf处理表单回显时,th:field可以自动回填表单控件的值。它要求绑定的对象必须存在。如果你使用了一个空表单页面(比如新增用户),Controller需要先往Model里放一个空对象:
@GetMapping("/user/add") public String addUser(Model model) { model.addAttribute("user", new User()); return "user-form"; }不传这个空对象直接渲染模板,th:field="${user.name}"在首次渲染时会报错。这是很多新手会遇到的情况——思路是错的,以为是"表单页面不展示数据就不用传对象"。th:field的设计逻辑是表单必然绑定一个对象,空对象也是对象,只是字段都为空而已。
还有一个checkbox绑定布尔值的例子。如果User类里有boolean vip属性,模板里写th:field="*{vip}",首次渲染的时候对象里vip默认是false,checkbox不会被选中,提交时不勾选checkbox则后端收到null,不是false。这种坑我记忆中花了不少时间才定位,要是碰到可以检查一下是否存在checked状态没有正确回显。
6. Thymeleaf配置复盘:我个人的使用心得
把配置Thymeleaf的整个链路走下来,我的体会是Spring Boot把95%的体力活都干完了,剩下的5%才是真正拉开开发效率的地方。
第一,开发环境一定把cache关掉,一旦忘了关,改完模板不见反应,各种怀疑人生。关闭缓存后,配合IDEA自带的Live Edit或者spring-boot-devtools,模板文件改完保存就能看到效果,体验接近前端的热更新。
第二,静态资源的路径映射尽早规划好。如果一个项目有前端参与,建议在启动开发时就约定好静态资源放static目录,模板引擎只做HTML渲染,两者职责分离。等项目跑起来再去迁移资源路径,牵一发动全身。
第三,模板文件里的表达式宁短勿长。单个模板文件里出现超过三层的嵌套表达式(比如${user.orderList[0].productName}),就要考虑在Controller层把数据预处理成扁平结构。模板是视图层,越简单越好维护,别把所有数据加工的活儿都堆在模板表达式里。
第四,安全意识。模板输出用户输入的富文本时存在XSS等风险场景。Thymeleaf默认会对th:text输出的内容进行HTML转义,这是安全的。如果确实需要输出原始HTML(比如富文本编辑器生成的内容),使用th:utext时要明确知道你在禁用转义,务必确保内容是可信的或经过清洗的。我实际项目中一般都禁用th:utext,除非有100%的把握数据源干净。
关于Thymeleaf和前端框架的关系,我的看法是:这不一定是二选一的关系。在实际项目里,面对的是内部管理系统那类以数据展示和表单操作为主的场景时,Thymeleaf服务端渲染比前端工程化方案更直接;遇到交互复杂、实时性要求高的模块,也可以用Thymeleaf渲染页面骨架,局部再用Vue或原生JS增强交互。混合模式虽然需要约定好数据传递的接口,但确实能两端兼顾。
最后再分享一个配置上的小经验。不管项目多大多小,建议把Thymeleaf相关配置集中放在一个配置类或配置文件的固定区域,并加上清晰注释,说明每一个配置项为什么这么设、哪个环境需要怎么改。后来接手的人打开配置文件一看就懂,不用翻文档猜测某个配置是干什么的。工程上的这种小习惯,维护成本降低是很明显的。