網(wǎng)站國(guó)際化實(shí)現(xiàn)(2)—Spring MVC國(guó)際化實(shí)現(xiàn)及原理

一君珠、背景

很多網(wǎng)站的用戶分布在世界各地慷吊,因此網(wǎng)站需要針對(duì)不同國(guó)家的用戶展示不同語(yǔ)言的內(nèi)容馍惹,因此就有了國(guó)際化實(shí)現(xiàn)的需求瑞眼,大多數(shù)網(wǎng)站都會(huì)在網(wǎng)站的頭部或尾部設(shè)置語(yǔ)言切換鏈接龙宏,這樣就可以直接切換成相應(yīng)的內(nèi)容。其中有些網(wǎng)站是通過(guò)網(wǎng)站地址或參數(shù)進(jìn)行區(qū)分伤疙,有些是通過(guò)設(shè)置cookie值進(jìn)行進(jìn)行區(qū)分银酗。

二、解決思路

前面已經(jīng)寫(xiě)過(guò)一篇JDK的國(guó)際化支持徒像,講解了JDK實(shí)現(xiàn)國(guó)際化的具體實(shí)現(xiàn)黍特。那么網(wǎng)站的國(guó)際化實(shí)現(xiàn)具體如何做呢?

其實(shí)網(wǎng)站的國(guó)際化實(shí)現(xiàn)與前面介紹的JDK實(shí)現(xiàn)思路類似锯蛀,只是本地化信息的獲取需要從頁(yè)面得到而已灭衷。得到了頁(yè)面信息,再獲取對(duì)應(yīng)的數(shù)據(jù)并進(jìn)行格式化處理旁涤,最后渲染到頁(yè)面即可翔曲。這里主要說(shuō)明后端的處理思路,前端的處理思路其實(shí)也類似拭抬,只是實(shí)現(xiàn)方式有區(qū)別而已部默。

那如何從頁(yè)面獲取本地化信息呢?這個(gè)是所有處理的首要環(huán)節(jié)造虎,常用的幾種方式有:
(1)直接根據(jù)Request.getLocale()方法得到本地化信息傅蹂,實(shí)際就是從Http Request Headers里面取“accept-language”對(duì)應(yīng)的值,該值擁有瀏覽器端的語(yǔ)言信息算凿;
(2)在瀏覽器端保存一個(gè)自定義名字的cookie份蝴,默認(rèn)情況下指定一個(gè)值,對(duì)應(yīng)的切換通過(guò)語(yǔ)言切換鏈接的點(diǎn)擊修改對(duì)應(yīng)的值氓轰;
(3)在請(qǐng)求URL上面添加帶本地化信息的參數(shù)或者地址里面包含本地化信息婚夫。

通過(guò)上面幾種方式,在web程序中就可以直接從request中得到了本地化信息署鸡,然后根據(jù)本地化信息從相應(yīng)的properties文件中獲取數(shù)據(jù)(比如可以通過(guò)JDK的ResourceBundle類)案糙,得到數(shù)據(jù)后如果需要的化再對(duì)數(shù)據(jù)進(jìn)行格式化處理(比如可以通過(guò)JDK的MessageFormat類)限嫌,最后將處理過(guò)的數(shù)據(jù)展示到前臺(tái)即完成了整個(gè)國(guó)際化操作。

思路已經(jīng)有了时捌,那么具體如何實(shí)現(xiàn)呢怒医?下面以Spring MVC的實(shí)現(xiàn)為例,因?yàn)樵摽蚣茏隽撕芎玫某橄蠛头庋b奢讨,是個(gè)非常好的參考例子稚叹。

三、Spring MVC實(shí)現(xiàn)及原理

3.1 本地化信息獲取

3.1.1 概述

Spring MVC的DispatcherServlet類會(huì)在initLocaleResolver方法中查找一個(gè)locale resolver拿诸,如果沒(méi)有找到就會(huì)用默認(rèn)的AcceptHeaderLocaleResolver類扒袖。locale resolver會(huì)去根據(jù)請(qǐng)求Request設(shè)置當(dāng)前的locale信息。

除了resolver類亩码,還可以定義攔截器去設(shè)置locale信息季率,比如通過(guò)請(qǐng)求參數(shù)去設(shè)置,具體下面細(xì)講描沟。

