1. 程式人生 > >javaSE國際化+springMVC當中的國際化深入理解)

javaSE國際化+springMVC當中的國際化深入理解)

假設我們正在開發一個支援多國語言的Web應用程式,要求系統能夠根據客戶端的系統的語言型別返回對應的介面:英文的作業系統返回英文介面,而中文的作業系統則返回中文介面——這便是典型的i18n國際化問題。對於有國際化要求的應用系統,我們不能簡單地採用硬編碼的方式編寫使用者介面資訊、報錯資訊等內容,而必須為這些需要國際化的資訊進行特殊處理。簡單來說,就是為每種語言提供一套相應的資原始檔,並以規範化命名的方式儲存在特定的目錄中,由系統自動根據客戶端語言選擇適合的資原始檔。

基礎知識

  “國際化資訊”也稱為“本地化資訊”,一般需要兩個條件才可以確定一個特定型別的本地化資訊,它們分別是“語言型別”和“國家/地區的型別”。如中文字地化資訊既有中國大陸地區的中文,又有中國臺灣、中國香港地區的中文,還有新加坡地區的中文。Java通過java.util.Locale類表示一個本地化物件,它允許通過語言引數和國家/地區引數建立一個確定的本地化物件。
  語言引數使用ISO標準語言程式碼表示,這些程式碼是由ISO-639標準定義的,每一種語言由兩個小寫字母表示。在許多網站上都可以找到這些程式碼的完整列表,下面的網址是提供了標準語言程式碼的資訊:
http://www.loc.gov/standards/iso639-2/php/English_list.php

  國家/地區引數也由標準的ISO國家/地區程式碼表示,這些程式碼是由ISO-3166標準定義的,每個國家/地區由兩個大寫字母表示。使用者可以從以下網址檢視ISO-3166的標準程式碼:http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html

  表5-2給出了一些語言和國家/地區的標準程式碼:


Locale

  Java.util.Locale是表示語言和國家/地區資訊的本地化類,它是建立國際化應用的基礎。下面給出幾個建立本地化物件的示例:

  1. //①帶有語言和國家/地區資訊的本地化物件  
  2. Locale locale1 = new Locale("zh","CN");     
  3. //②只有語言資訊的本地化物件  
  4. Locale locale2 = new Locale("zh");     
  5. //③等同於Locale("zh","CN")  
  6. Locale locale3 = Locale.CHINA;     
  7. //④等同於Locale("zh")  
  8. Locale locale4 = Locale.CHINESE;     
  9. //⑤獲取本地系統預設的本地化物件  
  10. Locale locale 5= Locale.getDefault();    
  使用者既可以同時指定語言和國家/地區引數定義一個本地化物件①,也可以僅通過語言引數定義一個泛本地化物件②。Locale類中通過靜態常量定義了一些常用的本地化物件,③和④處就直接通過引用常量返回本地化物件。此外,使用者還可以獲取系統預設的本地化物件,如⑤所示。
  在測試時,如果希望改變系統預設的本地化設定,可以在啟動JVM時通過命令引數指定:java -Duser.language=en -Duser.region=US MyTest。

