Java程序員都曾遇到過這樣的問題:輸入的中文不能正確顯示在界面上,保存在數據庫中的也是一堆亂碼,或者數據庫或數據文件中存放的是正確的中文,可是在Java程序中看到的卻是一大串的“?”。
這就是通常所說的“中文問題”。 Java中與中文相關的編碼
在JDK中,提供了對大多數常用語言的支持。在解決“中文問題”時,表1中的編碼是最常用,或者就是最有關系的。
表1 JDK中與中文相關的編碼列表
編碼名稱說明ASCII7位,與ASCII7相同ISO8859-18位,與8859-1、ISO-8859-1、ISO_8859-1、Latin1等相同 GB2312-8016位、與gb2312、GB2312-1980、EUC_CN、euccn、1381、Cp1381、1383、Cp1383、ISO2022CN、ISO2022CN_GB等相同GBK與MS936相同,注意:區分大小寫UTF與URF-8相同GB18030與cp1392、1392相同,目前支持的JDK很少
在實際編程時,接觸得比較多的是GB2312(GBK)和ISO8859-1。
在實際編程時,接觸得比較多的是GB2312(GBK)和ISO8859-1。
為什么會有“?”號
上文說過,異種語言之間的轉換是通過Unicode來完成的。假設有兩種不同的語言A和B,轉換的步驟為:先把A轉化為Unicode,再把Unicode轉化為B。
舉例說明。有GB2312中有一個漢字“李”,其編碼為“C0EE”,欲轉化為ISO8859-1編碼。步驟為:先把“李”字轉化為Unicode,得到“674E”,再把“674E”轉化為ISO8859-1字符。當然,這個映射不會成功,因為ISO8859-1中根本就沒有與“674E”對應的字符。
當映射不成功時,問題就發生了!當從某語言向Unicode轉化時,如果在某語言中沒有該字符,得到的將是Unicode的代碼“uffffd”(“u”表示是Unicode編碼,)。而從Unicode向某語言轉化時,如果某語言沒有對應的字符,則得到的是“0x3f”(“?”)。這就是“?”的由來。
例如:把字符流buf =“0x80 0x40 0xb0 0xa1”進行new String(buf, "gb2312")操作,得到的結果是“ufffdu554a”,再println出來,得到的結果將是“?啊”,因為“0x80 0x40”是GBK中的字符,在GB2312中沒有。
再如,把字符串String="u00d6u00ecu00e9u0046u00bbu00f9"進行new String (buf.getBytes("GBK"))操作,得到的結果是“3fa8aca8a6463fa8b4”,其中,“u00d6”在“GBK”中沒有對應的字符,得到“3f”,“u00ec”對應著“a8ac”,“u00e9”對應著“a8a6”,“0046”對應著“46”(因為這是ASCII字符),“u00bb”沒找到,得到“3f”,最后,“u00f9”對應著“a8b4”。把這個字符串println一下,得到的結果是“?ìéF?ù”。看到沒?這里并不全是問號,因為GBK與Unicode映射的內容中除了漢字外還有字符,本例就是最好的明證。
所以,在漢字轉碼時,如果發生錯亂,得到的不一定都是問號噢!不過,錯了終究是錯了,50步和100步并沒有質的差別。
或者會問:如果源字符集中有,而Unicode中沒有,結果會如何?回答是不知道。因為我手頭沒有能做這個測試的源字符集。但有一點是肯定的,那就是源字符集不夠規范。在Java中,如果發生這種情況,是會拋出異常的。 什么是UTF
UTF,是Unicode Text Format的縮寫,意為Unicode文本格式。對于UTF,是這樣定義的:
(1)如果Unicode的16位字符的頭9位是0,則用一個字節表示,這個字節的首位是“0”,剩下的7位與原字符中的后7位相同,如“u0034”(0000 0000 0011 0100),用“34” (0011 0100)表示;(與源Unicode字符是相同的);
(2)如果Unicode的16位字符的頭5位是0,則用2個字節表示,首字節是“110”開頭,后面的5位與源字符中除去頭5個零后的最高5位相同;第二個字節以“10”開頭,后面的6位與源字符中的低6位相同。如“u025d”(0000 0010 0101 1101),轉化后為“c99d”(1100 1001 1001 1101);
(3)如果不符合上述兩個規則,則用三個字節表示。第一個字節以“1110”開頭,后四位為源字符的高四位;第二個字節以“10”開頭,后六位為源字符中間的六位;第三個字節以“10”開頭,后六位為源字符的低六位;如“u9da7”(1001 1101 1010 0111),轉化為“e9b6a7”(1110 1001 1011 0110 1010 0111);
注:UTF是Unicode Transformation Format的縮寫,意為Unicode轉換格式。可以這么描述JAVA程序中Unicode與UTF的關系,雖然不絕對。字符串在內存中運行時,表現為Unicode代碼,而當要保存到文件或其它介質中去時,用的是UTF。這個轉化過程是由writeUTF和readUTF來完成得。 Servlet/JSP對中文的處理過程
總體流程
把問題想成是一個黑匣子。先看黑匣子的一級表示(如圖1所示):
圖1 IPO模型
這就是一個IPO模型,即輸入、處理和輸出。同樣的內容要經過“從charsetA到Unicode再到charsetB”的轉化。
再看二級表示(如圖2所示):
圖2 JSP、Java輸出模型
在這個圖中,輸入的是JSP和Java源文件。在處理過程中,以Class文件為載體,然后輸出。再細化到三級(如圖3所示):
圖3 IPO模型
JSP文件先生成中間的Java文件,再生成Class。而Servlet和普通App則直接編譯生成Class,然后,從Class再輸出到瀏覽器、控制臺或數據庫等。JSP:從源文件到Class的過程
JSP源文件是以“.jsp”結尾的文本文件。在本節中,將闡述JSP文件的解釋和編譯過程,并跟蹤其中中文內容的變化。
一般地,JSP源文件經過如下步驟后變成可被引擎執行的Class文件:
1. JSP/Servlet引擎提供的JSP轉換工具(JSPC)搜索JSP文件中用<%@ page="" contenttype="text/html; charset=<Jsp-charset>">中指定的charset。如果在JSP文件中未指定,則默認為ISO8859-1(或者說是Latin-1)。
2. JSPC用相當于“Javac -encoding”解釋JSP文件中出現的所有字符,包括中文字符和ASCII字符。然后把這些字符轉換成Unicode字符,再轉化成UTF格式,存為Java文件。ASCII碼字符轉化為Unicode字符時只是簡單地在前面加“00”,如“A”,轉化為“u0041”。然后,經過了UTF的轉換,又變回“41”了。這也就是可以使用普通文本編輯器查看由JSP生成的Java文件的原因。
3. 引擎用相當于“Javac -encoding UTF-8”的命令,把Java文件編譯成Class文件。
先看一下這些過程中中文字符的轉換情況。有如下源代碼:
這段代碼是在UltraEdit for Windows上編寫的。保存后,“中文”兩個字的16進制編碼為“D6 D0 CE C4”(GB2312編碼)。經查表,“中文”兩字的Unicode編碼為“u4E2Du6587”,用 UTF表示就是“E4 B8 AD E6 96 87”。打開引擎生成的由JSP文件轉變成的Java文件,發現其中的“中文”兩個字的位置確實被“E4 B8 AD E6 96 87”替代了,再查看由Java文件編譯生成的Class文件,發現結果與Java文件中的完全一樣,也是“E4 B8 AD E6 96 87”。
再看JSP中指定的CharSet為ISO-8859-1的情況:
同樣,該文件是用UltraEdit編寫的。“中文”這兩個字也是存為GB2312編碼“D6 D0 CE C4”。先模擬一下生成的Java文件和Class文件的過程:JSPC用ISO-8859-1來解釋“中文”,并把它映射到Unicode。由于ISO-8859-1是8位的,且是拉丁語系,其映射規則就是在每個字節前加“00”。所以,映射后的Unicode編碼應為“u00D6u00D0 u00CEu00C4”,轉化成UTF后應該是“C3 96 C3 90 C3 8E C3 84”。好,打開文件Java文件和CLASS文件,“中文”兩個字的位置果然都表示為“C3 96 C3 90 C3 8E C3 84”。
如果上述代碼中不指定,即把第一行寫成“<%@ page="" contenttype="text/html">”,JSPC會使用默認的“ISO8859-1”來解釋JSP文件。
到現在為止,已經解釋了從JSP文件到Class文件的轉變過程中中文字符的映射過程。一句話,從“Jsp-CharSet到Unicode再到UTF”。表2總結了這個過程: 表2 “中文”從JSP到Class的轉化過程
Jsp-CharSetJSP文件中JAVA文件中CLASS文件中GB2312D6 DO CE C4(GB23112)從u4E2Du6587(Unicode)到E4 B8(UTF)E4 B8 AD E6 96 87 (UTF)ISO-8859-1D6 D0 CE C4 (GB2312)從u00D6u00D0u00CEu00C4(Unicode)到C3 96 C3 90C3 8E C3 84 (UTF)C3 96 C3 908E C3 C3 8EC3 84 (UTF)無(默認=file.encoding)同ISO-8859-1同ISO-8859-1同ISO-8859-1Servlet:從源文件到Class的過程
Servlet源文件是以“.Java”結尾的文本文件。我們將討論Servlet的編譯過程并跟蹤其中的中文變化。
用“Javac”編譯Servlet源文件。Javac可以帶“-encoding”參數,意思是“用< Compile-charset >中指定的編碼來解釋Serlvet源文件”。
源文件在編譯時,用來解釋所有字符,包括中文字符和ASCII字符。然后把字符常量轉變成Unicode字符。最后,把Unicode轉變成UTF。
在Servlet中,還有一個地方設置輸出流的CharSet。通常在輸出結果前,調用HttpServletResponse的setContent Type方法來達到與在JSP中設置一樣的效果,稱之為。
注意:文中一共提到了三個變量:、和。其中,JSP文件只與有關,而和只與Servlet有關。
看下例:
import Javax.servlet.*;
import Javax.servlet.http.*;
Class testServlet extends HttpServlet
{
public void doGet(HttpServletRequest req,HttpServletResponse resp)
throws ServletException,Java.io.IOException
{
resp.setContentType("text/html; charset=GB2312");
Java.io.PrintWriter out=resp.getWriter();
out.println("");
out.println("#中文#");
out.println("");
}
}
該文件也是用UltraEdit for Windows編寫的,其中的“中文”兩個字保存為字節流“D6 D0 CE C4”(GB2312編碼)。
開始編譯。表3是不同時,Class文件中“中文”兩字的十六進制碼。在編譯過程中,不起任何作用。只對Class文件的輸出產生影響,可以說和一起,達到與JSP文件中的相同的效果,因為對編譯過程和Class文件的輸出都會產生影響。
表3 “中文”從Servlet源文件到Class的轉變過程
Compile-charsetServlet源文件中Class文件中等效的Unicode碼GB2312D6 D0 CE C4(GB2312)E4 B8 AD E6 96 87(UTF)u4E2Du6587(在Unicode中=“中文”)ISO-8859-1D6 D0 CE C4(GB2312)C3 96 C3 90C3 8E C3 84(UTF)u00d6u00D0u00CEu00C4(在D6 D0 CE C4前面各加了一個00)無(默認)D6 D0 CE C4(GB2312)同ISO-8859-1同ISO-8859-1
注意:普通Java程序的編譯過程與Servlet完全一樣。
截止現在,從JSP或Servlet的源文件到Class文件的過程中中文內容的蛻變歷程是不是昭然若揭了?OK,接下來看看Class文件中的中文又是怎樣被輸出的呢?
Class:輸出字符串
Class文件是Java程序的一種存儲載體。當Class文件被虛擬機執行時,通過readUTF把Class文件中的內容讀入內存中。字符串在內存中表示為Unicode編碼。當要把內存中的內容輸出到別的程序或是外圍設備(如終端)上去時,問題就來了(為了簡單起見,把“別的程序或外圍設備”稱之為“輸出對象”)。
1.如果輸出對象能處理Unicode字符,則一切都很簡單,只要把Unicode字符直接傳給輸出對象即可。
2.事實是,大多數輸出對象不能直接處理Unicode,它們只能處理ISO8859-1和GB2312等。在往輸出對象輸出字符串時,需要做一定的轉換才行。
看看下面的例子,給定一個有四個字符的Unicode字符串“00D6 00D0 00CE 00C4”,如果輸出到只能識別“ISO8859-1”的程序中去,則直接去掉前面的“00”即可得到目的字符串“D6 D0 CE C4”。假如把它們輸出到GB2312的程序中去,得到的結果很可能是一大堆亂碼。因為在GB2312中可能沒有(也有可能有)字符與00D6等字符對應(如果對應不上,將得到0x3f,也就是問號,如果對應上了,由于00D6等字符太靠前,估計也是一些特殊符號,真正的漢字在Unicode中的編碼從4E00開始)。
同樣的Unicode字符,輸出到不同編碼的對象中去時,結果是不同的。當然,這其中有一種是我們期望的結果。對于能處理中文的輸出對象而言,自然希望輸入的內容(也就是Java程序輸出的內容)是基于GB2312編碼有意義的中文字符串。
以上例而論,“D6 D0 CE C4”應該是我們所想要的。當把“D6 D0 CE C4”輸出到IE中時,用“簡體中文”方式查看,就能看到清楚的“中文”兩個字了。
得出如下結論:
Java程序在輸出字符串前,必須先把Unicode的字符串按照某一種內碼重新生成字節流,然后把字節流輸出給“輸出對象”,相當于進行了一步“String.getBytes(???)”操作,其中???代表一種字符集的名字。
1.如果是Servlet,這種字符集是在HttpServlet Response.setContentType()方法中指定的,也就是上文定義的。
2. 如果是JSP,這種字符集是在<%@ page="" content="" type="">中指定的,也就是上文定義的。
3. 如果是Java程序,這種字符集是由file.encoding中指定的,默認為ISO8859-1。
當輸出對象是瀏覽器時
以流行的瀏覽器IE為例。IE支持多種字符集。假如IE接收到了字節流“D6 D0 CE C4”,你可以嘗試用各種內碼去查看。你會發現用“簡體中文”時能得到正確的結果。因為“D6 D0 CE C4”本來就是簡體中文中“中文”兩個字的編碼。
OK,完整地看一遍JSP和Servlet中,中文內容的變化細節。
從JSP源文件到瀏覽器
前提:JSP源文件為GB2312格式的文本文件,且JSP源文件中有“中文”這兩個漢字
如果指定了為GB2312,轉化過程如表4。 表4 Jsp-charset=GB2312時的變化過程
序號步驟說明結果1編寫JSP源文件,且存為GB2312格式D6 D0 CE C4(D6D0=中 CEC4=文)2JSPC把JSP源文件轉化為臨時Java文件,并把字符串按照GB2312映射到Unicode,并用UTF格式寫入Java文件中E4 B8 AD E6 96 873把臨時Java文件編譯成Class文件E4 B8 AD E6 96 874運行時,先從Class文件中用readUTF讀出字符串,在內存中的是Unicode編碼4E 2D 65 87 (在Unicode中4E2D=中 6587=文)5根據Jsp-charset=GB2312 把Unicode轉化為字節流D6 D0 CE C46把字節流輸出到IE中,并設置IE的編碼為GB2312(作者按:這個信息隱藏在HTTP頭中)D6 D0 CE C47IE用“簡體中文”查看結果“中文”(正確顯示)
如果指定了為ISO8859-1,轉化過程如表5。 表5 Jsp-charset=ISO8859-1時的變化過程
序號步驟說明結果1編寫JSP源文件,且存為GB2312格式D6 D0 CE C4(D6D0=中 CEC4=文)2JSPC把JSp源文件轉化為臨時Java文件,并把字符串按照ISO8859-1映射到Unicode,并用UTF格式寫入Java文件中C3 96 C3 90 C3 8E C3 843把臨時Java文件編譯成Class文件C3 96 C3 90 C3 BE C3 844運行時,先從Class文件中用readUTF讀出字符串,在內存中的是Unicode編碼00 D6 00 D0 00 CE 00 C4(哈哈都不是!!!)5根據Jsp-charset=ISO8859-1把Unicode轉花為字節流D6 D0 CE C46把字節流輸出到IE中,并設置IE的編碼為ISO8859-1(作者按:這個信息隱藏在HTTP頭中)D6 D0 CE C47IE用“西歐字符”查看結果(注:西歐字符與ISO8859-1是相對應的)亂碼,啟示時四個ASCII字符,但由于大于128,所以顯示出來得怪模怪樣8改變IE的頁面編碼為“簡體中文”“中文”(正確顯示)奇怪了!為什么把設成GB2312和ISO8859-1是一個樣的,都能正確顯示?因為表4、表5中的第2步和第5步互逆,是相互“抵消”的。只不過當指定為ISO8859-1時,要增加第8步操作,殊為不便。
通過表6再看看不指定時的情況。
表6 未指定Jsp-charset時的變化過程
序號步驟說明結果1編寫JSP源文件,且存為GB2312格式D6 D0 CE C4(D6D0=中 CEC4=文)2JSPC把JSP源文件轉化為臨時Java文件,并把字符串按照ISO8859-1映射到Unicode,并用UTF格式寫入Java文件中C3 96 C390 C3 8E C3 843把臨時Java文件編譯成Class文件C3 96 C390 C3 8E C3844運行事,先從Class文件中用readUTF 讀出字符串,在內存中的是Unicode編碼00 D6 00 D000 CE 00 C4(哈都不是!!!)5根據Jsp-charset=ISO8859-1把Unicode轉化為字節流D6 D0 CE C46把字節流輸出到IE中D6 D0 CE C47IE用發出請求時的頁面的編碼查看結果視情況而定。如果是簡體中文,則能正確顯示,否則需執行表5中的第8步
從Servlet源文件到瀏覽器
前提:Servlet源文件為Java文件,格式是GB2312,且含有“中文”這兩個漢字。
如果=GB2312,則=GB2312(見表7)。 表7 Compile-Charset=Servlet-charset=GB2312時的變化過程
序號步驟說明結果1編寫Servlet源文件,且存為GB2312格式D6 D0 CE C4(D6D0=中 CEC4=文)2由于Compole-charset是GB2312,所以用Java-encoding GB2312把JAVA源文件編譯成Class文件E4 B8 AD E6 96 87(UTF)3運行時,先從Class文件中用readUTF讀出字符串,在內存中的是Unicode編碼4E 2D 65 87(Unicode)4根據Servlet-charset=GB2312把Unicode轉化為字節流D6 D0 CE C4(GB2312)5把字節流輸出到IE中并設置為IE的編碼屬性為Servlet-charset=GB2312D6 D0 CE C4(GB2312)6IE用“簡體中文”查看結果“中文”(正確顯示)
如果=ISO8859-1,則=ISO8859-1(見表8)。 表8 Compile-charset=Servlet-charset=ISO8859-1時的變化過程
序號步驟說明結果1編寫Servlet源文件,且存為GB2312格式D6 D0 CE C4(D6D0=中 CEC4=文)2用Javac-encoding ISO8859-1把Java源文件編譯成Class文件C3 96 C3 90 C3 8E C3 84(UTF)3運行時,先從Class文件中用readUTF讀出字符串,在內存中的是Unicode編碼00 D6 00 D0 00 CE 00 C4(哈都不是!!!)4根據Servlet-charset=ISO8859-1把Unicode轉化為字節流D6 D0 CE C45把字節流輸出到IE中并設置IE的編碼屬性為Servlet-charset=ISO8859-1D6 D0 CE C46IE用“西歐字符”查看結果亂碼(原因同表5)7改變IE的頁面編碼為“簡體中文”“中文”(正確顯示)
注意:如果不指定Compile-charset或Servlet-charset,其默認值均為ISO8859-1。
當Compile-charset=Servlet-charset時,第2步和第4步能互逆,“抵消”,顯示結果均能正確。讀者可試著寫一下Compile-charset≠Servlet-charset時的情況,肯定是不正確的。
當輸出對象是數據庫時
輸出到數據庫時,原理與輸出到瀏覽器也是一樣的。我們只以Servlet為例,JSP的情況請讀者自行推導(見表9)。
假設有一個Servlet,它接收來自客戶端(IE,簡體中文)的漢字字符串,然后把它寫入到字符集為ISO8859-1的數據庫中,然后再從數據庫中取出這個字符串,顯示到客戶端。
前提:客戶端的字符集是GB2312,數據庫的字符集是ISO8859-1。解釋一下,表中第4、第5步和第15、第16步表示要由編程者來作轉換。第4、5兩步其實就是一句話:“new String(source.getBytes("ISO8859-1"), DBCharset)”。第15、16兩步也是一句話:“new String(source.getBytes(DBCharset), ClientCharset)”。親愛的讀者,你在這樣編寫代碼時,是否想過為什么要這么做呢? 序號步驟說明結果宿主程序1在IE中輸入“中文”D6 D0 CE C4IE2IE把字節流傳輸到服務器端 3Servlet接受到輸入流,并讀出其中的字符00 D6 00 D0 00 CE 00 C4Servlet4編程者在Servlet中必須把字符串根據ISO8859-1還原委字節流,注意,這里一定是ISO8859-1,與客戶端和數據庫的字符集都無關D6 D0 CE C45編程者根據數據庫字符集ISO8859-1生成新的字符串00 D6 00 D0 00 CE 00 C46把新生成的字符串提交給JDBC00 D6 00 D0 00 CE 00 C4Servlet7JDBC檢測到數據庫字符集為ISO8859-100 D6 00 D0 00 CE 00 C4JDBC8JDBC把接收到的字符串按照數據庫字符集生成字節流D6 D0 CE C49JDBC把字節流寫入數據庫中D6 D0 CE C410完成數據存儲工作D6 D0 CE C4數據庫截止現在,數據入庫的工作即已完成,用其它的非Java程序讀出的數據也是正確的“中文”兩字(字節流為“C6 Do CE C4”)。以下是從數據庫中取出數據的過程00 D6 00 D0 00 CE 00 C411JDBC從數據庫中取出字節流D6 D0 CE C4JDBC12JDBC按照數據庫字符集ISO8859-1生成字符串,并提交給Servlet00 D6 00 D0 00 CE 00 C4(Unicode)字節流13Servlet獲得字符串00 D6 00 D0 00 CE 00 C4(Unicode)Servlet15編程者必須根據數據庫的字符集ISO8859-1還原成原始字節流D6 D0 CE C416編程者必須根據客戶端字符集GB2312生成新的字符串4E 2D 65 87(Unicode)Servlet準備把字符串輸出到客戶端17Servlet根據生成字節流。一般說來,應該與客戶端字符集一致。本文假定它為GB2312D6 D0 CE C4Servlet18Servlet把字節流輸出到IE中,如果已指定,還會設置IE的編碼為D6 D0 CE C4數據庫19IE根據指定的編碼和默認編碼查看結果“中文”(正確顯示)IE結論及結束語
行文至此,已可告一段落了。以下給出一個結論,作為結尾。
1.在JSP文件中,要指定contentType。其中,charset的值要與客戶端瀏覽器所用的字符集一樣;對于其中的字符串常量,不需做任何處理;對于字符串變量,要求能根據ContentType中指定的字符集還原成客戶端能識別的字節流,通俗地說,就是“字符串變量是基于字符集的”。
2.在Servlet中,必須用HttpServletResponse. setContentType()設置charset,且設置成與客戶端字符集一致;對于其中的字符串常量,需要在Javac編譯時指定encoding,這個encoding必須與編寫源文件平臺的字符集一樣。一般說來都是GB2312或GBK;對于字符串變量,與JSP一樣。必須“是基于字符集的”。
終點又回到了起點,對于編程者而言,幾乎是什么影響都沒有。因為我們早就被告之要這么做了。
案例分析
案例:某用戶在英文Windows上,安裝了外掛的中文平臺,操作系統的字符集是“西歐字符”,對應著ISO8859-1字符集,外掛的中文平臺是基于Big5碼的。當操作者在瀏覽器(默認編碼是ISO8859-1)中輸入漢字時,這個漢字用Big5編碼(在頁面上無法正確顯示)。然后,瀏覽器把數據提交給服務器端。同時,有另一個用戶,在中文版的Windows 2000平臺上做了同樣的事情。服務器端程序需要正確處理來自多種內碼的客戶端的字符串,以便正確地保存到數據庫中。
本案例涉及到多步轉換。在第一種客戶端上:
1. 在客戶端,Big5內碼封裝成ISO8859-1內碼;
2. 把封裝后的ISO8859-1字符流傳輸到Java程序端;
3. Java程序先是用ISO8859-1識別輸入流,再用Big5內碼來識別夾雜在其中的Big5字符;
4.在Java程序中的字符串已經是Unicode的了,而且它所代表的圖形符號與客戶端的文字所呈現的圖形符號是完全相同的。
在第二種終端上:
1.客戶端把GB2312的字符串與其它內容一起以GB2312編碼方式傳輸到服務器端;
2.Java程序先用GB2312內碼識別所有輸入流,再用GB2312內碼識別其中的字符串;
3.Java程序中的Unicode編碼的字符串所代表的圖形符號與客戶端字符的圖形符號是完全相同的。
以上是輸入邏輯。再看輸出邏輯。
有兩個與數據庫相關的字符集:一是數據庫真正的字符集,稱為DBCharSet;二是數據庫中表現中文的字符集,稱為DBChineseCharSet。這一點有些難以理解。請看下述規則:
1. 與中文相關的內容被按照DBChineseCharSet轉化成字節流A;
2. 把字節流A和其它非中文的內容加在一起,形成新的字節流B;
3. 數據庫以自己的字符集(DBCharSet)存放字節流B的所有內容。
這種思想類似于TCP/IP協議的層層封裝。
還是看一看具體的例子吧。以第一種客戶端為例(第二種原理是一樣的)。假定數據庫字符集是ISO8859-1,數據庫中中文字符集為GBK(如圖4):
圖4
圖4所示是從客戶端接收數據然后寫到數據庫中的過程。從數據庫中讀出是其逆過程,請讀者自行擴展到各種情況。
下面給出一段Servlet源程序,僅供參考。其功能是模擬客戶端輸入,然后寫入數據庫中。請讀者自行體會與上文中的例子“testServlet3.Java”的區別。
import Java.io.*;
import Java.sql.*;
public Class testEncode
{
public static final String SOURCE="中文";
public static final String CLIENT_ CN_CHARSET ="Big5";
public static final String CLIENT_CHARSET= "ISO8859-1";
public static final String DB_CN_CHARSET="GBK";
public static final String DB_CHARSET="ISO8859-1";
public static void main(String[] args)
{
try
{
System.out.println("SOURCE="+toBytes(SOURCE));
//模擬客戶端把BIG5轉為ISO8859-1
String Source_Iso = new String(SOURCE.getBytes
(CLIENT_CN_CHARSET), CLIENT_CHARSET);
System.out.println("Source_Iso="+toBytes(Sour ce_Iso));
//模擬服務器端接收到字節流
String Java_Iso = Source_Iso;
System.out.println("Java_Iso="+toBytes(Java_Iso));
//模擬JAVA程序先用ISO8859-1識別,再用BIG5識別
String Java_Unicode = new String(Java_Iso.getBytes
(CLIENT_CHARSET), CLIENT_CN_CHARSET);
System.out.println("Java_Unicode="+toBytes(Java_Unicode));
//模擬JAVA程序根據GBK生成字節流,然后再生成ISO8859-1
String DB_Iso = new String(Java_Unicode.getBytes(DB_CN_CHARSET),
DB_CHARSET);
System.out.println("DB_Iso="+toBytes(DB_Iso));
DriverManager.registerDriver(new
oracle.jdbc.driver.OracleDriver());
Connection con = DriverManager.getConnection("jdbc:oracle:thin:
@172.18.131.206:1521:ora816","scott","tiger");
try
{
Statement stmt = con.createStatement();
stmt.execute("INSERT INTO TEST_TABLE(NAME)
VALUES('"+DB_Iso+"')");
stmt.close();
}
finally
{
con.close();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static String toBytes(String s)
{
if (s == null) return null;
StringBuffer result = new StringBuffer();
for (int i=0; i<s.length(); i++)
{
char c = s.charAt(i);
int intc = (int)c;
result.append(Integer.toHexString(intc));
}
return new String(result);
}
}
用“Javac-encoding gb2312 testEncode.Java”編譯完成后,執行之。這里之所以用GB2312進行編譯,是因為該文件用UltraEdit for Windows在GB2312環境下書寫的。結果如下:
SOURCE=4e2d6587 //這是用Javac -encoding gb2312編譯的結果
Source_Iso=a4a4a4e5 //顯示出來時把前導的“00”丟掉了,實際中應該有
Java_Iso=a4a4a4e5 //同上
Java_Unicode=4e2d6587 //在Unicode中表示“中文”這兩個字
DB_Iso=d6d0cec4 //也是在顯示時把前導“00”丟掉了
OK,檢查一下數據庫中是不是正確存放了用GBK表示的“中文”兩字。打開SQLPLUS,輸入如下命令:
SELECT ASCII(SUBSTR(NAME,1,1)),ASCII(SUBSTR(NAME,2,1)),
ASCII(SUBSTR(NAME,3,1)), ASCII(SUBSTR(NAME,4,1))
FROM TEST_TABLE;
得到的結果如下:“214 208 206 196”,正是十六進制的“D6 D0 CE C4”。
驗證成功!
SetCharacterEncoding和getCharacterEncoding
在Servlet/JSP規范中,還有兩個很重要的方法:setCharacterEncoding和getCharacterEncoding。這兩個方法是在ServletRequest類中定義的。顯而易見,就是設置(獲取)如何從HTTP輸入流中讀取字符的字符集的。從上文可以看出,HTTP在網絡上傳輸字符串的方式是先把字符串按照某種字符集編碼。然后,把編碼后的字符串按ASCII方式傳輸。
如果這時直接用諸如getParameter()方法讀取參數,那么得到的就是經過編碼后的字符串,而不是源字符串。通過setCharacterEncoding設置正確的字符集后,可以在讀取參數(getParameter)時,直接把經過編碼后的字符串還原為源字符串。當然,這時的“源字符串”是用Unicode碼表示的。
這兩個方法給編程帶來了方便,但是卻不被某些Servlet/JSP引擎支持,如Tomcat 3.2.x。最新的Tomcat 4.0.1和WebLogic Server 6.1支持該方法。 |