Spring MVC相關(guān)的處理類都在org.springframework.web.servlet.i18n包下蚀同。而本地化信息的獲取可以通過(guò)RequestContext.getLocale()方法得到。另外啊掏,RequestContext.getTimeZone()方法還可以得到時(shí)區(qū)信息。

3.1.2 AcceptHeaderLocaleResolver

這個(gè)從名字也能看出大概來(lái)衰猛,這個(gè)類是解析request的header中的accept-language值迟蜜,這個(gè)值通常包含客戶端支持的本地化信息,所以通過(guò)這個(gè)值可以獲取本地化信息啡省。不過(guò)這個(gè)類拿不到時(shí)區(qū)信息娜睛。這個(gè)類是默認(rèn)配置的,所以使用的話不用額外配置卦睹。

3.1.3 CookieLocaleResolver

這個(gè)類是通過(guò)cookie去存取本地化信息畦戒,客戶端可以在cookie中存儲(chǔ)一個(gè)指定名字的值代表本地化信息,然后這個(gè)類獲取后做相應(yīng)的解析即可结序。具體的配置如下:
<code><bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<property name="cookieMaxAge" value="100000"/>
<property name="cookiePath" value="/"/>
</bean></code>
這里對(duì)幾個(gè)配置的屬性做下說(shuō)明:

屬性 默認(rèn)值 說(shuō)明
cookieName classname + LOCALE cookie名字
cookieMaxAge Servlet容器默認(rèn)值 這個(gè)值為cookie在客戶端保留的時(shí)間障斋,如果值為-1,則不保留徐鹤;這個(gè)值會(huì)在關(guān)閉瀏覽器后無(wú)效垃环。
cookiePath / 這個(gè)值設(shè)置cookie的適用路徑,如果這個(gè)值設(shè)置了返敬,那么就表示cookie只對(duì)當(dāng)前目錄及其子目錄可見(jiàn)劲赠。

3.1.4 SessionLocaleResolver

這個(gè)類是通過(guò)request獲取本地化信息的估蹄,然后存在HttpSession中,所以本地化信息存取依賴于session的生命周期。具體配置如下:
<code><bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
</bean></code>

3.1.5 LocaleChangeInterceptor

這個(gè)攔截器會(huì)攔截請(qǐng)求中的參數(shù),然后根據(jù)參數(shù)去調(diào)用LocaleResolver的setLocale()方法,改變當(dāng)前的locale值当犯。下面舉個(gè)例子宏榕,有這個(gè)地址http://www.sf.net/home.view?siteLanguage=nl,參數(shù)siteLanguage代表locale信息,配置攔截修改locale值:
<code><bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/></bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/></code>
這里配置CookieLocaleResolver是因?yàn)長(zhǎng)ocaleChangeInterceptor需要調(diào)用LocaleResolver的setLocale()方法卜壕,這個(gè)例子里面用到了CookieLocaleResolver,當(dāng)然也可以用其他的LocaleResolver實(shí)現(xiàn)類。

3.2 數(shù)據(jù)獲取與格式化

Spring MVC的數(shù)據(jù)處理定義了一個(gè)接口MessageSource,該接口定義了數(shù)據(jù)獲取的方法。方法如下:

  1. String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
    這里code為屬性文件中的key值,args是文件中需要替換的參數(shù)值,defaultMessage是找不到內(nèi)容時(shí)的默認(rèn)內(nèi)容,locale為本地化信息访诱。
  2. String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
    這個(gè)方法與上面的方法類似哲泊,只是沒(méi)有了默認(rèn)內(nèi)容催蝗,而是找不到內(nèi)容時(shí)拋出異常切威。
  3. String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
    這里參數(shù)中有個(gè)新接口MessageSourceResolvable,對(duì)前面的參數(shù)進(jìn)行了封裝丙号,locale為本地化信息先朦。

對(duì)于MessageSource接口缰冤,Spring MVC的ApplicationContext和HierarchicalMessageSource都有繼承,ApplicationContext在加載的時(shí)候喳魏,它會(huì)先去上下文里面查找bean名為messageSource的實(shí)現(xiàn)棉浸,找到后上面MessageSource方法的調(diào)用就用這個(gè)實(shí)現(xiàn)類; 如果找不到就會(huì)找包含MessageSource bean的類去使用刺彩; 再找不到就用DelegatingMessageSource去執(zhí)行方法調(diào)用了迷郑。

