奇怪了!为什么把设成GB2312和ISO8859-1是一个样的,都能正确显示?因为表4、表5中的第2步和第5步互逆,是相互“抵消”的。只不过当指定为ISO8859-1时,要增加第8步操作,殊为不便。
通过表6再看看不指定
时的情况。
表6 未指定Jsp-charset时的变化过程
从Servlet源文件到浏览器
前提:Servlet源文件为Java文件,格式是GB2312,且含有“中文”这两个汉字。
如果=GB2312,则=GB2312(见表7)。
表7 Compile-Charset=Servlet-charset=GB2312时的变化过程
如果=ISO8859-1,则=ISO8859-1(见表8)。
表8 Compile-charset=Servlet-charset=ISO8859-1时的变化过程
注意:如果不指定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.在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”的区别。
用“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支持该方法。