本地化工具類

  JDK的java.util包中提供了幾個支援本地化的格式化操作工具類:NumberFormat、DateFormat、MessageFormat。下面,我們分別通過例項瞭解它們的用法:
  NumberFormat:

  1. Locale locale = new Locale("zh""CN");    
  2. NumberFormat currFmt = NumberFormat.getCurrencyInstance(locale);    
  3. double amt = 123456.78;    
  4. System.out.println(currFmt.format(amt));  
  上面的例項通過NumberFormat按本地化的方式對貨幣金額進行格式化操作,執行例項,輸出以下資訊:
  1. ¥123,456.78  
  DateFormat:
  1. Locale locale = new Locale("en""US");    
  2. Date date = new Date();    
  3. DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);    
  4. System.out.println(df.format(date));  
  通過DateFormat#getDateInstance(int style,Locale locale)方法按本地化的方式對日期進行格式化操作。該方法第一個入參為時間樣式,第二個入參為本地化物件。執行以上程式碼,輸出以下資訊:
  1. Jan 8, 2007  
  MessageFormat在NumberFormat和DateFormat的基礎上提供了強大的佔位符字串的格式化功能,它支援時間、貨幣、數字以及物件屬性的格式化操作。下面的例項演示了一些常見的格式化功能:
  MessageFormat:
  1. //①資訊格式化串  
  2. String pattern1 = "{0},你好!你於 {1} 在工商銀行存入 {2} 元。";  
  3. String pattern2 = "At {1,time,short} On {1,date,long},{0} paid {2,number, currency}.";  
  4. //②用於動態替換佔位符的引數  
  5. Object[] params = {"John"new GregorianCalendar().getTime(), 1.0E3};  
  6. //③使用預設本地化物件格式化資訊  
  7. String msg1 = MessageFormat.format(pattern1, params);  
  8. //④使用指定的本地化物件格式化資訊  
  9. MessageFormat mf = new MessageFormat(pattern2, Locale.US);  
  10. String msg2 = mf.format(params);  
  11. System.out.println(msg1);  
  12. System.out.println(msg2);  
  pattern1是簡單形式的格式化資訊串,通過{n}佔位符指定動態引數的替換位置索引,{0}表示第一個引數,{1}表示第二個引數,以此類推。
  pattern2格式化資訊串比較複雜一些,除引數位置索引外,還指定了引數的型別和樣式。從pattern2中可以看出格式化資訊串的語法是很靈活的,一個引數甚至可以出現在兩個地方:如 {1,time,short}表示從第二個入參中獲取時間部分的值,顯示為短樣式時間;而{1,date,long}表示從第二個入參中獲取日期部分的值,顯示為長樣式時間。關於MessageFormat更詳細的使用方法,請參見JDK的Javadoc。
  在②處,定義了用於替換格式化佔位符的動態引數,這裡,我們使用到了JDK5.0自動裝包的語法,否則必須採用封裝類表示基本型別的引數值。
  在③處,通過MessageFormat的format()方法格式化資訊串。它使用了系統預設的本地化物件,由於我們是中文平臺,因此預設為Locale.CHINA。而在④處,我們顯式指定MessageFormat的本地化物件。
  執行上面的程式碼,輸出以下資訊:
  1. John,你好!你於 14-7-7 下午11:29 在工商銀行存入 1,000 元。  
  2. At 11:29 PM On July 7, 2014,John paid $1,000.00.  
  如果應用系統中某些資訊需要支援國際化功能,則必須為希望支援的不同本地化型別分別提供對應的資原始檔,並以規範的方式進行命名。國際化資原始檔的命名規範規定資源名稱採用以下的方式進行命名:

  <資源名>_<語言程式碼>_<國家/地區程式碼>.properties
  其中,語言程式碼和國家/地區程式碼都是可選的。<資源名>.properties命名的國際化資原始檔是預設的資原始檔,即某個本地化型別在系統中找不到對應的資原始檔,就採用這個預設的資原始檔。<資源名>_<語言程式碼>.properties命名的國際化資原始檔是某一語言預設的資原始檔,即某個本地化型別在系統中找不到精確匹配的資原始檔,將採用相應語言預設的資原始檔。
  舉一個例子:假設資源名為resource,則語言為英文,國家為美國,則與其對應的本地化資原始檔命名為resource_en_US.properties。資訊在資原始檔以屬性名/值的方式表示:

  1. greeting.common=How are you!  
  2. greeting.morning = Good morning!  
  3. greeting.afternoon = Good Afternoon!  
  對應語言為中文,國家/地區為中國大陸的本地化資原始檔則命名為resource_zh_ CN.properties,資原始檔內容如下:
  1. greeting.common=\u60a8\u597d\uff01  
  2. greeting.morning=\u65e9\u4e0a\u597d\uff01  
  3. greeting.afternoon=\u4e0b\u5348\u597d\uff01  
  本地化不同的同一資原始檔,雖然屬性值各不相同,但屬性名卻是相同的,這樣應用程式就可以通過Locale物件和屬性名精確呼叫到某個具體的屬性值了。
  讀者可能已經注意到,上面中文的本地化資原始檔內容採用了特殊的編碼表示中文字元,這是因為資原始檔對檔案內容有嚴格的要求:只能包含ASCII字元。所以必須將非ASCII字元的內容轉換為Unicode程式碼的表示方式。如上面中文的resource_zh_CN.properties資原始檔的三個屬性值分別是“您好!”、“早上好!”和“下午好!”三個中文字串對應的Unicode程式碼串。
  如果在應用開發時,直接採用Unicode程式碼編輯資原始檔是很不方便的,所以,通常我們直接使用正常的方式編寫資原始檔,在測試或部署時再採用工具進行轉換。JDK在bin目錄下為我們提供了一個完成此項功能的native2ascii工具,它可以將中文字元的資原始檔轉換為Unicode程式碼格式的檔案,命令格式如下:
  native2ascii [-reverse] [-encoding 編碼] [輸入檔案 [輸出檔案]]

  resource_zh_CN.properties包含中文字元並且以UTF-8進行編碼,假設將該資原始檔放到d:\目錄下,通過下面的命令就可以將其轉換為Unicode程式碼的形式:

  1. D:\>native2ascii -encoding utf-8 d:\resource_zh_CN.properties  
  2. d:\resource_zh_CN_1.properties  
  由於原資原始檔採用UTF-8編碼,所以必須顯式通過-encoding指定編碼格式。

  通過native2ascii命令手工轉換資原始檔,不但在操作上不方便,轉換後資原始檔中的屬性內容由於採用了ASCII編碼,閱讀起來也不方便。很多IDE開發工具都有屬性編輯器的外掛,外掛會自動將資原始檔內容轉換為ASCII形式的編碼,同時以正常的方式閱讀和編輯資原始檔的內容,這給開發和維護帶來了很大的便利。對於MyEclipse來說,使用MyEclipse Properties Editor編輯資源屬性檔案;對於Intellij IDEA來說,無須安裝任何外掛就自然支援資源屬性檔案的這種編輯方式了。

  如果應用程式中擁有大量的本地化資原始檔,直接通過傳統的File操作資原始檔顯然太過笨拙。Java為我們提供了用於載入本地化資原始檔的方便類java.util.ResourceBoundle。
  ResourceBoundle為載入及訪問資原始檔提供便捷的操作,下面的語句從相對於類路徑的目錄中載入一個名為resource的本地化資原始檔:

  1. ResourceBundle rb = ResourceBundle.getBundle("com/baobaotao/i18n/resource", locale)  
  通過以下的程式碼即可訪問資原始檔的屬性值:
  1. rb.getString("greeting.common")   
  來看下面的例項:
  1. ResourceBundle rb1 = ResourceBundle.getBundle("com/baobaotao/i18n/resource", Locale.US);  
  2. ResourceBundle rb2 = ResourceBundle.getBundle("com/baobaotao/i18n/resource", Locale.CHINA);  
  3. System.out.println("us:"+rb1.getString("greeting.common"));  
  4. System.out.println("cn:"+rb2.getString("greeting.common"));  
  rb1載入了對應美國英語本地化的resource_en_US.properties資原始檔;而rb2載入了對應中國大陸中文的resource_zh_CN.properties資原始檔。執行上面的程式碼,將輸出以下資訊:
  1. us:How are you!  
  2. cn:你好!  
  載入資原始檔時,如果不指定本地化物件,將使用本地系統預設的本地化物件。所以,在中文系統中,ResourceBundle.getBundle("com/baobaotao/i18n/resource")語句也將返回和程式碼清單5-14中rb2相同的本地化資源。
  ResourceBundle在載入資源時,如果指定的本地化資原始檔不存在,它按以下順序嘗試載入其他的資源:本地系統預設本地化物件對應的資源→預設的資源。上面的例子中,假設我們使用ResourceBundle.getBundle("com/baobaotao/i18n/resource",Locale.CANADA)載入資源,由於不存在resource_en_CA.properties資原始檔,它將嘗試載入resource_zh_CN.properties的資原始檔,假設resource_zh_CN.properties資原始檔也不存在,它將繼續嘗試載入resource.properties的資原始檔,如果這些資源都不存在,將丟擲java.util.MissingResourceException異常。

