Java Spring項目國際化(i18n)詳細方法與實例
Spring國際化概述
國際化基本規(guī)則
國際化信息”也稱為“本地化信息”,一般需要兩個條件才可以確定一個特定類型的本地化信息,它們分別是“語言類型”和“國家/地區(qū)的類型”。如中文本地化信息既有中國大陸地區(qū)的中文,又有中國臺灣、中國香港地區(qū)的中文,還有新加坡地區(qū)的中文。Java通過java.util.Locale類表示一個本地化對象,它允許通過語言參數(shù)和國家/地區(qū)參數(shù)創(chuàng)建一個確定的本地化對象。
語言參數(shù)使用ISO標(biāo)準(zhǔn)語言代碼表示,這些代碼是由ISO-639標(biāo)準(zhǔn)定義的,每一種語言由兩個小寫字母表示。在許多網(wǎng)站上都可以找到這些代碼的完整列表,下面的網(wǎng)址是提供了標(biāo)準(zhǔn)語言代碼的信息:http://www.loc.gov/standards/iso639-2/php/English_list.php。
國家/地區(qū)參數(shù)也由標(biāo)準(zhǔn)的ISO國家/地區(qū)代碼表示,這些代碼是由ISO-3166標(biāo)準(zhǔn)定義的,每個國家/地區(qū)由兩個大寫字母表示。用戶可以從以下網(wǎng)址查看ISO-3166的標(biāo)準(zhǔn)代碼:http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html,部分語言和國家/地區(qū)的標(biāo)準(zhǔn)代碼如下所示:
| 語言 | 簡稱 |
|---|---|
| 簡體中文(中國) | zh_CN |
| 繁體中文(中國臺灣) | zh_TW |
| 繁體中文(中國香港) | zh_HK |
| 英語(中國香港) | en_HK |
| 英語(美國) | en_US |
| 英語(英國) | en_GB |
| 英語(全球) | en_WW |
| 英語(加拿大) | en_CA |
| 英語(澳大利亞) | en_AU |
| 英語(愛爾蘭) | en_IE |
| 英語(芬蘭) | en_FI |
| 芬蘭語(芬蘭) | fi_FI |
| 英語(丹麥) | en_DK |
| 丹麥語(丹麥) | da_DK |
| 英語(以色列) | en_IL |
| 希伯來語(以色列) | he_IL |
| 英語(南非) | en_ZA |
| 英語(印度) | en_IN |
| 英語(挪威) | en_NO |
| 英語(新加坡) | en_SG |
| 英語(新西蘭) | en_NZ |
| 英語(印度尼西亞) | en_ID |
| 英語(菲律賓) | en_PH |
| 英語(泰國) | en_TH |
| 英語(馬來西亞) | en_MY |
| 英語(阿拉伯) | en_XA |
| 韓文(韓國) | ko_KR |
| 日語(日本) | ja_JP |
| 荷蘭語(荷蘭) | nl_NL |
| 荷蘭語(比利時) | nl_BE |
| 葡萄牙語(葡萄牙) | pt_PT |
| 葡萄牙語(巴西) | pt_BR |
| 法語(法國) | fr_FR |
| 法語(盧森堡) | fr_LU |
| 法語(瑞士) | fr_CH |
| 法語(比利時) | fr_BE |
| 法語(加拿大) | fr_CA |
| 西班牙語(拉丁美洲) | es_LA |
| 西班牙語(西班牙) | es_ES |
| 西班牙語(阿根廷) | es_AR |
| 西班牙語(美國) | es_US |
| 西班牙語(墨西哥) | es_MX |
| 西班牙語(哥倫比亞) | es_CO |
| 西班牙語(波多黎各) | es_PR |
| 德語(德國) | de_DE |
| 德語(奧地利) | de_AT |
| 德語(瑞士) | de_CH |
| 俄語(俄羅斯) | ru_RU |
| 意大利語(意大利) | it_IT |
| 希臘語(希臘) | el_GR |
| 挪威語(挪威) | no_NO |
| 匈牙利語(匈牙利) | hu_HU |
| 土耳其語(土耳其) | tr_TR |
| 捷克語(捷克共和國) | cs_CZ |
| 斯洛文尼亞語 | sl_SL |
| 波蘭語(波蘭) | pl_PL |
| 瑞典語(瑞典) | sv_SE |
| 西班牙語(智利) | es_CL |
語言類型判斷
1)基于瀏覽器語言
根據(jù)Request Headers中的Accept-language來判斷。
2)基于客戶端傳參
要求客戶端第一次(或者每次)傳遞的自定義參數(shù)值來判斷,如規(guī)定傳locale,值為zh-cn、en-us等內(nèi)容,如果只在第一次傳入則local以及timeZone先關(guān)信息要存入session或者cookie中,后面的請求語言方式則直接從兩者中取,其有效時間與session和cookie設(shè)置的生命周期關(guān)聯(lián)。
3)基于默認配置
當(dāng)獲取語言類型時沒有找到對應(yīng)類型時,會使用默認的語言類型。
語言類型保存
<!-- 定義本地化變更攔截器 --> <bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" /> <!-- 定義注解URL映射處理器,所有的請求映射關(guān)聯(lián)本地化攔截器,或者也可自定義該攔截器路徑映射--> <bean id="urlMapping" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"> <property name="interceptors" ref=" localeChangeInterceptor " /> <property name="order" value="1"></property> </bean>
基于url
該種方式需要每次都在請求的url上帶上local參數(shù),指定該次需要的語言類型,并且該方式的local解析器需要配置,如下:
<a href="xxx.do?locale=zh_CN" rel="external nofollow" >中文</a>或<a href="xxx.do?locale=en" rel="external nofollow" >英文</a> <bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver"/>
但在該配置下使用會拋Cannot change HTTP accept header - use a different locale resolution strategy異常,這是因為spring source做了限制,無法對本地的local賦值修改,解決辦法如下,新建一個類MyLocaleResolver繼承AcceptHeaderLocaleResolver,重寫resolveLocale和setLocale方法,并將上面的localeResolver的class指向如下MyLocaleResolver類:
public class MyLocaleResolver extends AcceptHeaderLocaleResolver {
private Locale myLocal;
public Locale resolveLocale(HttpServletRequest request) {
return myLocal == null ? request.getLocale() : myLocal;
}
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
myLocal = locale;
}
}
基于session
基于session的狀態(tài)保存方式只需要在第一次請求的時候指定語言類型,localResolver會將該屬性保存到session中,后面的請求直接從session中獲取該語言類型,該種方式的localResolver對應(yīng)的類為SessionLocaleResolver,如下配置:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
基于cookie
與session的機制類似,差異在于兩者的存儲和周期,鑒于安全、大小以及體驗等因素的影響,實際使用中使用者更傾向于前者,該種cookie保存方式的localResolver為
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver" />
文案數(shù)據(jù)來源
對于語言類型的資源文件,需要開發(fā)者對文案進行搜集整理,并翻譯成相應(yīng)的語言確定關(guān)鍵字key,目前大多數(shù)情況是將這些信息置于.properties文件中,在使用的時候直接訪問獲取,當(dāng)然也可置于數(shù)據(jù)庫中,但頻繁的文案獲取會影響服務(wù)器性能及產(chǎn)品體驗,可結(jié)合數(shù)據(jù)字典以及緩存工具使用。
數(shù)據(jù)庫
1)spring 配置方式
<!-- 默認的注解映射的支持 --> <mvc:annotation-driven validator="validator" conversion-service="conversionService" /> <!-- 資源文件 --> <bean id="propertiesMessageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>resource</value> <value>validation</value> </list> </property> </bean> <bean id="databaseMessageSource" class="com.obs2.util.MessageResource"> <property name="parentMessageSource" ref="propertiesMessageSource"/> </bean> <bean id="messageInterpolator" class="com.obs2.util.MessageResourceInterpolator"> <property name="messageResource" ref="databaseMessageSource"/> </bean> <!-- 驗證器 --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="messageInterpolator" ref="messageInterpolator"/> </bean>
這里定義了一個propertiesMessageSource,一個databaseMessageSourcer,和一個messageInterpolator。propertiesMessageSource用于讀取properties文件databaseMessageSourcer用于讀取數(shù)據(jù)庫的數(shù)據(jù)配置,其中,有一個屬性設(shè)置它的父MessageSource為propertiesMessageSource。意思是如果數(shù)據(jù)庫找不到對應(yīng)的數(shù)據(jù),到properties文件當(dāng)中查找。messageInterpolator是個攔截器。
2)數(shù)據(jù)庫的POJO定義
@Entity
@SuppressWarnings("serial")
@Table(name="resource")
public class Resource implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="resource_id")
private long resourceId;
@Column(name="name", length=50, nullable=false)
private String name;
@Column(name="text", length=1000, nullable=false)
private String text;
@Column(name="language", length=5, nullable=false)
private String language;
public long getResourceId() {
return resourceId;
}
public void setResourceId(long resourceId) {
this.resourceId = resourceId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
}
定義了一張表[resource],字段有:[resource_id]、[name]、[text]、[language]。
3)讀取數(shù)據(jù)庫的MessageResource類
/**
* 取得資源數(shù)據(jù)
* @author Robin
*/
public class MessageResource extends AbstractMessageSource implements ResourceLoaderAware {
@SuppressWarnings("unused")
private ResourceLoader resourceLoader;
@Resource
private ResourceService resourceService;
/**
* Map切分字符
*/
protected final String MAP_SPLIT_CODE = "|";
protected final String DB_SPLIT_CODE = "_";
private final Map<String, String> properties = new HashMap<String, String>();
public MessageResource() {
reload();
}
public void reload() {
properties.clear();
properties.putAll(loadTexts());
}
protected Map<String, String> loadTexts() {
Map<String, String> mapResource = new HashMap<String, String>();
List<com.obs2.service.bean.Resource> resources = resourceService.findAll();
for (com.obs2.service.bean.Resource item : resources) {
String code = item.getName() + MAP_SPLIT_CODE + item.getLanguage();
mapResource.put(code, item.getText());
}
return mapResource;
}
private String getText(String code, Locale locale) {
String localeCode = locale.getLanguage() + DB_SPLIT_CODE + locale.getCountry();
String key = code + MAP_SPLIT_CODE + localeCode;
String localeText = properties.get(key);
String resourceText = code;
if(localeText != null) {
resourceText = localeText;
}else {
localeCode = Locale.ENGLISH.getLanguage();
key = code + MAP_SPLIT_CODE + localeCode;
localeText = properties.get(key);
if(localeText != null) {
resourceText = localeText;
}else {
try {
if(getParentMessageSource() != null) {
resourceText = getParentMessageSource().getMessage(code, null, locale);
}
} catch (Exception e) {
logger.error("Cannot find message with code: " + code);
}
}
}
return resourceText;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
String msg = getText(code, locale);
MessageFormat result = createMessageFormat(msg, locale);
return result;
}
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
String result = getText(code, locale);
return result;
}
}
主要是重載AbstractMessageSource和ResourceLoaderAware,以實現(xiàn)Spring MVC的MessageSource國際化調(diào)用。類中的reload()方法,我把它寫到了一個ServletListener當(dāng)中,讓項目啟動時,自動加載數(shù)據(jù)到static的map中。
4)Listener
/**
* 系統(tǒng)啟動監(jiān)聽
* @author Robin
*/
public class SystemListener implements ServletContextListener {
/**
* context初始化時激發(fā)
*/
@Override
public void contextInitialized(ServletContextEvent e) {
// 取得ServletContext
ServletContext context = e.getServletContext();
WebApplicationContext applicationContext = WebApplicationContextUtils .getWebApplicationContext(context);
// 設(shè)置國際化多語言
MessageResource messageSource = applicationContext.getBean(MessageResource.class);
messageSource.reload();
}
/**
* context刪除時激發(fā)
*/
@Override
public void contextDestroyed(ServletContextEvent e) {
}
/**
* 創(chuàng)建一個 session時激發(fā)
* @param e
*/
public void sessionCreated(HttpSessionEvent e) {
}
/**
* 當(dāng)一個 session失效時激發(fā)
* @param e
*
public void sessionDestroyed(HttpSessionEvent e) {
}
/**
* 設(shè)置 context的屬性,它將激發(fā)attributeReplaced或attributeAdded方法
* @param e
*/
public void setContext(HttpSessionEvent e) {
}
/**
* 增加一個新的屬性時激發(fā)
* @param e
*/
public void attributeAdded(ServletContextAttributeEvent e) {
}
/**
*刪除一個新的屬性時激發(fā)
* @param e
*/
public void attributeRemoved(ServletContextAttributeEvent e) {
}
/*
* 屬性被替代時激發(fā)
* @param e
*/
public void attributeReplaced(ServletContextAttributeEvent e) {
}
}
該Listener需要加入到web.xml當(dāng)中:
<!-- 系統(tǒng)啟動監(jiān)聽 --> <listener> <listener-class>com.obs2.util.SystemListener</listener-class> </listener>
5)Interceptor攔截器
/**
* 攔截Annotation驗證信息
* @author Robin
*
*/
public class MessageResourceInterpolator implements MessageInterpolator {
@Resource
private MessageResource messageResource;
public void setMessageResource(MessageResource messageResource) {
this.messageResource = messageResource;
}
@Override
public String interpolate(String messageTemplate, Context context) {
String messageTemp = null;
if(messageTemplate.startsWith("{") && messageTemplate.endsWith("}")) {
messageTemp = messageTemplate.substring(1, messageTemplate.length() - 1);
}else {
return messageTemplate;
}
String[] params = (String[]) context.getConstraintDescriptor().getAttributes().get("params");
MessageBuilder builder = new MessageBuilder().code(messageTemp);
if (params != null) {
for (String param : params) {
builder = builder.arg(param);
}
}
String result = builder.build().resolveMessage(messageResource, Locale.ENGLISH).getText();
return result;
}
@Override
public String interpolate(String messageTemplate, Context context, Locale locale) {
String messageTemp = null;
if(messageTemplate.startsWith("{") && messageTemplate.endsWith("}")) {
messageTemp = messageTemplate.substring(1, messageTemplate.length() - 1);
}else {
return messageTemplate;
}
String[] params = (String[]) context.getConstraintDescriptor().getAttributes().get("params");
MessageBuilder builder = new MessageBuilder().code(messageTemp);
if (params != null) {
builder = builder.args(params);
}
String result = builder.build().resolveMessage(messageResource, locale).getText();
return result
}
}
靜態(tài)資源
<!-- 資源文件綁定器,文件名稱:messages.properties(沒有找到時的默認文件), messages_en.properties(英文),messages_zh_CN.properties(中文),等等--> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="config.messages.messages" /> <property name="defaultEncoding" value="UTF-8"/> <property name="basename" value="i18n.messages"/> <property name="useCodeAsDefaultMessage" value="true" /> </bean>
文案獲取
資源獲取接口
MessageSource詳解
Spring定義了訪問國際化信息的MessageSource接口,并提供了幾個易用的實現(xiàn)類。首先來了解一下該接口的幾個重要方法:
1)String getMessage(String code, Object[] args, String defaultMessage, Locale locale) code
表示國際化資源中的屬性名;args用于傳遞格式化串占位符所用的運行期參數(shù);當(dāng)在資源找不到對應(yīng)屬性名時,返回defaultMessage參數(shù)所指定的默認信息;locale表示本地化對象;
2)String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException
與上面的方法類似,只不過在找不到資源中對應(yīng)的屬性名時,直接拋出NoSuchMessageException異常;
3)String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException
MessageSourceResolvable 將屬性名、參數(shù)數(shù)組以及默認信息封裝起來,它的功能和第一個接口方法相同。
MessageSource類結(jié)構(gòu)
MessageSource分別被HierarchicalMessageSource和ApplicationContext接口擴展,這里我們主要看一下HierarchicalMessageSource接口的幾個實現(xiàn)類

