iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

30 篇淺談 Spring 框架的核心底層組件 系列 第 11 篇

Day 11: MessageSource —— 多語系 i18n 與文字訊息管理機制

  • 分享至 

  • xImage
  •  

從 Application-Context 的繼承我們可以知道,Spring 內部的 i18m 是透過 Message-Source 這個介面完成的。Message-Source 內部有 3 個 getMessage 的方法多載,雖然方法的簽名略有不同,但核心要傳入的參數基本上大同小異,主要圍繞在 code, args, 和 locale 這 3 個參數上。

https://ithelp.ithome.com.tw/upload/images/20260925/20184114XXazm38ttz.png

code(訊息代碼)負責充當設定檔中的 Key,用來作為查找特定文字的唯一識別碼,例如 user.not.found;args(動態參數陣列)用來依序填入文字內容中的動態數值,例如原始的文字可能是 "找不到使用者:{0}",就會用 args[0] 取代掉裡面的 {0};最後的 locale(語系物件)則是告訴容器當前要抓取哪一個地區與語言的文字?像是 zh_TW, zh_CN 或 en_US ...等。透過 code + locale 先找到對應的 i18n 字串,再把 args 的字串依序取代裡面的 {0}, {1} ...。算是 IoC 容器裡面最易懂的容器功能之一。

  • MessageFormat

說完了大綱,再來往細節前進:首先,{0} {1} 這類字串的取代方式,用的是 Java 原生的 Message-Format:因為功能相對單純,Spring 並沒有針對這個文字的取代功能去重新發明輪子,寫一套新的字串替換引擎,而是直接復用 Java 原生的 Message-Format 進行實現。

也因為依賴的是原生 MessageFormat,所以我們也可以對佔位符做一些原本就可以介入的手腳,像是用 {0, date, yyyy-MM-dd} 做日期的自動取值、或者是用 {1, number, currency} 做金額數字的預設顯示 ...等,詳細的字串用法可以再參考 Message-Format 的支援寫法。

  • MessageSourceResolvable

其次,MessageSource 定義的方法內,有一個 Message-Source-Resolvable 參數,這個參數是一組訊息描述的封裝介面,可以簡單把它想像成一個「Spring 專門在 i18n 這個功能內定義的 DTO」。它會把單一訊息所需的多組候選 code、動態參數 args 以及兜底的 default-Message 打包在同一個物件裡。

Message-Source-Resolvable 最經典的應用場景,就是 Spring MVC 在 Controller 層對請求參數的校驗:當 @Valid 驗證未通過時,系統會拋出一個 Field-Error,它本身就實作了 Message-Source-Resolvable,內部會預先封裝好欄位名稱、驗證規則的候選 Code 清單、限制條件參數、以及預設訊息。

這樣做的目的是:讓框架不需要在出錯的瞬間,就決定錯誤訊息要怎麼呈現。而是將錯誤先包裝起來,使 Spring MVC 能在後續統一的例外處理中,配合當前 HTTP 請求所帶的 Locale(或其他針對語系的判斷方法),一口氣將這個物件解析成對應語系的錯誤提示,再回傳給前端。

  • ResourceBundle

再來簡單提一下 Spring 語系設定檔的綁定是怎麼實作的:語系的綁定有很多種做法,最常見的實作類別是 Resource-Bundle-Message-Source。類別內包了一層 Java 原生的 Resource-Bundle 物件,同時對外暴露 setBasename 方法跟 setBasenames 方法,用來處理單一值或多值的情況。這裡的 basename 指的是語系檔案的基底名稱,常見預設是 "messages" 字串,後面會再根據不同的 zh_TW, en_US ...等 Locale code,拼接出 messages_zh_TW.properties 這樣的檔案名稱。

後續的查找規則同樣是 JDK 的原生行為(這一段都不是 Spring 加工過的查找方法):先透過完整名稱,檢索語系檔案,找不到就逐步降級比對,一路從 messages_zh_TW.properties 退到 messages_zh.properties,最後再退回預設的 messages.properties,那如果這些檔案都找不到,退無可退的情況就會用回 default-Message 了。

  • ReloadableResourceBundleMessageSource

這邊也有一個小東西可以延伸:因為 Resource-Bundle-Message-Source 底層依賴的是 Class-Loader 搭配 Resource-Bundle 內建的快取,代表一份語系檔只要被讀過一次,就會常駐在記憶體裡面,想要修改內容的話,不重啟應用是不會生效的。因此,針對這個問題,Spring 額外提供了一個 Reloadable-Resource-Bundle-Message-Source,改用之前提過的 Resource / ResourceLoader 去讀取檔案,並多寫了一個 cacheSeconds 參數,用來設定語系快取的有效期限。

實務上,無論是純 Spring 還是 Spring Boot 的預設自動配置,底層預設使用的都是一般的 Resource-Bundle-Message-Source。如果有正式環境免重啟動態更新語系檔的需求,我們通常會手動配置一個 Reloadable-Resource-Bundle-Message-Source 的 Bean 來取代預設行為。

LocaleContextHolder

最後補充一點比較貼近 Web 開發的應用:前面提到的 getMessage() 都需要手動傳入一個 Locale 參數,但實務上寫 Controller 的時候,我們是不用自己額外注入 Locale 的,原因是 Spring MVC 內有一個組件叫做 Locale-Resolver,它會負責在每次請求進來的當下,決定這次該用哪個 Locale,並把解析出來的結果放進另一個 Locale-Context-Holder。

使用 Locale-Context-Holder,不管是 Controller、Service,還是前面提到的 FieldError 要轉換錯誤訊息,都可以直接透過 LocaleContextHolder.getLocale() 拿到目前請求對應的語系,不需要手動從 request 裡剖析標頭,也不用一路把 Locale 當參數往下傳,算是一個相對貼心的簡化邏輯呈現。


期待後續各位的閱讀與分享,我是 Pax,我們明天見。


上一篇
Day 10. Environment —— 環境配置與多環境隔離
系列文
30 篇淺談 Spring 框架的核心底層組件 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言