在資原始檔中使用格式化串



  在上面的資原始檔中,屬性值都是一般的字串,它們不能結合執行時的動態引數構造出靈活的資訊,而這種需求是很常見的。要解決這個問題很簡單,只須使用帶佔位符的格式化串作為資原始檔的屬性值並結合使用MessageFormat就可以滿足要求了。
  上面的例子中,我們僅向用戶提供一般性問候,下面我們對資原始檔進行改造,通過格式化串讓問候語更具個性化:

  1. greeting.common=How are you!{0},today is {1}  
  2. greeting.morning = Good morning!{0},now is {1 time short}  
  3. greeting.afternoon = Good Afternoon!{0} now is {1 date long}  
  將該資原始檔儲存在fmt_resource_en_US.properties中,按照同樣的方式編寫對應的中文字地化資原始檔fmt_resource_zh_CN.properties。
  下面,我們聯合使用ResourceBoundle和MessageFormat得到美國英文的本地化問候語:
  1. //①載入本地化資源  
  2. ResourceBundle rb1 =     
  3.              ResourceBundle.getBundle("com/baobaotao/i18n/fmt_ resource",Locale.US);     
  4. ResourceBundle rb2 =     
  5.               ResourceBundle.getBundle("com/baobaotao/i18n/fmt_ resource",Locale.CHINA);    
  6. Object[] params = {"John"new GregorianCalendar().getTime()};    
  7. String str1 = new MessageFormat(rb1.getString("greeting.common"),Locale.US).format(params);    
  8. String str2 =new MessageFormat(rb2.getString("greeting.morning"),Locale.CHINA).format(params);    
  9. String str3 =new MessageFormat(rb2.getString("greeting.afternoon"),Locale.CHINA).format(params);    
  10. System.out.println(str1);    
  11. System.out.println(str2);    
  12. System.out.println(str3);  
  執行以上的程式碼,將輸出以下資訊:
  1. How are you!John,today is 1/9/07 4:11 PM  
  2. 早上好!John,現在是下午4:11  
  3. 下午好!John,現在是2007年1月9日  

