Java程序员都曾遇到过这样的问题:输入的中文不能正确显示在界面上,保存在数据库中的也是一堆乱码,或者数据库或数据文件中存放的是正确的中文,可是在Java程序中看到的却是一大串的“?”。
这就是通常所说的“中文问题”。 Java中与中文相关的编码
在JDK中,提供了对大多数常用语言的支持。在解决“中文问题”时,表1中的编码是最常用,或者就是最有关系的。
表1 JDK中与中文相关的编码列表
编码名称 | 说明 | | ASCII | 7位,与ASCII7相同 | | ISO8859-1 | 8位,与8859-1、ISO-8859-1、ISO_8859-1、Latin1等相同 | GB2312-80 | 16位、与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="%> 中指定的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文件。
先看一下这些过程中中文字符的转换情况。有如下源代码:
<%@ page contentType="text/html; charset=gb2312"%>
<%
String a="中文";
out.println(a);
%>
|
这段代码是在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的情况:
<%@ page contentType="text/html; charset=ISO-8859-1"%>
<%
String a="中文";
out.println(a);
%>
|
同样,该文件是用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-CharSet | JSP文件中 | JAVA文件中 | CLASS文件中 | | GB2312 | D6 DO CE C4(GB23112) | 从u4E2Du6587(Unicode)到E4 B8(UTF) | E4 B8 AD E6 96 87 (UTF) | | ISO-8859-1 | D6 D0 CE C4 (GB2312) | 从u00D6u00D0u00CEu00C4 (Unicode)到C3 96 C3 90 C3 8E C3 84 (UTF) | C3 96 C3 90 8E C3 C3 8E C3 84 (UTF) | | 无(默认=file.encoding) | 同ISO-8859-1 | 同ISO-8859-1 | 同ISO-8859-1 | Servlet:从源文件到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-charset | Servlet源文件中 | Class文件中 | 等效的Unicode码 | | GB2312 | D6 D0 CE C4(GB2312) | E4 B8 AD E6 96 87(UTF) | u4E2Du6587(在Unicode中=“中文”) | | ISO-8859-1 | D6 D0 CE C4(GB2312) | C3 96 C3 90 C3 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=文) | | 2 | JSPC把JSP源文件转化为临时Java文件,并把字符串按照GB2312映射到Unicode,并用UTF格式写入Java文件中 | E4 B8 AD E6 96 87 | | 3 | 把临时Java文件编译成Class文件 | E4 B8 AD E6 96 87 | | 4 | 运行时,先从Class文件中用readUTF读出字符串,在内存中的是Unicode编码 | 4E 2D 65 87 (在Unicode中4E2D=中 6587=文) | | 5 | 根据Jsp-charset=GB2312 把Unicode转化为字节流 | D6 D0 CE C4 | | 6 | 把字节流输出到IE中,并设置IE的编码为GB2312(作者按:这个信息隐藏在HTTP头中) | D6 D0 CE C4 | | 7 | IE用“简体中文”查看结果 | “中文”(正确显示) |
如果指定了为ISO8859-1,转化过程如表5。 表5 Jsp-charset=ISO8859-1时的变化过程
| 序号 | 步骤说明 | 结果 | | 1 | 编写JSP源文件,且存为GB2312格式 | D6 D0 CE C4(D6D0=中 CEC4=文) | | 2 | JSPC把JSp源文件转化为临时Java文件,并把字符串按照ISO8859-1映射到Unicode,并用UTF格式写入Java文件中 | C3 96 C3 90 C3 8E C3 84 | | 3 | 把临时Java文件编译成Class文件 | C3 96 C3 90 C3 BE C3 84 | | 4 | 运行时,先从Class文件中用readUTF读出字符串,在内存中的是Unicode编码 | 00 D6 00 D0 00 CE 00 C4(哈哈都不是!!!) | | 5 | 根据Jsp-charset=ISO8859-1把Unicode转花为字节流 | D6 D0 CE C4 | | 6 | 把字节流输出到IE中,并设置IE的编码为ISO8859-1(作者按:这个信息隐藏在HTTP头中) | D6 D0 CE C4 | | 7 | IE用“西欧字符”查看结果(注:西欧字符与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=文) | | 2 | JSPC把JSP源文件转化为临时Java文件,并把字符串按照ISO8859-1映射到Unicode,并用UTF格式写入Java文件中 | C3 96 C390 C3 8E C3 84 | | 3 | 把临时Java文件编译成Class文件 | C3 96 C390 C3 8E C384 | | 4 | 运行事,先从Class文件中用readUTF 读出字符串,在内存中的是Unicode编码 | 00 D6 00 D000 CE 00 C4(哈都不是!!!) | | 5 | 根据Jsp-charset=ISO8859-1把Unicode转化为字节流 | D6 D0 CE C4 | | 6 | 把字节流输出到IE中 | D6 D0 CE C4 | | 7 | IE用发出请求时的页面的编码查看结果 | 视情况而定。如果是简体中文,则能正确显示,否则需执行表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=GB2312 | D6 D0 CE C4(GB2312) | | 6 | IE用“简体中文”查看结果 | “中文”(正确显示) |
如果 =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 C4 | | 5 | 把字节流输出到IE中并设置IE的编码属性为Servlet-charset=ISO8859-1 | D6 D0 CE C4 | | 6 | IE用“西欧字符”查看结果 | 乱码(原因同表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 C4 | IE | | 2 | IE把字节流传输到服务器端 | | | 3 | Servlet接受到输入流,并读出其中的字符 | 00 D6 00 D0 00 CE 00 C4 | Servlet | | 4 | 编程者在Servlet中必须把字符串根据ISO8859-1还原委字节流,注意,这里一定是ISO8859-1,与客户端和数据库的字符集都无关 | D6 D0 CE C4 | | 5 | 编程者根据数据库字符集ISO8859-1生成新的字符串 | 00 D6 00 D0 00 CE 00 C4 | | 6 | 把新生成的字符串提交给JDBC | 00 D6 00 D0 00 CE 00 C4 | Servlet | | 7 | JDBC检测到数据库字符集为ISO8859-1 | 00 D6 00 D0 00 CE 00 C4 | JDBC | | 8 | JDBC把接收到的字符串按照数据库字符集生成字节流 | D6 D0 CE C4 | | 9 | JDBC把字节流写入数据库中 | D6 D0 CE C4 | | 10 | 完成数据存储工作 | D6 D0 CE C4 | 数据库 | 截止现在,数据入库的工作即已完成,用其它的非Java程序读出的数据也是正确的“中文”两字(字节流为“C6 Do CE C4”)。 以下是从数据库中取出数据的过程 | 00 D6 00 D0 00 CE 00 C4 | | 11 | JDBC从数据库中取出字节流 | D6 D0 CE C4 | JDBC | | 12 | JDBC按照数据库字符集ISO8859-1生成字符串,并提交给Servlet | 00 D6 00 D0 00 CE 00 C4(Unicode) | 字节流 | | 13 | Servlet获得字符串 | 00 D6 00 D0 00 CE 00 C4(Unicode) | Servlet | | 15 | 编程者必须根据数据库的字符集ISO8859-1还原成原始字节流 | D6 D0 CE C4 | | 16 | 编程者必须根据客户端字符集GB2312生成新的字符串 | 4E 2D 65 87(Unicode) | | Servlet准备把字符串输出到客户端 | | 17 | Servlet根据 生成字节流。一般说来, 应该与客户端字符集一致。本文假定它为GB2312 | D6 D0 CE C4 | Servlet | | 18 | Servlet把字节流输出到IE中,如果已指定 ,还会设置IE的编码为 | D6 D0 CE C4 | 数据库 | | 19 | IE根据指定的编码和默认编码查看结果 | “中文”(正确显示) | 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
|
用“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支持该方法。 |