SpringBoot 實戰:國際化元件 MessageSource 的執行邏輯與原始碼

語言: CN / TW / HK

你好,我是看山。

前文介紹了 SpringBoot 中的國際化元件 MessageSource 的使用,本章我們一起看下 ResourceBundleMessageSourceReloadableResourceBundleMessageSource 的執行邏輯。SpringBoot 的 MessageSource 元件有很多抽象化,原始碼看起來比較分散,所以本文會通過流程圖的方式進行講解。

配置檔案

配置檔案是基礎,會影響執行邏輯,我們先來看下配置項:

  • basename:載入資源的檔名,可以多個資源名稱,通過逗號隔開,預設是“messages”;

  • encoding:載入檔案的字符集,預設是 UTF-8,這個不多說;

  • cacheDuration:檔案載入到記憶體後快取時間,預設單位是秒。如果沒有設定,只會載入一次快取,不會自動更新。這個引數在 ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 稍微有些差異,會具體說下。

  • fallbackToSystemLocale:這是一個兜底開關。預設情況下,如果在指定語言中找不到對應的值,會從 basename 引數(預設是 messages.properties)中查詢,如果再找不到可能直接返回或拋錯。該引數設定為 true 的話,還會再走一步兜底邏輯,從當前系統語言對應配置檔案中查詢。該引數預設是 true;

  • alwaysUseMessageFormat:MessageSource 元件通過 MessageFormat.format 函式對國際化資訊格式化,如果注入引數,輸出結果是經過格式化的。比如 MessageFormat.format("Hello, {0}!", "Kanshan") 輸出結果是“Hello, Kanshan!”。該引數控制的是,當輸入引數為空時,是否還是使用 MessageFormat.format 函式對結果進行格式化,預設是 false;

  • useCodeAsDefaultMessage:當沒有找到對應資訊的時候,是否返回 code。也就是當找了所有能找的配置檔案後,還是沒有找到對應的資訊,是否直接返回 code 值。預設是 false,即不返回 code,丟擲 NoSuchMessageException 異常。

這些配置引數都有各自的預設值。如果沒有特殊的需求,可以直接直接按照預設約定使用。

執行邏輯

接下來我們看下流程圖,下面的流程圖綠色部分是 cacheDuration 沒有配置的情況。對於 ResourceBundleMessageSource 是隻載入一次配置檔案,ReloadableResourceBundleMessageSource 會根據檔案修改時間判斷是否需要重新載入。

ResourceBundleMessageSource 的流程圖

ReloadableResourceBundleMessageSource 的流程圖

AbstractMessageSource 的幾個 getMessage 方法原始碼

@Overridepublic final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {    String msg = getMessageInternal(code, args, locale);    if (msg != null) {        return msg;    }    if (defaultMessage == null) {        return getDefaultMessage(code);    }    return renderDefaultMessage(defaultMessage, args, locale);}
@Overridepublic final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; } String fallback = getDefaultMessage(code); if (fallback != null) { return fallback; } throw new NoSuchMessageException(code, locale);}
@Overridepublic final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { String[] codes = resolvable.getCodes(); if (codes != null) { for (String code : codes) { String message = getMessageInternal(code, resolvable.getArguments(), locale); if (message != null) { return message; } } } String defaultMessage = getDefaultMessage(resolvable, locale); if (defaultMessage != null) { return defaultMessage; } throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);}

複製程式碼

第一個 getMessage 方法,是可以傳入預設值 defaultMessage 的,也就是當所有 basename 的配置檔案中不存在 code 指定的值,就會使用 defaultMessage 值進行格式化返回。

第二個 getMessage 方法,是通過判斷 useCodeAsDefaultMessage 配置,如果設定了 true,在所有 basename 的配置檔案中不存在 code 指定的值的情況下,會返回 code 作為返回值。但是當設定為 false 時,code 不存在的情況下,會丟擲 NoSuchMessageException 異常。

第三個 getMessage 方法,傳入的是 MessageSourceResolvable 介面物件,查詢的 code 更加多種多樣。不過如果最後還是找不到,會丟擲 NoSuchMessageException 異常。