MessageSource


  spring定義了訪問國際化資訊的MessageSource介面,並提供了幾個易用的實現類。首先來了解一下該介面的幾個重要方法:

  • String getMessage(String code, Object[] args, String defaultMessage, Locale locale) code表示國際化資源中的屬性名;args用於傳遞格式化串佔位符所用的執行期引數;當在資源找不到對應屬性名時,返回defaultMessage引數所指定的預設資訊;locale表示本地化物件;
  • String getMessage(String code, Object[] args, Locale locale)  throws NoSuchMessageException
    與上面的方法類似,只不過在找不到資源中對應的屬性名時,直接丟擲NoSuchMessageException異常;
  • String getMessage(MessageSourceResolvable resolvable, Locale locale)  throws NoSuchMessageException
    MessageSourceResolvable 將屬性名、引數陣列以及預設資訊封裝起來,它的功能和第一個介面方法相同。

MessageSource的類結構


  MessageSource分別被HierarchicalMessageSource和ApplicationContext介面擴充套件,這裡我們主要看一下HierarchicalMessageSource介面的幾個實現類,如圖5-7所示:


  HierarchicalMessageSource介面添加了兩個方法,建立父子層級的MessageSource結構,類似於前面我們所介紹的HierarchicalBeanFactory。該介面的setParentMessageSource (MessageSource parent)方法用於設定父MessageSource,而getParentMessageSource()方法用於返回父MessageSource。
  HierarchicalMessageSource介面最重要的兩個實現類是ResourceBundleMessageSource和ReloadableResourceBundleMessageSource。它們基於Java的ResourceBundle基礎類實現,允許僅通過資源名載入國際化資源。ReloadableResourceBundleMessageSource提供了定時重新整理功能,允許在不重啟系統的情況下,更新資源的資訊。StaticMessageSource主要用於程式測試,它允許通過程式設計的方式提供國際化資訊。而DelegatingMessageSource是為方便操作父MessageSource而提供的代理類。