HierarchicalMessageSource接口添加了兩個方法,建立父子層級的MessageSource結(jié)構(gòu),類似于前面我們所介紹的HierarchicalBeanFactory。該接口的setParentMessageSource (MessageSource parent)方法用于設(shè)置父MessageSource,而getParentMessageSource()方法用于返回父MessageSource。
HierarchicalMessageSource接口最重要的兩個實現(xiàn)類是ResourceBundleMessageSource和ReloadableResourceBundleMessageSource。它們基于Java的ResourceBundle基礎(chǔ)類實現(xiàn),允許僅通過資源名加載國際化資源。ReloadableResourceBundleMessageSource提供了定時刷新功能,允許在不重啟系統(tǒng)的情況下,更新資源的信息。StaticMessageSource主要用于程序測試,它允許通過編程的方式提供國際化信息。而DelegatingMessageSource是為方便操作父MessageSource而提供的代理類。
ResourceBundleMessageSource與ReloadableResourceBundleMessageSource對比
1)通過ResourceBundleMessageSource配置資源
<bean id=" messageSource " class="org.springframework.context.support.ResourceBundleMessageSource"> <!--①通過基名指定資源,相對于類根路徑--> <property name="basenames"> <list> <value>com/baobaotao/i18n/fmt_resource</value> </list> </property> </bean>
2)通過ReloadableResourceBundleMessageSource配置資源
<bean id="messageSource " class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames"> <list> <value>com/baobaotao/i18n/fmt_resource</value> </list> </property> <!--①刷新資源文件的周期,以秒為單位--> <property name="cacheSeconds" value="5"/> </bean>
3)對比
兩者都是利用資源名通過getMessage()接口就可以加載整套的國際化資源文件,唯一區(qū)別在于ReloadableResourceBundleMessageSource可以定時刷新資源文件,以便在應(yīng)用程序不重啟的情況下感知資源文件的變化。很多生產(chǎn)系統(tǒng)都需要長時間持續(xù)運行,系統(tǒng)重啟會給運行帶來很大的負面影響,這時通過該實現(xiàn)類就可以解決國際化信息更新的問題。上面的配置中cacheSeconds屬性讓ReloadableResourceBundleMessageSource每5秒鐘刷新一次資源文件(在真實的應(yīng)用中,刷新周期不能太短,否則頻繁的刷新將帶來性能上的負面影響,一般不建議小于30分鐘)。cacheSeconds默認值為-1表示永不刷新,此時,該實現(xiàn)類的功能就蛻化為ResourceBundleMessageSource的功能。
頁面獲取文案
利用Spring標(biāo)簽獲取
引入標(biāo)簽庫:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
獲取文案:
<s:message code="test.app"/> <spring:message code="main.title" />
利用JSTL標(biāo)簽獲取
引入標(biāo)簽庫:
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt"%>
獲取文案:
<fmt:message key="test.app"/>
Java代碼中獲取文案
利用MessageSource接口獲取
1)自動注入
@Autowired
private MessageSource messageSource;
String s = messageSource.getMessage("SystemError", new Object[]{}, Locale.US);
2)手動bean獲取
a. 獲取容器
容器已經(jīng)初始化:
WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
容器沒有初始化:
String[] configs = {"com/baobaotao/i18n/beans.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
b. 獲取bean跟文案
MessageSource ms = (MessageSource) wac.getBean("myResource");
Object[] params = {"John", new GregorianCalendar().getTime()};
String str1 = ms.getMessage("greeting.common",params,Locale.US);
利用Spring容器獲取
在前面的MessageSource類圖結(jié)構(gòu)中我們發(fā)現(xiàn)ApplicationContext實現(xiàn)了MessageSource的接口,也就是說ApplicationContext的實現(xiàn)類本身也是一個MessageSource對象。
將ApplicationContext和MessageSource整合起來, Spring此處的設(shè)計人為:在一般情況下,國際化信息資源應(yīng)該是容器級。我們一般不會將MessageSource作為一個Bean注入到其他的Bean中,相反MessageSource作為容器的基礎(chǔ)設(shè)施向容器中所有的Bean開放。只要我們考察一下國際化信息的實際消費場所就更能理解Spring這一設(shè)計的用意了。國際化信息一般在系統(tǒng)輸出信息時使用,如Spring MVC的頁面標(biāo)簽,控制器Controller等,不同的模塊都可能通過這些組件訪問國際化信息,因此Spring就將國際化消息作為容器的公共基礎(chǔ)設(shè)施對所有組件開放。
既然一般情況下我們不會直接通過引用MessageSource Bean使用國際信息,那如何聲明容器級的國際化信息呢? Spring容器啟動過程時,在初始化容器的時候通過initMessageSource()方法所執(zhí)行的工作就是初始化容器中的國際化信息資源,它根據(jù)反射機制從BeanDefinitionRegistry中找出名稱為“messageSource”且類型為org.springframework.context.MessageSource的Bean,將這個Bean定義的信息資源加載為容器級的國際化信息資源。請看下面的配置:
<!--注冊資源Bean,其Bean名稱只能為messageSource --> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>com/baobaotao/i18n/fmt_resource</value> </list> </property> </bean>
然后通過ApplicationContext直接訪問國際化信息:
a. 獲取容器
容器已經(jīng)初始化:
WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
容器沒有初始化:
String[] configs = {"com/baobaotao/i18n/beans.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
b. 獲取bean跟文案
Object[] params = {"John", new GregorianCalendar().getTime()};
String str1 = ctx.getMessage("greeting.common",params,Locale.US);
注意事項
1)編碼問題
a. 改變properties文件編碼為UTF-8/GBK,然而ResourceBundleMessageSource的默認編碼defaultEncoding是ISO-8859-1,需要在xml中增加一個相應(yīng)屬性將其改變?yōu)槟阈枰腢TF-8/GBK之類。
b. 如果資源文件想統(tǒng)一使用ISO-8859-1格式,可以將原本用UTF-8寫好的中文資源文件使用jdk自帶的工具native2ascii將UTF-8文件和內(nèi)容轉(zhuǎn)為ISO-8859-1文件,其中的中文內(nèi)容會使用16進制unicode編碼為\u****格式:
cmd命令:
JAVA_HOME\bin\native2ascii -encoding UTF-8 messages_zh_CN.properties messages_zh_C1N.properties
本文主要講解了Java Spring項目國際化(i18n)詳細方法與實例,更多關(guān)于Java Spring項目國際化技巧請查看下面的相關(guān)鏈接
相關(guān)文章
Python函數(shù)必須先定義,后調(diào)用說明(函數(shù)調(diào)用函數(shù)例外)
這篇文章主要介紹了Python函數(shù)必須先定義,后調(diào)用說明(函數(shù)調(diào)用函數(shù)例外),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06