MessageSource常見(jiàn)的實(shí)現(xiàn)主要有如下幾個(gè):

  1. ResourceBundleMessageSource類:這個(gè)類實(shí)際是依賴的JDK的ResourceBundle類獲取數(shù)據(jù)、MessageFormat去做格式化创倔。
  2. ReloadableResourceBundleMessageSource類:這個(gè)與上面的比較就多了可重新加載嗡害,即可以在不重新啟動(dòng)應(yīng)用的情況下重新讀取新的內(nèi)容。具體實(shí)現(xiàn)方式也有區(qū)別三幻,這個(gè)類是通過(guò)Spring的PropertiesPersister策略加載就漾,依賴的是JDK的Properties類讀取內(nèi)容。
  3. StaticMessageSource類:這個(gè)類提供了簡(jiǎn)單的實(shí)現(xiàn)念搬,內(nèi)容是需要先配置好的抑堡。使用比較少,適合在內(nèi)容較少較簡(jiǎn)單情況下使用朗徊。

下面以最常用的ResourceBundleMessageSource類做個(gè)簡(jiǎn)單示例:
<code><bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
</list>
</property>
</bean></code>
format與exceptions為文件基礎(chǔ)名首妖,對(duì)應(yīng)內(nèi)容配置在具體locale相應(yīng)的文件名中即可。詳細(xì)示例見(jiàn)下面部分爷恳。

3.3 小結(jié)與示例

上面兩節(jié)主要講了本地化信息獲取有缆,數(shù)據(jù)獲取與格式化兩部分,這兩部分其實(shí)也是整個(gè)國(guó)際化過(guò)程最核心的兩個(gè)部分温亲,至于請(qǐng)求的匹配與接收棚壁,返回結(jié)果的頁(yè)面渲染這個(gè)就不展開(kāi)講,與國(guó)際化不直接相關(guān)栈虚,屬于Spring MVC的基礎(chǔ)內(nèi)容袖外。

這里對(duì)整個(gè)Spring MVC的國(guó)際化過(guò)程做個(gè)大概的梳理,整個(gè)過(guò)程大概是這樣:接收請(qǐng)求——>LocaleResolver獲取/設(shè)置locale信息——>MessageSource獲取數(shù)據(jù)并格式化——>內(nèi)容展示到頁(yè)面魂务。

講了半天曼验,還是有點(diǎn)抽象,下面直接來(lái)個(gè)詳細(xì)示例:

<code>@Controller
public class I18nController {
@Autowired
private MessageSource messageSource;
@RequestMapping("i18n")
public String i18n(Model model){
//獲取本地化信息粘姜,從LocaleContext中得到
Locale locale = LocaleContextHolder.getLocale();
//初始化參數(shù)鬓照,這里簡(jiǎn)便演示,真實(shí)參數(shù)可能是從數(shù)據(jù)庫(kù)查詢處理的孤紧。這里的參數(shù)是與i18n目錄下的配置文件需要替換的內(nèi)容對(duì)應(yīng)的
Object [] objArr = new Object[4];
objArr[0] = new Date();
objArr[1] = messageSource.getMessage("goods", null, locale);//這個(gè)具體商品從配置中讀取
objArr[2] = "taobao";
objArr[3] = new BigDecimal("39.20");
//獲取格式化后的內(nèi)容
String content = messageSource.getMessage("template", objArr, locale);
model.addAttribute("content", content);
return "/i18n/show";
}
}
</code>

LocaleResolver配置豺裆,這里以Cookie為例:
<code><bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<property name="cookieMaxAge" value="100000"/>
<property name="cookiePath" value="/"/>
</bean></code>

MessageSource配置,這里以ResourceBundleMessageSource為例:
<code> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/i18n/message</value>
</list>
</property>
</bean></code>

Properties配置号显,這里統(tǒng)一放在/i18n目錄下留储,message名字開(kāi)頭:

文件配置.png

更詳細(xì)的代碼可以查看我的Github項(xiàng)目翼抠。

四、拓展介紹