ResourceBundleMessageSource

  該實現類允許使用者通過beanName指定一個資源名(包括類路徑的全限定資源名),或通過beanNames指定一組資源名。在前面的程式碼清單中,我們通過JDK的基礎類完成了本地化的操作,下面我們使用ResourceBundleMessageSource來完成相同的任務。讀者可以比較兩者的使用差別,並體會Spring所提供的國際化處理功能所帶給我們的好處:

  通過ResourceBundleMessageSource配置資源

  1. <beanid="myResource"
  2. class="org.springframework.context.support.ResourceBundleMessageSource">
  3.     <!--①通過基名指定資源,相對於類根路徑-->
  4.     <propertyname="basenames">
  5.        <list>
  6.           <value>com/baobaotao/i18n/fmt_resource</value>
  7.        </list>
  8.     </property>
  9.   </bean>
  啟動Spring容器,並通過MessageSource訪問配置的國際化資源,如下程式碼清單所示:
  訪問國際化訊息:ResourceBundleMessageSource:
  1. String[] configs = {"com/baobaotao/i18n/beans.xml"};    
  2. ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);    
  3. //①獲取MessageSource的Bean  
  4. MessageSource ms = (MessageSource)ctx.getBean("myResource");     
  5. Object[] params = {"John"new GregorianCalendar().getTime()};    
  6. //②獲取格式化的國際化資訊  
  7. String str1 = ms.getMessage("greeting.common",params,Locale.US);    
  8. String str2 = ms.getMessage("greeting.morning",params,Locale.CHINA);    
  9. String str3 = ms.getMessage("greeting.afternoon",params,Locale.CHINA);    
  10. System.out.println(str1);    
  11. System.out.println(str2);    
  12. System.out.println(str3);  
  比較程式碼清單中的程式碼,我們發現最主要的區別在於我們無須再分別載入不同語言、不同國家/地區的本地化資原始檔,僅僅通過資源名就可以載入整套的國際化資原始檔。此外,我們無須顯式使用MessageFormat操作國際化資訊,僅通過MessageSource# getMessage()方法就可以完成操作了。這段程式碼的執行結果與前面的程式碼的執行結果完全一樣。

ReloadableResourceBundleMessageSource

  前面,我們提到該實現類比之於ResourceBundleMessageSource的唯一區別在於它可以定時重新整理資原始檔,以便在應用程式不重啟的情況下感知資原始檔的變化。很多生產系統都需要長時間持續執行,系統重啟會給執行帶來很大的負面影響。這時,通過該實現類就可以解決國際化資訊更新的問題。請看下面的配置:
  通過ReloadableResourceBundleMessageSource配置資源:

  1. <beanid="myResource"
  2. lass="org.springframework.context.support.ReloadableResourceBundleMessageSource">
  3.    <propertyname="basenames">
  4.       <list>
  5.         <value>com/baobaotao/i18n/fmt_resource</value>
  6.       </list>
  7.    </property>
  8.    <!--① 重新整理資原始檔的週期,以秒為單位-->
  9.    <propertyname="cacheSeconds"value="5"/>
  10.  </bean>
  在上面的配置中,我們通過cacheSeconds屬性讓ReloadableResourceBundleMessageSource每5秒鐘重新整理一次資原始檔(在真實的應用中,重新整理週期不能太短,否則頻繁的重新整理將帶來效能上的負面影響,一般不建議小於30分鐘)。cacheSeconds預設值為-1表示永不重新整理,此時,該實現類的功能就蛻化為ResourceBundleMessageSource的功能。
  我們編寫一個測試類對上面配置的ReloadableResourceBundleMessageSource進行測試:
  1. String[] configs = {"com/baobaotao/i18n/beans.xml"};    
  2. ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);    
  3. MessageSource ms = (MessageSource)ctx.getBean("myResource");    
  4. Object[] params = {"John"new GregorianCalendar().getTime()};    
  5. for (int i = 0; i < 2; i++) {    
  6.     String str1 = ms.getMessage("greeting.common",params,Locale.US);        
  7.     System.out.println(str1);    
  8.     Thread.currentThread().sleep(20000); //①模擬程式應用,在此期間,我們更改資原始檔   
  9. }   
  在①處,我們讓程式睡眠20秒鐘,在這期間,我們將fmt_resource_zh_CN.properties資原始檔的greeting.common鍵值調整為:
  1. ---How are you!{0},today is {1}---  
  我們將看到兩次輸出的格式化資訊分別對應更改前後的內容,也即本地化資原始檔的調整被自動生效了:
  1. How are you!John,today is 1/9/07 4:55 PM  
  2. ---How are you!John,today is 1/9/07 4:55 PM---  

