SpringBoot 實(shí)戰(zhàn):國(guó)際化組件 MessageSource 的執(zhí)行邏輯與源碼

配置文件

配置文件是基礎(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)化一些判斷邏輯。

此處的 BasenameLocalerecord 類型,具體語法可以參考Java16 的新特性 中的 Record 類型一節(jié)狈涮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末狐胎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子歌馍,更是在濱河造成了極大的恐慌握巢,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骆姐,死亡現(xiàn)場(chǎng)離奇詭異镜粤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)玻褪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門肉渴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人带射,你說我怎么就攤上這事同规。” “怎么了窟社?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵券勺,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我灿里,道長(zhǎng)关炼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任匣吊,我火速辦了婚禮儒拂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘色鸳。我一直安慰自己社痛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布命雀。 她就那樣靜靜地躺著蒜哀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吏砂。 梳的紋絲不亂的頭發(fā)上撵儿,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音狐血,去河邊找鬼统倒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛氛雪,可吹牛的內(nèi)容都是我干的房匆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼浴鸿!你這毒婦竟也來了井氢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤岳链,失蹤者是張志新(化名)和其女友劉穎花竞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掸哑,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡约急,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了苗分。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厌蔽。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖摔癣,靈堂內(nèi)的尸體忽然破棺而出奴饮,到底是詐尸還是另有隱情,我是刑警寧澤择浊,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布戴卜,位于F島的核電站,受9級(jí)特大地震影響琢岩,放射性物質(zhì)發(fā)生泄漏投剥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一担孔、第九天 我趴在偏房一處隱蔽的房頂上張望江锨。 院中可真熱鬧,春花似錦攒磨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谒府,卻和暖如春拼坎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背完疫。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工泰鸡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人壳鹤。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓盛龄,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子余舶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容