1. 從JSP到Thymeleaf一個模板引擎的演進(jìn)與選擇如果你是從Java Web開發(fā)的“上古時代”一路走過來的肯定對JSPJavaServer Pages又愛又恨。愛它簡單直接在HTML里寫點% %就能嵌入Java代碼快速出活恨它維護(hù)起來簡直是災(zāi)難前后端邏輯攪在一起稍微復(fù)雜點的頁面就難以閱讀更別提單元測試了。后來雖然有了FreeMarker、Velocity這些更清晰的模板引擎但它們本質(zhì)上還是“服務(wù)器端渲染”的思維模板文件離開了后端服務(wù)器就是一堆無法直接預(yù)覽的、帶有特殊標(biāo)簽的“殘次品”。Thymeleaf的出現(xiàn)很大程度上就是為了解決這個痛點。我第一次接觸Thymeleaf是在一個需要前端設(shè)計師高度參與的項目里設(shè)計師習(xí)慣用瀏覽器直接打開HTML文件看效果而我們后端開發(fā)者又需要動態(tài)數(shù)據(jù)。用JSP設(shè)計師打不開。用純HTMLAJAX初期原型和簡單頁面又顯得殺雞用牛刀。Thymeleaf的“自然模板”理念正中下懷——它允許你寫標(biāo)準(zhǔn)的、語法良好的HTML文件那些用于動態(tài)替換的屬性比如th:text在不經(jīng)過服務(wù)器渲染時會被瀏覽器當(dāng)作普通屬性忽略頁面依然能顯示靜態(tài)的默認(rèn)值。這意味著同一個.html文件既是設(shè)計師眼里可預(yù)覽的靜態(tài)原型也是我們后端眼里的動態(tài)模板。這幾年雖然前后端分離架構(gòu)大行其道Vue、React成了前端主流但Thymeleaf并沒有消失反而在一些特定場景下更加穩(wěn)固。比如需要快速開發(fā)的后臺管理系統(tǒng)、對SEO有要求的服務(wù)端渲染頁面、郵件模板、PDF報告生成或者就是一些不那么復(fù)雜、不希望引入重型前端框架的內(nèi)部應(yīng)用。最近社區(qū)里討論的“thymeleaf flying saucer”生成PDF以及“thymeleaf多頁面布局”恰恰說明了它在報表輸出和視圖復(fù)用這些傳統(tǒng)強(qiáng)項上依然有著旺盛的生命力。所以無論你是維護(hù)一個老項目還是開啟一個適合服務(wù)端渲染的新項目花點時間了解Thymeleaf都是一筆不錯的投資。2. Thymeleaf核心設(shè)計哲學(xué)與工作原理拆解2.1 “自然模板”是如何實現(xiàn)的Thymeleaf的核心賣點是“自然模板”Natural Templates。這聽起來有點玄乎但原理其實很直觀。我們來看一段代碼!-- 這是一個標(biāo)準(zhǔn)的Thymeleaf模板片段 -- p歡迎您span th:text${user.name}訪客/span/p當(dāng)這個文件被設(shè)計師用瀏覽器直接打開時瀏覽器不認(rèn)識th:text這個屬性它會將其忽略并顯示標(biāo)簽內(nèi)的靜態(tài)文本“訪客”。于是設(shè)計師看到的是“歡迎您訪客”。而當(dāng)這個文件通過Thymeleaf模板引擎在服務(wù)器端處理時引擎會識別th:text屬性用模型Model中user.name變量的值比如“張三”替換掉整個span標(biāo)簽的內(nèi)容。最終發(fā)送給瀏覽器的是“歡迎您張三”。這種“優(yōu)雅降級”的能力實現(xiàn)了視圖原型和最終成品的高度統(tǒng)一極大地提升了前后端協(xié)作效率。它所有的屬性都以前綴開頭默認(rèn)是th:所以不會污染HTML標(biāo)準(zhǔn)。這種設(shè)計使得模板文件本身就是合法的HTML5文件可以被編輯器校驗、被瀏覽器渲染符合現(xiàn)代開發(fā)工具鏈的習(xí)慣。2.2 模板引擎的三大核心要素理解任何一個模板引擎都可以從三個核心要素入手模板、數(shù)據(jù)模型和引擎處理器。Thymeleaf也不例外。模板Template就是那些包含th:*屬性的HTML文件。Thymeleaf支持多種模板模式最常用的是HTML模式。它不僅僅是簡單的變量替換而是包含了一整套完整的語法能處理條件判斷th:if、循環(huán)th:each、片段包含th:replace、鏈接處理{}等復(fù)雜邏輯。數(shù)據(jù)模型Context在Spring MVC中這通常就是我們放在Model、ModelMap或ModelAndView里的那些鍵值對。在Thymeleaf的語境里它被封裝成一個IContext對象常用實現(xiàn)是WebContext或Context。模板中所有${...}表達(dá)式要獲取的變量都來自于這個上下文Context。例如控制器中model.addAttribute(user, userObj)模板中就能用${user.name}來訪問。引擎處理器TemplateEngine這是大腦。SpringTemplateEngine是Spring生態(tài)中的標(biāo)配。它的工作流程可以簡化為解析讀取模板文件根據(jù)模板模式如HTML創(chuàng)建對應(yīng)的解析器將模板解析成一棵抽象語法樹AST。處理遍歷這棵樹識別所有th:*屬性處理器。每個處理器如TextTagProcessor對應(yīng)th:text負(fù)責(zé)執(zhí)行自己的邏輯計算表達(dá)式、訪問數(shù)據(jù)模型、操作DOM等。渲染將處理后的、純凈的HTML DOM樹序列化為字符串也就是最終的HTML響應(yīng)輸出。這個過程是完全在服務(wù)器端同步完成的所以Thymeleaf天生適合服務(wù)端渲染SSR。對于“thymeleaf生成pdf頁碼”這類需求通常的路徑是先用Thymeleaf渲染出完整的HTML字符串再使用像Flying Saucer這類基于iText的HTML轉(zhuǎn)PDF庫將HTML轉(zhuǎn)換為帶頁碼、頁眉頁腳的PDF文檔。Thymeleaf在這里扮演了生成高質(zhì)量、帶樣式的HTML內(nèi)容的角色。3. 基礎(chǔ)環(huán)境搭建與核心語法精講3.1 在Spring Boot中快速集成現(xiàn)在幾乎所有的Java Web項目都基于Spring Boot集成Thymeleaf簡單到令人發(fā)指。在你的pom.xml中只需要引入一個starter依賴dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency引入之后Spring Boot的自動配置就已經(jīng)為你做好了一切默認(rèn)模板位置classpath:/templates/默認(rèn)模板后綴.html自動配置好了SpringTemplateEngine、ThymeleafViewResolver等組件。你唯一需要做的就是創(chuàng)建控制器和模板文件。創(chuàng)建一個控制器Controller public class HelloController { GetMapping(/hello) public String hello(Model model) { model.addAttribute(message, Hello, Thymeleaf!); model.addAttribute(currentTime, LocalDateTime.now()); return hello; // 對應(yīng) templates/hello.html } }然后在src/main/resources/templates/下創(chuàng)建hello.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title入門示例/title /head body h1 th:text${message}默認(rèn)標(biāo)題/h1 p當(dāng)前時間是span th:text${#temporals.format(currentTime, yyyy-MM-dd HH:mm:ss)}2023-01-01 12:00:00/span/p /body /html啟動應(yīng)用訪問/hello你就會看到動態(tài)渲染的頁面。注意文件開頭的xmlns:th聲明它雖然不是HTML5必須的但能讓IDE更好地提供語法高亮和提示建議加上。3.2 表達(dá)式語法不僅僅是${...}Thymeleaf的表達(dá)式語言Thymeleaf Standard Expression Language非常強(qiáng)大主要有五種類型變量表達(dá)式${...}最常用用于訪問上下文中的變量和屬性。它支持OGNLObject-Graph Navigation Language和Spring EL因此可以嵌套訪問。p用戶名${user.name}/p p公司地址${user.company.address.city}/p !-- 調(diào)用方法 -- p姓名大寫${user.name.toUpperCase()}/p選擇變量表達(dá)式*{...}通常與th:object綁定使用用于簡化對選定對象的訪問。div th:object${user} p姓名*{name}/p !-- 等同于 ${user.name} -- p郵箱*{email}/p /div注意*{...}的作用域僅限于被th:object包裹的標(biāo)簽及其子標(biāo)簽。在這個區(qū)域外使用會報錯。這在表單回顯時特別有用。消息表達(dá)式#{...}用于國際化i18n。它會從消息源如.properties文件中根據(jù)key獲取對應(yīng)的文本。h1 th:text#{page.home.title}首頁/h1鏈接表達(dá)式{...}用于構(gòu)建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使用{...}后你再也不用擔(dān)心應(yīng)用部署路徑改變導(dǎo)致的鏈接失效問題。片段表達(dá)式~{...}用于引入模板片段是實現(xiàn)“thymeleaf多頁面布局”和代碼復(fù)用的關(guān)鍵我們會在后面詳細(xì)講解。3.3 常用屬性處理器實戰(zhàn)屬性處理器是th:*屬性的執(zhí)行者。掌握以下幾個就能應(yīng)對80%的場景。th:text與th:utext文本替換。th:text會對內(nèi)容進(jìn)行HTML轉(zhuǎn)義防止XSS攻擊。th:utext“un-escaped text”則不會轉(zhuǎn)義直接輸出原始HTML除非你非常確定內(nèi)容安全否則慎用。p th:text${htmlContent}默認(rèn)文本/p !-- 輸出strong加粗/strong -- p th:utext${htmlContent}默認(rèn)文本/p !-- 輸出strong加粗/strong --th:each循環(huán)迭代。狀態(tài)變量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條件渲染。判斷依據(jù)是表達(dá)式的布爾值。Thymeleaf對“假”的判斷很寬松null、false、0、false、off、no、空字符串、空集合、空數(shù)組等都被視為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 !-- * 是默認(rèn)case -- /divth:href,th:src,th:action與鏈接表達(dá)式{...}結(jié)合動態(tài)設(shè)置資源路徑。img th:src{/images/logo.png} altLogo form th:action{/user/save} methodpost ... /formth:object與th:field表單數(shù)據(jù)綁定和回顯的黃金搭檔。th:object指定表單綁定的對象th:field綁定對象的具體屬性它能自動生成id、name、value并處理復(fù)選框、單選框的選中狀態(tài)。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會自動將提交的值和錯誤信息回顯到表單中這是開發(fā)CRUD功能時極大的便利。4. 高級特性與項目實戰(zhàn)應(yīng)用4.1 布局與模板復(fù)用告別重復(fù)代碼當(dāng)你的網(wǎng)站有統(tǒng)一的頁頭、導(dǎo)航欄、頁腳時為每個頁面復(fù)制粘貼這些代碼是維護(hù)的噩夢。Thymeleaf提供了強(qiáng)大的布局功能主要通過th:fragment、th:replace、th:insert和th:include3.x版本已廢棄th:include建議用replace/insert來實現(xiàn)。1. 定義片段Fragment 在/templates/layout目錄下創(chuàng)建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}默認(rèn)標(biāo)題/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會用片段完全替換當(dāng)前標(biāo)簽insert則會將片段插入當(dāng)前標(biāo)簽內(nèi)部。!-- /templates/page/index.html -- html head th:replacelayout/common :: common_head(首頁) !-- 這里的原始內(nèi)容會被 common_head 片段完全替換 -- /head body div th:replacelayout/common :: common_header/div main h1首頁內(nèi)容/h1 /main div th:insertlayout/common :: common_footer !-- common_footer 片段會插入到這個div內(nèi)部 -- /div /body /html3. 參數(shù)化片段 片段可以接收參數(shù)使其更加靈活。如上例中common_head(title)。!-- 在另一個頁面 -- head th:replacelayout/common :: common_head(用戶管理)/head這就是實現(xiàn)“thymeleaf多頁面布局”的核心。通過合理的片段劃分你可以像搭積木一樣構(gòu)建頁面極大提升代碼復(fù)用率和可維護(hù)性。4.2 內(nèi)聯(lián)與文本模板模式有時我們需要在JavaScript或CSS中使用Thymeleaf表達(dá)式但th:*屬性在script或style標(biāo)簽內(nèi)無效。這時就需要內(nèi)聯(lián)Inlining。JavaScript內(nèi)聯(lián)使用th:inlinejavascript。script th:inlinejavascript var userId [[${user.id}]]; var userName /*[[${user.name}]]*/ 默認(rèn)用戶名; console.log(用戶${userName}, ID: ${userId}); /script[[...]]是轉(zhuǎn)義的輸出/*[[...]]*/的注釋語法可以在靜態(tài)打開時提供一個可讀的默認(rèn)值。CSS內(nèi)聯(lián)使用th:inlinetext。這在需要動態(tài)生成樣式時有用。style th:inlinetext .user-avatar { background-image: url([[{/avatar/ user.avatarUrl}]]); } .priority-[[${task.priority}]] { color: red; } /style文本模板模式是另一個強(qiáng)大的特性。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}]] 歡迎注冊我們的服務(wù)這比用字符串拼接生成動態(tài)文本要優(yōu)雅和強(qiáng)大得多。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}則直接輸出該字段的所有錯誤信息默認(rèn)會以br/分隔。國際化i18n Spring Boot默認(rèn)會從classpath:/messages.properties及其語言變體如messages_zh_CN.properties加載消息源。Thymeleaf通過#{...}表達(dá)式直接使用。創(chuàng)建messages.propertieswelcome.messageHello, {0}! page.titleUser Profile在模板中使用h1 th:text#{page.title}Title/h1 p th:text#{welcome.message(${user.name})}Hello, User!/p通過#{}表達(dá)式Thymeleaf會自動根據(jù)當(dāng)前請求的Locale通常通過Accept-Language頭或Session設(shè)定選擇對應(yīng)的語言文件。5. 性能調(diào)優(yōu)、常見問題與排查實錄5.1 緩存策略與性能考量Thymeleaf默認(rèn)會緩存已解析的模板這對于生產(chǎn)環(huán)境是至關(guān)重要的性能優(yōu)化可以避免每次請求都重新解析模板文件。但在開發(fā)階段這會導(dǎo)致你修改了模板文件后需要重啟應(yīng)用才能看到變化這顯然是不可接受的。開發(fā)環(huán)境關(guān)閉緩存 在application.properties或application.yml中配置# application.properties spring.thymeleaf.cachefalse# application.yml spring: thymeleaf: cache: false我個人的習(xí)慣是在開發(fā)環(huán)境的配置文件中顯式地設(shè)置為false在生產(chǎn)環(huán)境配置文件中設(shè)置為true或默認(rèn)不寫因為默認(rèn)就是true。模板解析優(yōu)化 對于非常復(fù)雜的頁面模板解析本身可能成為瓶頸。雖然不常見但如果你遇到性能問題可以考慮檢查模板中是否有多余的、復(fù)雜的表達(dá)式計算。避免在模板中進(jìn)行大量的數(shù)據(jù)轉(zhuǎn)換或格式化操作盡量在控制器或服務(wù)層處理好。使用th:block作為邏輯塊容器而不是濫用div因為th:block不會渲染成實際的HTML標(biāo)簽可以減少輸出體積。5.2 高頻問題排查手冊在實際開發(fā)中你肯定會遇到下面這些問題。這里我整理了一份速查表問題現(xiàn)象可能原因解決方案頁面顯示空白或th:*屬性原樣輸出1. 模板文件不在默認(rèn)的classpath:/templates/目錄下。2. 控制器返回的視圖名與模板文件名不匹配注意后綴。3. 沒有引入Thymeleaf依賴或依賴沖突。1. 檢查文件路徑。Spring Boot默認(rèn)找templates/下的.html文件。2. 控制器return viewName對應(yīng)templates/viewName.html。3. 檢查pom.xml運行mvn dependency:tree查看是否有其他模板引擎沖突。表達(dá)式${...}不生效顯示為字符串1. 變量未放入Model。2. 變量名拼寫錯誤。3. 在th:object塊內(nèi)錯誤使用了${}應(yīng)使用*{}。1. 確認(rèn)控制器中使用了model.addAttribute()。2. 仔細(xì)核對變量名大小寫。3. 在th:object范圍內(nèi)訪問該對象的屬性應(yīng)使用*{property}。靜態(tài)資源CSS/JS/圖片404鏈接沒有使用Thymeleaf的{}表達(dá)式或者靜態(tài)資源目錄配置不對。1.始終使用th:href{/path/to/resource}或th:src{...}。2. Spring Boot默認(rèn)靜態(tài)資源目錄是classpath:/static/、/public/等確保資源文件放在這些目錄下。th:field回顯失敗或綁定錯誤1. 表單提交后返回的視圖沒有重新放入包含BindingResult的命令對象ModelAttribute。2. 對象屬性沒有正確的getter/setter方法。3.th:field的值表達(dá)式寫錯。1. POST處理方法處理完驗證后無論是成功還是失敗返回視圖前都需要model.addAttribute(formObject, updatedObject)。2. 確認(rèn)你的Java Bean是符合規(guī)范的POJO。3.th:field的值必須是*{...}表達(dá)式且指向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. 沒有設(shè)置正確的CharacterEncodingFilter。1. 將IDE和文件編碼統(tǒng)一設(shè)置為UTF-8。2. Spring Boot通常自動配置好了。如果不行檢查是否在application.properties中設(shè)置了spring.thymeleaf.encodingUTF-8和spring.http.encoding.charsetUTF-8。5.3 自定義方言與擴(kuò)展雖然Thymeleaf內(nèi)置的功能已經(jīng)非常強(qiáng)大但有時你需要為特定項目創(chuàng)建一些自定義的處理器或表達(dá)式工具。這時就需要了解它的擴(kuò)展機(jī)制——方言Dialect。例如公司內(nèi)部有一個常用的工具類StringUtils你想在模板中直接調(diào)用它的方法。你可以創(chuàng)建一個自定義方言將工具類注冊為表達(dá)式工具對象。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(...)}。不過在大多數(shù)情況下更簡單的做法是直接將工具類實例作為變量放入Model或者使用Spring的Component注解將其注入然后在控制器中傳給Model。自定義方言更適合封裝一組緊密相關(guān)、且需要在多個模板中頻繁使用的復(fù)雜功能。踩過幾次坑之后我的體會是Thymeleaf的學(xué)習(xí)曲線前期平緩但想用得精深必須理解其“自然模板”的哲學(xué)和與Spring深度集成的特性。把th:*屬性當(dāng)作給靜態(tài)HTML添加的“動態(tài)指令”而不是一門新的編程語言心態(tài)會平和很多。對于“thymeleaf生成pdf頁碼”這類需求記住Thymeleaf只負(fù)責(zé)生成完美的HTML剩下的交給專業(yè)的PDF渲染庫如Flying Saucer各司其職才能高效可靠。