1. 從JSP到Thymeleaf一個模板引擎的演進與選擇如果你是從Java Web開發(fā)的“上古時代”一路走過來的肯定對JSPJavaServer Pages又愛又恨。愛它簡單直接在HTML里寫點% %就能嵌入Java代碼快速出活恨它維護起來簡直是災難前后端邏輯攪在一起稍微復雜點的頁面就難以閱讀更別提單元測試了。后來雖然有了FreeMarker、Velocity這些更清晰的模板引擎但它們本質上還是“服務器端渲染”的思維模板文件離開了后端服務器就是一堆無法直接預覽的、帶有特殊標簽的“殘次品”。Thymeleaf的出現很大程度上就是為了解決這個痛點。我第一次接觸Thymeleaf是在一個需要前端設計師高度參與的項目里設計師習慣用瀏覽器直接打開HTML文件看效果而我們后端開發(fā)者又需要動態(tài)數據。用JSP設計師打不開。用純HTMLAJAX初期原型和簡單頁面又顯得殺雞用牛刀。Thymeleaf的“自然模板”理念正中下懷——它允許你寫標準的、語法良好的HTML文件那些用于動態(tài)替換的屬性比如th:text在不經過服務器渲染時會被瀏覽器當作普通屬性忽略頁面依然能顯示靜態(tài)的默認值。這意味著同一個.html文件既是設計師眼里可預覽的靜態(tài)原型也是我們后端眼里的動態(tài)模板。這幾年雖然前后端分離架構大行其道Vue、React成了前端主流但Thymeleaf并沒有消失反而在一些特定場景下更加穩(wěn)固。比如需要快速開發(fā)的后臺管理系統(tǒng)、對SEO有要求的服務端渲染頁面、郵件模板、PDF報告生成或者就是一些不那么復雜、不希望引入重型前端框架的內部應用。最近社區(qū)里討論的“thymeleaf flying saucer”生成PDF以及“thymeleaf多頁面布局”恰恰說明了它在報表輸出和視圖復用這些傳統(tǒng)強項上依然有著旺盛的生命力。所以無論你是維護一個老項目還是開啟一個適合服務端渲染的新項目花點時間了解Thymeleaf都是一筆不錯的投資。2. Thymeleaf核心設計哲學與工作原理拆解2.1 “自然模板”是如何實現的Thymeleaf的核心賣點是“自然模板”Natural Templates。這聽起來有點玄乎但原理其實很直觀。我們來看一段代碼!-- 這是一個標準的Thymeleaf模板片段 -- p歡迎您span th:text${user.name}訪客/span/p當這個文件被設計師用瀏覽器直接打開時瀏覽器不認識th:text這個屬性它會將其忽略并顯示標簽內的靜態(tài)文本“訪客”。于是設計師看到的是“歡迎您訪客”。而當這個文件通過Thymeleaf模板引擎在服務器端處理時引擎會識別th:text屬性用模型Model中user.name變量的值比如“張三”替換掉整個span標簽的內容。最終發(fā)送給瀏覽器的是“歡迎您張三”。這種“優(yōu)雅降級”的能力實現了視圖原型和最終成品的高度統(tǒng)一極大地提升了前后端協作效率。它所有的屬性都以前綴開頭默認是th:所以不會污染HTML標準。這種設計使得模板文件本身就是合法的HTML5文件可以被編輯器校驗、被瀏覽器渲染符合現代開發(fā)工具鏈的習慣。2.2 模板引擎的三大核心要素理解任何一個模板引擎都可以從三個核心要素入手模板、數據模型和引擎處理器。Thymeleaf也不例外。模板Template就是那些包含th:*屬性的HTML文件。Thymeleaf支持多種模板模式最常用的是HTML模式。它不僅僅是簡單的變量替換而是包含了一整套完整的語法能處理條件判斷th:if、循環(huán)th:each、片段包含th:replace、鏈接處理{}等復雜邏輯。數據模型Context在Spring MVC中這通常就是我們放在Model、ModelMap或ModelAndView里的那些鍵值對。在Thymeleaf的語境里它被封裝成一個IContext對象常用實現是WebContext或Context。模板中所有${...}表達式要獲取的變量都來自于這個上下文Context。例如控制器中model.addAttribute(user, userObj)模板中就能用${user.name}來訪問。引擎處理器TemplateEngine這是大腦。SpringTemplateEngine是Spring生態(tài)中的標配。它的工作流程可以簡化為解析讀取模板文件根據模板模式如HTML創(chuàng)建對應的解析器將模板解析成一棵抽象語法樹AST。處理遍歷這棵樹識別所有th:*屬性處理器。每個處理器如TextTagProcessor對應th:text負責執(zhí)行自己的邏輯計算表達式、訪問數據模型、操作DOM等。渲染將處理后的、純凈的HTML DOM樹序列化為字符串也就是最終的HTML響應輸出。這個過程是完全在服務器端同步完成的所以Thymeleaf天生適合服務端渲染SSR。對于“thymeleaf生成pdf頁碼”這類需求通常的路徑是先用Thymeleaf渲染出完整的HTML字符串再使用像Flying Saucer這類基于iText的HTML轉PDF庫將HTML轉換為帶頁碼、頁眉頁腳的PDF文檔。Thymeleaf在這里扮演了生成高質量、帶樣式的HTML內容的角色。3. 基礎環(huán)境搭建與核心語法精講3.1 在Spring Boot中快速集成現在幾乎所有的Java Web項目都基于Spring Boot集成Thymeleaf簡單到令人發(fā)指。在你的pom.xml中只需要引入一個starter依賴dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency引入之后Spring Boot的自動配置就已經為你做好了一切默認模板位置classpath:/templates/默認模板后綴.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; // 對應 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}默認標題/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你就會看到動態(tài)渲染的頁面。注意文件開頭的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包裹的標簽及其子標簽。在這個區(qū)域外使用會報錯。這在表單回顯時特別有用。消息表達式#{...}用于國際化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 常用屬性處理器實戰(zhàn)屬性處理器是th:*屬性的執(zhí)行者。掌握以下幾個就能應對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循環(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條件渲染。判斷依據是表達式的布爾值。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與鏈接表達式{...}結合動態(tài)設置資源路徑。img th:src{/images/logo.png} altLogo form th:action{/user/save} methodpost ... /formth:object與th:field表單數據綁定和回顯的黃金搭檔。th:object指定表單綁定的對象th:field綁定對象的具體屬性它能自動生成id、name、value并處理復選框、單選框的選中狀態(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)應用4.1 布局與模板復用告別重復代碼當你的網站有統(tǒng)一的頁頭、導航欄、頁腳時為每個頁面復制粘貼這些代碼是維護的噩夢。Thymeleaf提供了強大的布局功能主要通過th:fragment、th:replace、th:insert和th:include3.x版本已廢棄th:include建議用replace/insert來實現。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}默認標題/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[[...]]是轉義的輸出/*[[...]]*/的注釋語法可以在靜態(tài)打開時提供一個可讀的默認值。CSS內聯使用th:inlinetext。這在需要動態(tài)生成樣式時有用。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}]] 歡迎注冊我們的服務這比用字符串拼接生成動態(tài)文本要優(yōu)雅和強大得多。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通過#{...}表達式直接使用。創(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通過#{}表達式Thymeleaf會自動根據當前請求的Locale通常通過Accept-Language頭或Session設定選擇對應的語言文件。5. 性能調優(yōu)、常見問題與排查實錄5.1 緩存策略與性能考量Thymeleaf默認會緩存已解析的模板這對于生產環(huán)境是至關重要的性能優(yōu)化可以避免每次請求都重新解析模板文件。但在開發(fā)階段這會導致你修改了模板文件后需要重啟應用才能看到變化這顯然是不可接受的。開發(fā)環(huán)境關閉緩存 在application.properties或application.yml中配置# application.properties spring.thymeleaf.cachefalse# application.yml spring: thymeleaf: cache: false我個人的習慣是在開發(fā)環(huán)境的配置文件中顯式地設置為false在生產環(huán)境配置文件中設置為true或默認不寫因為默認就是true。模板解析優(yōu)化 對于非常復雜的頁面模板解析本身可能成為瓶頸。雖然不常見但如果你遇到性能問題可以考慮檢查模板中是否有多余的、復雜的表達式計算。避免在模板中進行大量的數據轉換或格式化操作盡量在控制器或服務層處理好。使用th:block作為邏輯塊容器而不是濫用div因為th:block不會渲染成實際的HTML標簽可以減少輸出體積。5.2 高頻問題排查手冊在實際開發(fā)中你肯定會遇到下面這些問題。這里我整理了一份速查表問題現象可能原因解決方案頁面顯示空白或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}。靜態(tài)資源CSS/JS/圖片404鏈接沒有使用Thymeleaf的{}表達式或者靜態(tài)資源目錄配置不對。1.始終使用th:href{/path/to/resource}或th:src{...}。2. Spring Boot默認靜態(tài)資源目錄是classpath:/static/、/public/等確保資源文件放在這些目錄下。th:field回顯失敗或綁定錯誤1. 表單提交后返回的視圖沒有重新放入包含BindingResult的命令對象ModelAttribute。2. 對象屬性沒有正確的getter/setter方法。3.th:field的值表達式寫錯。1. POST處理方法處理完驗證后無論是成功還是失敗返回視圖前都需要model.addAttribute(formObject, updatedObject)。2. 確認你的Java Bean是符合規(guī)范的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和文件編碼統(tǒng)一設置為UTF-8。2. Spring Boot通常自動配置好了。如果不行檢查是否在application.properties中設置了spring.thymeleaf.encodingUTF-8和spring.http.encoding.charsetUTF-8。5.3 自定義方言與擴展雖然Thymeleaf內置的功能已經非常強大但有時你需要為特定項目創(chuàng)建一些自定義的處理器或表達式工具。這時就需要了解它的擴展機制——方言Dialect。例如公司內部有一個常用的工具類StringUtils你想在模板中直接調用它的方法。你可以創(chuàng)建一個自定義方言將工具類注冊為表達式工具對象。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:*屬性當作給靜態(tài)HTML添加的“動態(tài)指令”而不是一門新的編程語言心態(tài)會平和很多。對于“thymeleaf生成pdf頁碼”這類需求記住Thymeleaf只負責生成完美的HTML剩下的交給專業(yè)的PDF渲染庫如Flying Saucer各司其職才能高效可靠。