容器級的國際化資訊資源

  在如圖5-7所示的MessageSource類圖結構中,我們發現ApplicationContext實現了MessageSource的介面。也就是說ApplicationContext的實現類本身也是一個MessageSource物件。
  將ApplicationContext和MessageSource整合起來,乍一看挺讓人費解的,Spring這樣設計的意圖究竟是什麼呢?原來Spring認為:在一般情況下,國際化資訊資源應該是容器級。我們一般不會將MessageSource作為一個Bean注入到其他的Bean中,相反MessageSource作為容器的基礎設施向容器中所有的Bean開放。只要我們考察一下國際化資訊的實際消費場所就更能理解Spring這一設計的用意了。國際化資訊一般在系統輸出資訊時使用,如Spring MVC的頁面標籤,控制器Controller等,不同的模組都可能通過這些元件訪問國際化資訊,因此Spring就將國際化訊息作為容器的公共基礎設施對所有元件開放。
  既然一般情況下我們不會直接通過引用MessageSource Bean使用國際資訊,那如何宣告容器級的國際化資訊呢?我們其實在5.1.1節講解Spring容器的內部工作機制時已經埋下了伏筆:在介紹容器啟動過程時,我們通過程式碼清單5-1對Spring容器啟動時的步驟進行剖析,④處的initMessageSource()方法所執行的工作就是初始化容器中的國際化資訊資源:它根據反射機制從BeanDefinitionRegistry中找出名稱為“messageSource”且型別為org.springframework.context.MessageSource的Bean,將這個Bean定義的資訊資源載入為容器級的國際化資訊資源。請看下面的配置:
  容器級資源的配置:

  1. <!--①註冊資源Bean,其Bean名稱只能為messageSource -->
  2. <beanid="messageSource"
  3.       class="org.springframework.context.support.ResourceBundleMessageSource">
  4.   <propertyname="basenames">
  5.      <list>
  6.        <value>com/baobaotao/i18n/fmt_resource</value>
  7.      </list>
  8.   </property>
  9. </bean>
  下面,我們通過ApplicationContext直接訪問國際化資訊,如下程式碼清單所示:
  1. String[] configs = {"com/baobaotao/i18n/beans.xml"};    
  2. ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);    
  3. //①直接通過容器訪問國際化資訊  
  4. Object[] params = {"John"new GregorianCalendar().getTime()};    
  5. String str1 = ctx.getMessage("greeting.common",params,Locale.US);    
  6. String str2 = ctx.getMessage("greeting.morning",params,Locale.CHINA);       
  7. System.out.println(str1);    
  8. System.out.println(str2);  
  執行以上程式碼,輸出以下資訊:
  1. How are you!John,today is 1/9/07 5:24 PM  
  2. 早上好!John,現在是下午5:24  
  假設MessageSource Bean名字沒有命名為“messageSource”,以上程式碼將丟擲NoSuchMessageException異常。

參考文獻:http://stamen.iteye.com/blog/1541732