一、背景
很多網站的用戶分佈在世界各地,因此網站需要針對不同國家的用戶展示不同語言的內容,因此就有了國際化實現的需求,大多數網站都會在網站的頭部或尾部設置語言切換鏈接,這樣就可以直接切換成相應的內容。其中有些網站是通過網站地址或參數進行區分,有些是通過設置cookie值進行進行區分。
二、解決思路
前面已經寫過一篇JDK的國際化支持,講解了JDK實現國際化的具體實現。那麼網站的國際化實現具體如何做呢?
其實網站的國際化實現與前面介紹的JDK實現思路類似,只是本地化信息的獲取需要從頁面得到而已。得到了頁面信息,再獲取對應的數據並進行格式化處理,最後渲染到頁面即可。這裏主要說明後端的處理思路,前端的處理思路其實也類似,只是實現方式有區別而已。
那如何從頁面獲取本地化信息呢?這個是所有處理的首要環節,常用的幾種方式有:
(1)直接根據Request.getLocale()方法得到本地化信息,實際就是從Http Request Headers裏面取“accept-language”對應的值,該值擁有瀏覽器端的語言信息;
(2)在瀏覽器端保存一個自定義名字的cookie,默認情況下指定一個值,對應的切換通過語言切換鏈接的點擊修改對應的值;
(3)在請求URL上面添加帶本地化信息的參數或者地址裏面包含本地化信息。
通過上面幾種方式,在web程序中就可以直接從request中得到了本地化信息,然後根據本地化信息從相應的properties文件中獲取數據(比如可以通過JDK的ResourceBundle類),得到數據後如果需要的化再對數據進行格式化處理(比如可以通過JDK的MessageFormat類),最後將處理過的數據展示到前臺即完成了整個國際化操作。
思路已經有了,那麼具體如何實現呢?下面以Spring MVC的實現爲例,因爲該框架做了很好的抽象和封裝,是個非常好的參考例子。
三、Spring MVC實現及原理
3.1 本地化信息獲取
3.1.1 概述
Spring MVC的DispatcherServlet類會在initLocaleResolver方法中查找一個locale resolver,如果沒有找到就會用默認的AcceptHeaderLocaleResolver類。locale resolver會去根據請求Request設置當前的locale信息。
除了resolver類,還可以定義攔截器去設置locale信息,比如通過請求參數去設置,具體下面細講。
Spring MVC相關的處理類都在org.springframework.web.servlet.i18n包下。而本地化信息的獲取可以通過RequestContext.getLocale()方法得到。另外,RequestContext.getTimeZone()方法還可以得到時區信息。
3.1.2 AcceptHeaderLocaleResolver
這個從名字也能看出大概來,這個類是解析request的header中的accept-language值,這個值通常包含客戶端支持的本地化信息,所以通過這個值可以獲取本地化信息。不過這個類拿不到時區信息。這個類是默認配置的,所以使用的話不用額外配置。
3.1.3 CookieLocaleResolver
這個類是通過cookie去存取本地化信息,客戶端可以在cookie中存儲一個指定名字的值代表本地化信息,然後這個類獲取後做相應的解析即可。具體的配置如下:
<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>
這裏對幾個配置的屬性做下說明:
cookieName | 默認值:classname + LOCALE | cookie名字 |
cookieMaxAge | 默認值:Servlet容器默認值 | 這個值爲cookie在客戶端保留的時間,如果值爲-1,則不保留;這個值會在關閉瀏覽器後無效。 |
cookiePath | 默認值:/ | 這個值設置cookie的適用路徑,如果這個值設置了,那麼就表示cookie只對當前目錄及其子目錄可見。 |
3.1.4 SessionLocaleResolver
這個類是通過request獲取本地化信息的,然後存在HttpSession中,所以本地化信息存取依賴於session的生命週期。具體配置如下:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
</bean>
3.1.5 LocaleChangeInterceptor
這個攔截器會攔截請求中的參數,然後根據參數去調用LocaleResolver的setLocale()方法,改變當前的locale值。下面舉個例子,有這個地址http://www.sf.net/home.view?siteLanguage=nl,參數siteLanguage代表locale信息,配置攔截修改locale值:
<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"/>
這裏配置CookieLocaleResolver是因爲LocaleChangeInterceptor需要調用LocaleResolver的setLocale()方法,這個例子裏面用到了CookieLocaleResolver,當然也可以用其他的LocaleResolver實現類。
3.2 數據獲取與格式化
Spring MVC的數據處理定義了一個接口MessageSource,該接口定義了數據獲取的方法。方法如下:
- String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
這裏code爲屬性文件中的key值,args是文件中需要替換的參數值,defaultMessage是找不到內容時的默認內容,locale爲本地化信息。 - String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
這個方法與上面的方法類似,只是沒有了默認內容,而是找不到內容時拋出異常。 - String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
這裏參數中有個新接口MessageSourceResolvable,對前面的參數進行了封裝,locale爲本地化信息。
對於MessageSource接口,Spring MVC的ApplicationContext和HierarchicalMessageSource都有繼承,ApplicationContext在加載的時候,它會先去上下文裏面查找bean名爲messageSource的實現,找到後上面MessageSource方法的調用就用這個實現類; 如果找不到就會找包含MessageSource bean的類去使用; 再找不到就用DelegatingMessageSource去執行方法調用了。
MessageSource常見的實現主要有如下幾個:
- ResourceBundleMessageSource類:這個類實際是依賴的JDK的ResourceBundle類獲取數據、MessageFormat去做格式化。
- ReloadableResourceBundleMessageSource類:這個與上面的比較就多了可重新加載,即可以在不重新啓動應用的情況下重新讀取新的內容。具體實現方式也有區別,這個類是通過Spring的PropertiesPersister策略加載,依賴的是JDK的Properties類讀取內容。
- StaticMessageSource類:這個類提供了簡單的實現,內容是需要先配置好的。使用比較少,適合在內容較少較簡單情況下使用。
下面以最常用的ResourceBundleMessageSource類做個簡單示例:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
</list>
</property>
</bean>
format與exceptions爲文件基礎名,對應內容配置在具體locale相應的文件名中即可。詳細示例見下面部分。
3.3 小結與示例
上面兩節主要講了本地化信息獲取,數據獲取與格式化兩部分,這兩部分其實也是整個國際化過程最核心的兩個部分,至於請求的匹配與接收,返回結果的頁面渲染這個就不展開講,與國際化不直接相關,屬於Spring MVC的基礎內容。
這裏對整個Spring MVC的國際化過程做個大概的梳理,整個過程大概是這樣:接收請求——>LocaleResolver獲取/設置locale信息——>MessageSource獲取數據並格式化——>內容展示到頁面。
講了半天,還是有點抽象,下面直接來個詳細示例:
@Controller
public class I18nController {
@Autowired
private MessageSource messageSource;
@RequestMapping("i18n")
public String i18n(Model model){
//獲取本地化信息,從LocaleContext中得到
Locale locale = LocaleContextHolder.getLocale();
//初始化參數,這裏簡便演示,真實參數可能是從數據庫查詢處理的。這裏的參數是與i18n目錄下的配置文件需要替換的內容對應的
Object [] objArr = new Object[4];
objArr[0] = new Date();
objArr[1] = messageSource.getMessage("goods", null, locale);//這個具體商品從配置中讀取
objArr[2] = "taobao";
objArr[3] = new BigDecimal("39.20");
//獲取格式化後的內容
String content = messageSource.getMessage("template", objArr, locale);
model.addAttribute("content", content);
return "/i18n/show";
}
}
LocaleResolver配置,這裏以Cookie爲例:
<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>
MessageSource配置,這裏以ResourceBundleMessageSource爲例:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/i18n/message</value>
</list>
</property>
</bean>
Properties配置,這裏統一放在/i18n目錄下,message名字開頭:
更詳細的代碼可以查看我的Github項目。
四、拓展介紹
4.1 LocaleResolver對應Bean是如何初始化的?
初始化工作是在DispatcherServlet類初始化時調用initLocaleResolver方法執行的。
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 + "]");
}
}
}
從代碼裏面可以看到處理化過程分爲兩步:
(1)先從當前上下文環境中取名字爲localeResolver的bean;
(2)如果找不到就根據默認策略去取LocaleResolver這個Class名字的bean,即執行getDefaultStrategy方法,該方法實際是取DispatcherServlet.properties文件中的org.springframework.web.servlet.LocaleResolver對應的值,即默認的AcceptHeaderLocaleResolver類,再創建對應的bean。
所以如果上下文中自定義了LocaleResolver就用自定義的,沒有定義會用默認的AcceptHeaderLocaleResolver類。這種寫法在寫公共邏輯且提供多種策略時很實用。