快取的使用

我們看原始碼不僅僅是為了看功能元件的實現,還是學習更加優秀的程式設計方式。比如下面這段記憶體快取的使用,Spring 原始碼中很多地方都用到了這種記憶體快取的使用方式:

// 兩層 Map,第一層是 basename,第二層是 localeprivate final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =        new ConcurrentHashMap<>();
@Nullableprotected ResourceBundle getResourceBundle(String basename, Locale locale) { if (getCacheMillis() >= 0) { // Fresh ResourceBundle.getBundle call in order to let ResourceBundle // do its native caching, at the expense of more extensive lookup steps. return doGetBundle(basename, locale); } else { // Cache forever: prefer locale cache over repeated getBundle calls. // 先從快取中獲取第一層 basename 的快取 Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename); if (localeMap != null) { // 如果命中第一層,在通過 locale 獲取第二層的值 ResourceBundle bundle = localeMap.get(locale); if (bundle != null) { // 如果命中第二層快取,直接返回 return bundle; } } try { // 走到這裡,說明沒有命中快取,就根據 basename 和 locale 建立物件 ResourceBundle bundle = doGetBundle(basename, locale); if (localeMap == null) { // 如果 localeMap 為空,說明第一級就不存在,通過 Map 的 computeIfAbsent 方法初始化 localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>()); } // 將新建的 ResourceBundle 物件放入 localeMap 中 localeMap.put(locale, bundle); return bundle; } catch (MissingResourceException ex) { if (logger.isWarnEnabled()) { logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); } // Assume bundle not found // -> do NOT throw the exception to allow for checking parent message source. return null; } }}

複製程式碼

還有一種使用 Map 實現記憶體快取的寫法,比如我們就對上面的這個方法進行改寫:

public class ResourceBundleMessageSourceExt extends ResourceBundleMessageSource {    private final Map<BasenameLocale, ResourceBundle> cachedResourceBundles = new ConcurrentHashMap<>();
@Override protected ResourceBundle getResourceBundle(String basename, Locale locale) { if (getCacheMillis() >= 0) { // Fresh ResourceBundle.getBundle call in order to let ResourceBundle // do its native caching, at the expense of more extensive lookup steps. return doGetBundle(basename, locale); } else { // Cache forever: prefer locale cache over repeated getBundle calls. final BasenameLocale basenameLocale = new BasenameLocale(basename, locale); ResourceBundle resourceBundle = this.cachedResourceBundles.get(basenameLocale); if (resourceBundle != null) { return resourceBundle; } try { ResourceBundle bundle = doGetBundle(basename, locale); this.cachedResourceBundles.put(basenameLocale, bundle); return bundle; } catch (MissingResourceException ex) { if (logger.isWarnEnabled()) { logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); } // Assume bundle not found // -> do NOT throw the exception to allow for checking parent message source. return null; } } }
public record BasenameLocale(String basename, Locale locale) { @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } BasenameLocale that = (BasenameLocale) o; return basename.equals(that.basename) && locale.equals(that.locale); }
@Override public int hashCode() { return Objects.hash(basename, locale); } }}

複製程式碼

我們可以利用 Map 是通過 equals 判斷 key 是否一致的原理,建立一個包含 basename、locale 的物件 BasenameLocale ,然後改寫 cachedResourceBundles 為一層 Map,會簡化一些判斷邏輯。

此處的 BasenameLocalerecord 型別,具體語法可以參考Java16 的新特性 中的 Record 型別一節。

文末總結

本文先介紹了 MessageSource 的配置項,然後通過流程圖的方式介紹了 ResourceBundleMessageSourceReloadableResourceBundleMessageSource 的執行邏輯,最後分享了兩個使用 Map 實現記憶體快取的方式。

下一節我們將擴充套件 MessageSource,實現從 Nacos 載入配置內容,同時實現動態修改配置內容的功能。

本文中的例項已經傳到 GitHub,關注公眾號「看山的小屋」,回覆 spring 獲取原始碼。

青山不改,綠水長流,我們下次見。