Struts原理與實(shí)踐(4)- -
本篇我們來(lái)討論一下struts的國際化編程問(wèn)題,即所謂的i18n編程問(wèn)題,這一篇我們討論其基礎部分。與這個(gè)問(wèn)題緊密相關(guān)的是在各java論壇中被頻繁提及的中文亂碼問(wèn)題,因為,英、美編程人員較少涉及到中文亂碼問(wèn)題,因此,這方面的英文資料也是非常奇缺的,同時(shí)也很少找到這方面比較完整的中文資料,本文也嘗試對中文亂碼問(wèn)題做一些探討。要解決上述問(wèn)題,需要有一定的字符集方面的知識,下面,我們就先介紹字符集的有關(guān)情況:
一、從ASCII到Unicode(UTF-8)
電子計算機技術(shù)是從美國開(kāi)始發(fā)展起來(lái)的,因為美國使用的文字為英文,美國規定的計算機信息交換用的字符編碼集是人們熟知的擴展ASCII碼,它以8bit字節為單位存儲,ASCII的0-31及127為控制符,32-126為可見(jiàn)字符,包括所有的英文字母,阿拉伯數字和其他一些常見(jiàn)符號,128-255的ASCII碼則沒(méi)有定義。
ASCII對英語(yǔ)國家是夠用了,但對其他西歐國家卻不夠用,因此,人們將ASCII擴展到0-255的范圍,形成了ISO-8859-1字符集。值得一提的是,因為考慮到程序中處理的信息大多是西文信息,因此有些WEB容器(如:Tomcat4.x)在處理所接收到的request字符串時(shí),如果您沒(méi)指定request的編碼方式則系統就缺省地采用ISO-8859-1,明白這一點(diǎn)對理解后面的問(wèn)題會(huì )有幫助。
相比西方的拼音文字,東方的文字(如中文)的字符數要大得多,根本不可能在一個(gè)字節內將它們表示出來(lái),因此,它們以?xún)蓚€(gè)字節為單位存儲,以中文國標字符集GB2312為例,它的第一個(gè)字節為128-255。系統可以據此判斷,若第一個(gè)字節大于127,則把與該字節后緊接著(zhù)的一個(gè)字節結合起來(lái)共兩個(gè)字節組成一個(gè)中文字符。這種由多個(gè)字節存儲一個(gè)字符的字符集叫多字節字符集(MultiByte Charsets),對應的象ASCII這種用一個(gè)字節存儲一個(gè)字符的字符集叫單字節字符集(SingleByte Charsets)。在GB2312字符集中,ASCII字符仍然用一個(gè)字節存儲,換句話(huà)說(shuō)該ASCII是該字符集的子集。
GB2312只包含數千個(gè)常用漢字,往往不能滿(mǎn)足實(shí)際需要,因此,人們對它進(jìn)行擴展,這就有了我們現在廣泛使用的GBK字符集,GBK是現階段Windows及其他一些中文操作系統的缺省字符集。它包含2萬(wàn)多個(gè)字符,除了保持和GB2312兼容外,還包含繁體中文字,日文字符和朝鮮字符。值得注意的是GBK只是一個(gè)規范而不是國家標準,新的國家標準是GB18030-2000,它是比GBK包含字符更多的字符集。
我國的臺灣地區使用的文字是繁體字,其字符集是BIG5,而日本采用的字符集則是SJIS。它們的編碼方法與GB2312類(lèi)似,它們的ASCII字符部分是兼容的,但擴展部分的編碼則是不兼容的,比如這幾種字符集中都有"中文"這兩個(gè)字符,但他們在各自的字符集中的編碼并不相同,這就是用GB2312寫(xiě)成的網(wǎng)頁(yè)用BIG5瀏覽時(shí),看到的是亂糟糟的信息的原因。
可見(jiàn),在字符集的世界里,呈現給我們的是一個(gè)群雄割據的局面,各字符集擁有一塊自己的地盤(pán)。這給各國和各地區交換信息帶來(lái)了很大的困難,同時(shí),也給國際化(本地化)編程造成了很大的麻煩。
常言道:"分久必合",隨著(zhù)國際標準ISO10646定義的通用字符集(Universal Character Set即UCS)的出現,使這種局面發(fā)生了徹底的改觀(guān)。UCS 是所有其他字符集標準的一個(gè)超集. 它保證與其他字符集是雙向兼容的. 就是說(shuō), 如果你將任何文本字符串翻譯到 UCS格式, 然后再翻譯回原編碼, 你不會(huì )丟失任何信息。UCS 包含了用于表達所有已知語(yǔ)言的字符。不僅包括拉丁語(yǔ)、希臘語(yǔ)、 斯拉夫語(yǔ)、希伯來(lái)語(yǔ)、阿拉伯語(yǔ)、亞美尼亞語(yǔ)和喬治亞語(yǔ)的描述、還包括中文、 日文和韓文這樣的象形文字、 以及平假名、片假名、 孟加拉語(yǔ)、 旁遮普語(yǔ)果魯穆奇字符(Gurmukhi)、 泰米爾語(yǔ)、印.埃納德語(yǔ)(Kannada)、Malayalam、泰國語(yǔ)、 老撾語(yǔ)、 漢語(yǔ)拼音(Bopomofo)、Hangul、 Devangari、Gujarati、Oriya、Telugu 以及其他數也數不清的語(yǔ)。對于還沒(méi)有加入的語(yǔ)言, 由于正在研究怎樣在計算機中最好地編碼它們, 因而最終它們都將被加入。
ISO 10646 定義了一個(gè) 31 位的字符集。 然而, 在這巨大的編碼空間中, 迄今為止只分配了前 65534 個(gè)碼位 (0x0000 到 0xFFFD)。 這個(gè) UCS 的 16位子集稱(chēng)為 基本多語(yǔ)言面 (Basic Multilingual Plane, BMP)。 將被編碼在 16 位 BMP 以外的字符都屬于非常特殊的字符(比如象形文字), 且只有專(zhuān)家在歷史和科學(xué)領(lǐng)域里才會(huì )用到它們。
UCS 不僅給每個(gè)字符分配一個(gè)代碼, 而且賦予了一個(gè)正式的名字。 表示一個(gè) UCS 值的十六進(jìn)制數, 通常在前面加上 "U+", 就象 U+0041 代表字符"拉丁大寫(xiě)字母A"。 UCS 字符 U+0000 到 U+007F 與 US-ASCII(ISO 646) 是一致的, U+0000 到 U+00FF 與 ISO 8859-1(Latin-1) 也是一致的。這里要注意的是它是以16bit為單位存儲,即便對字母"A"也是用16bit,這是與前面介紹的所有字符集不同的地方。
歷史上,在國際標準化組織研究ISO10646標準的同時(shí),另一個(gè)由多語(yǔ)言軟件制造商組成的協(xié)會(huì )也在從事創(chuàng )立單一字符集的工作,這就是現在人們熟知的Unicode。幸運的是,1991年前后ISO10646和Unicode的參與者都認識到,世界上不需要兩個(gè)不同的單一字符集。他們合并雙方的工作成果,并為創(chuàng )立單一編碼表而協(xié)同工作。兩個(gè)項目仍都存在并獨立地公布各自的標準,都同意保持ISO10646和Unicode的碼表兼容,并緊密地共同調整任何未來(lái)的擴展。這與當年在PC機上的操作系統MS-dos與PC-dos的情形有些相象。后面,我們將視ISO10646和Unicode為同一個(gè)東西。
有了Unicode,字符集問(wèn)題接近了完美的解決,但不要高興得過(guò)早。由于歷史的原因:一些操作系統如:Unix、Linux等都是基于A(yíng)SCII設計的。此外,還有一些數據庫管理系統軟件如:Oracle等也是圍繞ASCII來(lái)設計的(從其8i的白皮書(shū)上介紹的設置系統字符集和字段的字符集中可以間接地看到這一點(diǎn))。在這些系統中直接用Unicode會(huì )導致嚴重的問(wèn)題。用這些編碼的字符串會(huì )包含一些特殊的字符, 比如 ‘\0‘ 或 ‘/‘, 它們在 文件名和其他 C 庫函數參數里都有特別的含義。 另外, 大多數使用 ASCII 文件的 UNIX 下的工具, 如果不進(jìn)行重大修改是無(wú)法讀取 16 位的字符的。 基于這些原因, 在文件名, 文本文件, 環(huán)境變量等地方,直接使用Unicode是不合適的。
在 ISO 10646-1 Annex R 和 RFC 2279 里定義的 UTF-8 (Unicode Transformation Form 8-bit form)編碼沒(méi)有這些問(wèn)題。
UTF-8 有以下一些特性:
UCS 字符 U+0000 到 U+007F (ASCII) 被編碼為字節 0x00 到 0x7F (ASCII 兼容)。 這意味著(zhù)只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 兩種編碼方式下是一樣的。
所有 >U+007F 的 UCS 字符被編碼為一個(gè)多個(gè)字節的串, 每個(gè)字節都有標記位集。 因此,ASCII 字節 (0x00-0x7F) 不可能作為任何其他字符的一部分。
表示非 ASCII 字符的多字節串的第一個(gè)字節總是在 0xC0 到 0xFD 的范圍里, 并指出這個(gè)字符包含多少個(gè)字節。 多字節串的其余字節都在 0x80 到 0xBF 范圍里。 這使得重新同步非常容易, 并使編碼無(wú)國界,且很少受丟失字節的影響。
UTF-8 編碼字符理論上可以最多到 6 個(gè)字節長(cháng), 然而 16 位 BMP 字符最多只用到 3 字節長(cháng)。
字節 0xFE 和 0xFF 在 UTF-8 編碼中從未用到。
通過(guò),UTF-8這種形式,Unicode終于可以廣泛的在各種情況下使用了。在討論struts的國際化編程之前,我們先來(lái)看看我們以前在jsp編程中是怎樣處理中文問(wèn)題以及我們經(jīng)常遇到的。
二、中文字符亂碼的原因及解決辦法
java的內核是Unicode的,也就是說(shuō),在程序處理字符時(shí)是用Unicode來(lái)表示字符的,但是文件和流的保存方式是使用字節流的。在java的基本數據類(lèi)型中,char是Unicode的,而byte是字節,因此,在不同的環(huán)節java要對字節流和char進(jìn)行轉換。這種轉換發(fā)生時(shí)如果字符集的編碼選擇不當,就會(huì )出現亂碼問(wèn)題。
我們常見(jiàn)的亂碼大致有如下幾種情形:
1、漢字變成了問(wèn)號"?"
2、有的漢字顯示正確,有的則顯示錯誤
3、顯示亂碼(有些是漢字但并不是你預期的)
4、讀寫(xiě)數據庫出現亂碼
下面我們逐一對它們出現的原因做一些解釋?zhuān)?div style="height:15px;">
首先,我們討論漢字變成問(wèn)號的問(wèn)題。
Java中byte與char相互轉換的方法在sun.io包中。其中,byte到char的常用轉換方法是:
public static ByteToCharConverter getConverter(String encoding);
為了便于大家理解,我們先來(lái)做一個(gè)小實(shí)驗:比如,漢字"你"的GBK編碼為0xc4e3,其Unicode編碼是\u4f60。我們的實(shí)驗是這樣的,先有一個(gè)頁(yè)面比如名為a_gbk.jsp輸入漢字"你",提交給頁(yè)面b_gbk.jsp。在b_gbk.jsp文件中以某種編碼方式得到"你"的字節數組,再將該數組以某種編碼方式轉換成char,如果得到的char值是0x4f60則轉換是正確的。
a_gbk.jsp的代碼如下:
Input
*
b_gbk.jsp的代碼如下:
在瀏覽器中打開(kāi)a_gbk.jsp并輸入一個(gè)"你"字,點(diǎn)擊OK按鈕提交表單,則會(huì )出現如圖1所示的結果:
圖1
從圖1可以看出,在b_gbk.jsp中這樣將byte轉換為char是正確的,即得到的char是\u4f60。這里要注意的是:byte b[]=a.getBytes("ISO8859-1");中的編碼是ISO8859-1,這就是我們前面提到的有些web容器在您沒(méi)有指定request的字符集時(shí)它就采用缺省的ISO8859-1。
從圖1中我們還看到表達式 中的a并沒(méi)有正確地顯示"你"而是變成"??"這是什么原因呢?這里的a是作為一個(gè)String被顯示的,我們來(lái)看看我們常用的String構造函數:
String(byte[] bytes,String encoding);
在國標平臺上,該函數會(huì )認為bytes是按GBK編碼的,如果后一個(gè)參數省略,它也會(huì )認為是encoding是GBK。
對前一個(gè)參數就相當于將b_gbk.jsp文件的這句byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1改為GBK,這樣顯然在GBK字符集中找不到相應的目的編碼,它給出的結果是0x3f、0x3f。因此,就會(huì )顯示為"??",這也就是造成亂碼的第一種現象的原因。我們的例子是演示的從byte到char的轉換過(guò)程,相反的過(guò)程也會(huì )造成同樣的問(wèn)題,限于篇幅,就不在此討論了,大家自己可以做類(lèi)似的實(shí)驗來(lái)驗證。
解決該問(wèn)題的方法就是象例子中a1那樣,在獲取byte數組時(shí),指定編碼為ISO8859-1。
接下來(lái),我們討論有些漢字能正常顯示,有些不能正常顯示的問(wèn)題。
如果我們將String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的GBK改為GB2312則象朱镕基的"镕"字就不能正常顯示,這是因為該字是GBK中的字符而在GB2312中不存在。
解決上述兩種問(wèn)題的方法就是象a1那樣構造String,也就是人們常說(shuō)的同時(shí)也是常用的轉碼的方法。采用這種方法會(huì )在程序中到處出現這種語(yǔ)句,特別是在Struts中,Struts有一個(gè)回寫(xiě)表單的功能,在回寫(xiě)時(shí)也要做這種轉換,這樣的語(yǔ)句差不多要多一倍。因此,這是個(gè)比較笨拙的方法,有沒(méi)有簡(jiǎn)捷一些的方法呢?其實(shí)是有的,只要在取得request的字符串前加上request.setCharacterEncoding("GBK");這句,指定request的字符集。則 中的a就能正常顯示,a1反而不能正常顯示。此時(shí)要將byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1變成GBK,從byte到char的轉換才是正確的,這就是此時(shí)a能正常顯示而a1反而不能正常顯示的原因。如果此時(shí)要a1正常顯示則必須將String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的ISO8859-1改為GBK。
很顯然,使用request.setCharacterEncoding("GBK");只能解決GBK字符問(wèn)題,要解決i18n問(wèn)題則要使用UTF-8來(lái)取代GBK。我們接著(zhù)做上述實(shí)驗,將a_gbk.jsp和b_gbk.jsp分別另存為a.jsp和b.jsp將文件中的GBK改為UTF-8,更改后的代碼分別如下:
a.jsp代碼:
Input
*
b.jsp代碼:
再在a.jsp中輸入"你"字,你會(huì )發(fā)現顯示結果中,一個(gè)漢字是用三個(gè)byte表示的,它們的值分別是0xe4、0xbd、0xa0,也就是說(shuō)用UTF-8來(lái)表示漢字,每個(gè)漢字要比GBK多占用一個(gè)byte,這也是使用UTF-8要多付出的一點(diǎn)代價(jià)吧。
現在,我們討論一下第三個(gè)問(wèn)題,即顯示亂碼,有些莫名其妙的漢字并不是你預期的結果。
在上例中將String a1=new String(a.getBytes("UTF-8"),"UTF-8");改為String a1=new String(a.getBytes("UTF-8"),"GBK");再輸入"你"字,則a1會(huì )顯示成"浣?",您只要看一看"浣"的UTF-8碼和GBK碼就會(huì )知道其中的奧秘了。
下面,我們討論一下最后一個(gè)問(wèn)題,就是讀寫(xiě)數據庫時(shí)出現亂碼。
現在一些常用的數據庫都支持數據庫encoding,也就是說(shuō)在創(chuàng )建數據庫時(shí)可以指定它自己的字符集設置,數據庫數據以指定的編碼形式存儲。當應用程序訪(fǎng)問(wèn)數據庫時(shí),在入口和出口處都會(huì )有encoding轉換。如果,在應用程序中字符本來(lái)已變成了亂碼,當然也就無(wú)法正確地轉換為數據庫的字符集了。數據庫的encoding可根據需要來(lái)設置,比如要支持簡(jiǎn)、繁體中文、日、韓、英語(yǔ)選GBK,如果還要支持其他語(yǔ)言最好選UTF-8。
本篇文章對字符集及中文亂碼問(wèn)題做了一下探討,為實(shí)現國際化編程的實(shí)踐打下一個(gè)基礎。下一篇文章,我們將介紹struts中實(shí)現國際化編程的具體步驟,并將我們前面介紹的登錄例子進(jìn)行國際化。
參考文獻:
UTF-8 and Unicode FAQ
《JSP動(dòng)態(tài)網(wǎng)站技術(shù)入門(mén)與提高》太陽(yáng)工作室 孫曉龍 趙莉編著(zhù)