配置文件
配置文件是基礎(chǔ)碗旅,會(huì)影響執(zhí)行邏輯贷揽,我們先來看下配置項(xiàng):
basename:加載資源的文件名棠笑,可以多個(gè)資源名稱,通過逗號(hào)隔開擒滑,默認(rèn)是“messages”腐晾;
encoding:加載文件的字符集叉弦,默認(rèn)是 UTF-8,這個(gè)不多說藻糖;
cacheDuration:文件加載到內(nèi)存后緩存時(shí)間淹冰,默認(rèn)單位是秒。如果沒有設(shè)置巨柒,只會(huì)加載一次緩存樱拴,不會(huì)自動(dòng)更新。這個(gè)參數(shù)在 ResourceBundleMessageSource洋满、ReloadableResourceBundleMessageSource 稍微有些差異晶乔,會(huì)具體說下。
fallbackToSystemLocale:這是一個(gè)兜底開關(guān)牺勾。默認(rèn)情況下正罢,如果在指定語言中找不到對(duì)應(yīng)的值,會(huì)從 basename 參數(shù)(默認(rèn)是 messages.properties)中查找驻民,如果再找不到可能直接返回或拋錯(cuò)翻具。該參數(shù)設(shè)置為 true 的話,還會(huì)再走一步兜底邏輯回还,從當(dāng)前系統(tǒng)語言對(duì)應(yīng)配置文件中查找裆泳。該參數(shù)默認(rèn)是 true;
alwaysUseMessageFormat:MessageSource 組件通過
MessageFormat.format
函數(shù)對(duì)國(guó)際化信息格式化柠硕,如果注入?yún)?shù)工禾,輸出結(jié)果是經(jīng)過格式化的。比如MessageFormat.format("Hello, {0}!", "Kanshan")
輸出結(jié)果是“Hello, Kanshan!”蝗柔。該參數(shù)控制的是闻葵,當(dāng)輸入?yún)?shù)為空時(shí),是否還是使用MessageFormat.format
函數(shù)對(duì)結(jié)果進(jìn)行格式化癣丧,默認(rèn)是 false笙隙;useCodeAsDefaultMessage:當(dāng)沒有找到對(duì)應(yīng)信息的時(shí)候,是否返回 code坎缭。也就是當(dāng)找了所有能找的配置文件后竟痰,還是沒有找到對(duì)應(yīng)的信息,是否直接返回 code 值掏呼。默認(rèn)是 false坏快,即不返回 code,拋出
NoSuchMessageException
異常憎夷。
這些配置參數(shù)都有各自的默認(rèn)值莽鸿。如果沒有特殊的需求,可以直接直接按照默認(rèn)約定使用。
執(zhí)行邏輯
接下來我們看下流程圖祥得,下面的流程圖綠色部分是 cacheDuration 沒有配置的情況兔沃。對(duì)于 ResourceBundleMessageSource 是只加載一次配置文件,ReloadableResourceBundleMessageSource 會(huì)根據(jù)文件修改時(shí)間判斷是否需要重新加載级及。
ResourceBundleMessageSource 的流程圖
ReloadableResourceBundleMessageSource 的流程圖
AbstractMessageSource 的幾個(gè) getMessage 方法源碼
@Override
public 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);
}
@Override
public 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);
}
@Override
public 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);
}
第一個(gè) getMessage
方法乒疏,是可以傳入默認(rèn)值 defaultMessage
的,也就是當(dāng)所有 basename 的配置文件中不存在 code 指定的值饮焦,就會(huì)使用 defaultMessage
值進(jìn)行格式化返回怕吴。
第二個(gè) getMessage
方法,是通過判斷 useCodeAsDefaultMessage
配置县踢,如果設(shè)置了 true转绷,在所有 basename 的配置文件中不存在 code 指定的值的情況下,會(huì)返回 code 作為返回值硼啤。但是當(dāng)設(shè)置為 false 時(shí)议经,code 不存在的情況下,會(huì)拋出 NoSuchMessageException
異常谴返。
第三個(gè) getMessage
方法爸业,傳入的是 MessageSourceResolvable
接口對(duì)象,查找的 code 更加多種多樣亏镰。不過如果最后還是找不到,會(huì)拋出 NoSuchMessageException
異常拯爽。
緩存的使用
我們看源碼不僅僅是為了看功能組件的實(shí)現(xiàn)索抓,還是學(xué)習(xí)更加優(yōu)秀的編程方式。比如下面這段內(nèi)存緩存的使用毯炮,Spring 源碼中很多地方都用到了這種內(nèi)存緩存的使用方式:
// 兩層 Map逼肯,第一層是 basename,第二層是 locale
private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =
new ConcurrentHashMap<>();
@Nullable
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.
// 先從緩存中獲取第一層 basename 的緩存
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
// 如果命中第一層桃煎,在通過 locale 獲取第二層的值
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
// 如果命中第二層緩存篮幢,直接返回
return bundle;
}
}
try {
// 走到這里,說明沒有命中緩存为迈,就根據(jù) basename 和 locale 創(chuàng)建對(duì)象
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
// 如果 localeMap 為空三椿,說明第一級(jí)就不存在,通過 Map 的 computeIfAbsent 方法初始化
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
// 將新建的 ResourceBundle 對(duì)象放入 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 實(shí)現(xiàn)內(nèi)存緩存的寫法葫辐,比如我們就對(duì)上面的這個(gè)方法進(jìn)行改寫:
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 是否一致的原理搜锰,創(chuàng)建一個(gè)包含 basename、locale 的對(duì)象 BasenameLocale
耿战,然后改寫 cachedResourceBundles
為一層 Map蛋叼,會(huì)簡(jiǎn)化一些判斷邏輯。
此處的 BasenameLocale
是 record
類型,具體語法可以參考Java16 的新特性 中的 Record 類型一節(jié)狈涮。