4.1 LocaleResolver對(duì)應(yīng)Bean是如何初始化的获讳?

初始化工作是在DispatcherServlet類初始化時(shí)調(diào)用initLocaleResolver方法執(zhí)行的阴颖。
<code>private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Using LocaleResolver [" + this.localeResolver + "]");
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME +
"': using default [" + this.localeResolver + "]");
}
}
}</code>
從代碼里面可以看到處理化過(guò)程分為兩步:(1)先從當(dāng)前上下文環(huán)境中取名字為localeResolver的bean; (2)如果找不到就根據(jù)默認(rèn)策略去取LocaleResolver這個(gè)Class名字的bean丐膝,即執(zhí)行g(shù)etDefaultStrategy方法量愧,該方法實(shí)際是取DispatcherServlet.properties文件中的org.springframework.web.servlet.LocaleResolver對(duì)應(yīng)的值,即默認(rèn)的AcceptHeaderLocaleResolver類帅矗,再創(chuàng)建對(duì)應(yīng)的bean偎肃。
所以如果上下文中自定義了LocaleResolver就用自定義的,沒(méi)有定義會(huì)用默認(rèn)的AcceptHeaderLocaleResolver類浑此。這種寫(xiě)法在寫(xiě)公共邏輯且提供多種策略時(shí)很實(shí)用累颂。

五、參考資料

  1. Spring MVC using locales官方教程
  2. Spring MVC using MessageSource官方教程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凛俱,一起剝皮案震驚了整個(gè)濱河市紊馏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒲犬,老刑警劉巖朱监,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異原叮,居然都是意外死亡赫编,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)奋隶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)擂送,“玉大人,你說(shuō)我怎么就攤上這事唯欣∴诙郑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵黍聂,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我身腻,道長(zhǎng)产还,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任嘀趟,我火速辦了婚禮脐区,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘她按。我一直安慰自己牛隅,他們只是感情好炕柔,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著媒佣,像睡著了一般匕累。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上默伍,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天欢嘿,我揣著相機(jī)與錄音,去河邊找鬼也糊。 笑死炼蹦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的狸剃。 我是一名探鬼主播掐隐,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钞馁!你這毒婦竟也來(lái)了虑省?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤指攒,失蹤者是張志新(化名)和其女友劉穎慷妙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體允悦,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膝擂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了隙弛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片架馋。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖全闷,靈堂內(nèi)的尸體忽然破棺而出叉寂,到底是詐尸還是另有隱情,我是刑警寧澤总珠,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布屏鳍,位于F島的核電站,受9級(jí)特大地震影響局服,放射性物質(zhì)發(fā)生泄漏钓瞭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一淫奔、第九天 我趴在偏房一處隱蔽的房頂上張望山涡。 院中可真熱鬧,春花似錦、人聲如沸鸭丛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鳞溉。三九已至瘾带,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間穿挨,已是汗流浹背月弛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留科盛,地道東北人帽衙。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像贞绵,于是被迫代替她去往敵國(guó)和親厉萝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理榨崩,服務(wù)發(fā)現(xiàn)谴垫,斷路器,智...
    卡卡羅2017閱讀 134,600評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,748評(píng)論 6 342
  • 兒子,當(dāng)你來(lái)到這個(gè)世上彩郊,發(fā)出第一聲啼哭前弯,我是多么激動(dòng),我有了一個(gè)兄弟秫逝!是的恕出,我當(dāng)時(shí)就是這么想的。那天违帆,我就在想浙巫,怎...
    泰山月色閱讀 253評(píng)論 0 0
  • 四萬(wàn)年前已經(jīng)產(chǎn)生了智人的畴,之前是沒(méi)有人這的。那人這種與其他動(dòng)物不同的動(dòng)物是如何產(chǎn)生的呢尝胆?在千萬(wàn)里的環(huán)境變化中丧裁,各種動(dòng)...
    我是小代同學(xué)閱讀 328評(píng)論 0 2
  • 原標(biāo)題:麗江游玩指南 改寫(xiě)標(biāo)題: 1、如何用3000元玩遍麗江 2班巩、你去麗江邂逅了嗎渣慕? 3、別天天待家里了抱慌,看看外...
    凌霄霄閱讀 289評(píng)論 1 0