快速业务通道

JSP和Servlet对中文的处理过程

作者 佚名技术 来源 JSP编程 浏览 发布时间 2012-03-14
dquo;674E”转化为ISO8859-1字符。当然,这个映射不会成功,因为ISO8859-1中根本就没有与“674E”对应的字符。

当映射不成功时,问题就发生了!当从某语言向Unicode转化时,如果在某语言中没有该字符,得到的将是Unicode的代码“\uffffd”(“\u”表示是Unicode编码,)。而从Unicode向某语言转化时,如果某语言没有对应的字符,则得到的是“0x3f”(“?”)。这就是“?”的由来。

例如:把字符流buf =“0x80 0x40 0xb0 0xa1”进行new String(buf, "gb2312")操作,得到的结果是“\ufffd\u554a”,再println出来,得到的结果将是“?啊”,因为“0x80 0x40”是GBK中的字符,在GB2312中没有。

再如,把字符串String="\u00d6\u00ec\u00e9\u0046\u00bb\u00f9"进行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);

可以这么描述JAVA程序中Unicode与UTF的关系,虽然不绝对:字符串在内存中运行时,表现为Unicode代码,而当要保存到文件或其它介质中去时,用的是UTF。这个转化过程是由writeUTF和readUTF来完成的。

好了,基础性的论述差不多了,下面进入正题。

先把这个问题想成是一个黑匣子。先看黑匣子的一级表示:

input(charsetA)->process(Unicode)->output(charsetB)

简单,这就是一个IPO模型,即输入、处理和输出。同样的内容要经过“从charsetA到unicode再到charsetB”的转化。

再看二级表示:

SourceFile(jsp,java)->class->output


在这个图中,可以看出,输入的是jsp和java源文件,在处理过程中,以Class文件为载体,然后输出。再细化到三级表示:

jsp->temp file->class->browser,os console,db

app,servlet->class->browser,os console,db

这个图就更明白了。Jsp文件先生成中间的Java文件,再生成Class。而Servlet和普通App则直接编译生成Class。然后,从Class再输出到浏览器、控制台或数据库等。

JSP:从源文件到Class的过程

Jsp的源文件是以“.jsp”结尾的文本文件。在本节中,将阐述JSP文件的解释和编译过程,并跟踪其中的中文变化。

1、JSP/Servlet引擎提供的JSP转换工具(jspc)搜索JSP文件中用中指定的charset。如果在JSP文件中未指定,则取JVM中的默认设置file.encoding,一般情况下,这个值是ISO8859-1;

2、jspc用相当于“javac –encoding ”的命令解释JSP文件中出现的所有字符,包括中文字符和ASCII字符,然后把这些字符转换成Unicode字符,再转化成UTF格式,存为JAVA文件。ASCII码字符转化为Unicode字符时只是简单地在前面加“00”,如“A”,转化为“\u0041”(不需要理由,Unicode的码表就是这么编的)。然后,经过到UTF的转换,又变回“41”了!这也就是可以使用普通文本编辑器查看由JSP生成的JAVA文件的原因;

3、引擎用相当于“javac –encoding UNICODE”的命令,把JAVA文件编译成CLASS文件;

先看一下这些过程中中文字符的转换情况。有如下源代码:


这段代码是在UltraEdit for Windows上编写的。保存后,“中文”两个字的16进制编码为“D6 D0 CE C4”(GB2312编码)。经查表,“中文”两字的Unicode编码为“\u4E2D\u6587”,用 UTF表示就是“E4 B8 AD E6 96 87”。打开引擎生成的由JSP文件转变而成的JAVA文件,发现其中的“中文”两个字确实被“E4 B8 AD E6 96 87”替代了,再查看由JAVA文件编译生成的CLASS文件,发现结果与JAVA文件中的完全一样。

再看JSP中指定的CharSet为ISO-8859-1的情况。


同样,该文件是用UltraEdit编写的,“中文”这两个字也是存为GB2312编码“D6 D0 CE C4”。先模拟一下生成的JAVA文件和CLASS文件的过程:jspc用ISO-8859-1来解释“中文”,并把它映射到Unicode。由于ISO-8859-1是8位的,且是拉丁语系,其映射规则就是在每个字节前加“00”,所以,映射后的Unicode编码应为“\u00D6\u00D0\u00CE\u00C4”,转化成UTF后应该是“C3 96 C3 90 C3 8E C3 84”。好,打开文件看一下,JAVA文件和CLASS文件中,“中文”果然都表示为“C3 96 C3 90 C3 8E C3 84”。

如果上述代码中不指定,即把第一行写成“”,JSPC会使用file.encoding的设置来解释JSP文件。在RedHat 6.2上,其处理结果与指定为ISO-8859-1是完全相同的。

到现在为止,已经解释了从JSP文件到CLASS文件的转变过程中中文字符的映射过程。一句话:从“JspCharSet到Unicode再到UTF”。下表总结了这个过程:

表2 “中文”从JSP到CLASS的转化过程

Jsp-CharSet
JSP文件中
JAVA文件中
CLASS文件中

GB2312
D6 D0 CE C4

(GB2312)
从\u4E2D\u6587(Unicode)到

E4 B8 AD E6 96 87 (UTF)
E4 B8 AD E6 96 87 (UTF)

ISO-8859-1
D6 D0 CE C4

(GB2312)
从\u00D6\u00D0\u00CE\u00C4 (Unicode)到C3 96 C3 90 C3 8E C3 84 (UTF)
C3 96 C3 90 C3 8E C3 84 (UTF)

无(默认=file.encoding)
同ISO-8859-1
同ISO-8859-1
同ISO-8859-1

下节先讨论Servlet从JAVA文件到CLASS文件的转化过程,然后再解释从CLASS文件如何输出到客户端。之所以这样安排,是因为JSP和Servlet在输出时处理方法是一样的。

Servlet:从源文件到Class的过程

Servlet源文件是以“.java”结尾的文本文件。本节将讨论Servlet的编译过程并跟踪其中的中文变化。

用“javac”编译Servlet源文件。javac可以带“-encoding ”参数,意思是“用< Compile-charset >中指定的编码来解释Serlvet源文件”。

源文件在编译时,用来解释所有字符,包括中文字符和ASCII字符。然后把字符常量转变成Unicode字符,最后,把Unicode转变成UTF。

在Servlet中,还有一个地方设置输出流的CharSet。通常在输出结果前,调用HttpServletResponse的setContentType方法来达到与在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编码)。

开始编译。下表是不同时,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)
\u4E2D\u6587 (在Unicode中=“中文”)

ISO-8859-1
D6 D0 CE C4

(GB2312)
C3 96 C3 90 C3 8E C3 84 (UTF)
\u00D6 \u00D0 \u00CE \u00C4 (在D6 D0 CE C4前面各加了一个00)

无(默认)
D6 D0 CE C4

(GB2312)
同ISO-8859-1
同ISO-8859-1


普通Java程序的编译过程与Servlet完全一样。

CLASS文件中的中文表示法是不是昭然若揭了?OK,接下来看看CLASS又是怎样输出中文的呢?

Class:输出字符串

上文说过,字符串在内存中表现为Unicode编码。至于这种Unicode编码表示了什么,那要看它是从哪种字符集映射过来的,也就是说要看它的祖先。这好比在托运行李时,外观都是纸箱子,里面装了什么就要看寄邮件的人实际邮了什么东西。

看看上面的例子,如果给一串Unicode编码“00D6 00D0 00CE 00C4”,如果不作转换,直接用Unicode码表来对照它时,是四个字符(而且是特殊字符);假如把它与“ISO8859-1”进行映射,则直接去掉前面的“00”即可得到“D6 D0 CE C4”,这是ASCII码表中的四个字符;而假如把它当作GB2312来进行映射,得到的结果很可能是一大堆乱码,因为在GB2312中有可能没有(也有可能有)字符与00D6等字符对应(如果对应不上,将得到0x3f,也就是问号,如果对应上了,由于00D6等字符太靠前,估计也是一些特殊符号,真正的汉字在Unicode中的编码从4E00开始)。

各位看到了,同样的Unicode字符,可以解释成不同的样子。当然,这其中有一种是我们期望的结果。以上例而论,“D6 D0 CE C4”应该是我们所想要的,当把“D6 D0 CE C4”输出到IE中时,用“简体中文”方式查看,就能看到清楚的“中文”两个字了。(当然了,如果你一定要用“西欧字符”来看,那也没办法,你将得不到任何有何时何地的东西)为什么呢?因为“00D6 00D0 00CE 00C4”本来就是由ISO8859-1转化过去的。

给出如下结论:

在Class输出字符串前,会将Unicode的字符串按照某一种内码重新生成字节流,然后把字节流输入,相当于进行了一步“String.getBytes(???)”操作。???代表某一种字符集。

如果是Servlet,那么,这种内码就是在HttpServletResponse.setContentType()方法中指定的内码,也就是上文定义的。

如果是JSP,那么,这种内码就是在中指定的内码,也就是上文定义的。

如果是Java程序,那么,这种内码就是file.encoding中指定的内码,默认为ISO8859-1。

当输出对象是浏览器时

以流行的浏览器IE为例。IE支持多种内码。假如IE接收到了一个字节流“D6 D0 CE C4”,你可以尝试用各种内码去查看。你会发现用“简体中文”时能得到正确的结果。因为“D6 D0 CE C4”本来就是简体中文中“中文”两个字的编码。

OK,完整地看一遍。

JSP:源文件为GB2312格式的文本文件,且JSP源文件中有“中文”这两个汉字

如果指定了为GB2312,转化过程如下表。

表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 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 8E 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用“西欧字符”查看结果
乱码,其实是四个ASCII字符,但由于大于128,所以显示出来的怪模怪样

8
改变IE的页面编码为“简体中文”
“中文”(正确显示)

奇怪了!为什么把设成GB2312和ISO8859-1是一个样的,都能正确显示?因为表4表5中的第2步和第5步互逆,是相互“抵消”的。只不过当指定为ISO8859-1时,要增加第8步操作,殊为不便。

再看看不指定 时的情况。

表6 未指定Jsp-charset 时的变化过程

序号
步骤说明
结果

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 8E 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中
D6 D0 CE C4

7
IE用发出请求时的页面的编码查看结果
视情况而定。如果是简体中文,则能正确显示,否则,需执行表5中的第8步


Servlet:源文件为JAVA文件,格式是GB2312,源文件中含有“中文”这两个汉字

如果=GB2312,=GB2312

表7 Compile-charset=Servlet-charset=GB2312 时的变化过程

序号
步骤说明
结果

1
编写Servlet源文件,且存为GB2312格式
D6 D0 CE C4

(D6D0=中 CEC4=文)

2
用javac –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 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 (GB2312)

6
IE用“西欧字符”查看结果
乱码(原因同表5)

7
改变IE的页面编码为“简体中文”
“中文”(正确显示)

如果不指定Compile-charset或Servlet-charset,其默认值均为ISO8859-1。

当Compile-charset=Servlet-charset时,第2步和第4步能互逆,“抵消”,显示结果均能正确。读者可试着写一下Compile-charset<>Servlet-charset时的情况,肯定是不正确的。

当输出对象是数据库时

输出到数据库时,原理与输出到浏览器也是一样的。本节只是Servlet为例,JSP的情况请读者自行推导。

假设有一个Servlet,它能接收来自客户端(IE,简体中文)的汉字字符串,然后把它写入到内码为ISO8859-1的数据库中,然后再从数据库中取出这个字符串,显示到客户端。

表9 输出对象是数据库时的变化过程(1)

序号
步骤说明
结果

1
在IE中输入“中文”
D6 D0 CE C4
IE

2
IE把字符串转变成UTF,并送入传输流中
E4 B8 AD E6 96 87

3
Servlet接收到输入流,用readUTF读取
4E 2D 65 87(unicode)
Servlet

4
编程者在Servlet中必须把字符串根据GB2312还原为字节流
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

7
JDBC检测到数据库内码为ISO8859-1
00 D6 00 D0 00 CE 00 C4
JDBC

8
JDBC把接收到的字符串按照ISO8859-1生成字节流
D6 D0 CE C4

9
JDBC把字节流写入数据库中
D6 D0 CE C4

10
完成数据存储工作
D6 D0 CE 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根据生成字节流
D6 D0 CE C4
Servlet

18
Servlet把字节流输出到IE中,如果已指定,还会设置IE的编码为
D6 D0 CE C4

19
IE根据指定的编码或默认编码查看结果
“中文”(正确显示)
IE


解释一下,表中第4第5步和第15第16步是用红色标记的,表示要由编码者来作转换。第4、5两步其实就是一句话:“new String(source.getBytes("GB2312"), "ISO8859-1")”。第15、16两步也是一句话:“new String(source.getBytes("ISO8859-1"), "GB2312")”。亲爱的读者,你在这样编写代码时是否意识到了其中的每一个细节呢?

至于客户端内码和数据库内码为其它值时的流程,和输出对象是系统控制台时的流程,请读者自己想吧。明白了上述流程的原理,相信你可以轻松地写出来。

行文至此,已可告一段落了。终点又回到了起点,对于编程者而言,几乎是什么影响都没有。

因为我们早就被告之要这么做了。

以下给出一个结论,作为结尾。

1、 在Jsp文件中,要指定contentType,其中,charset的值要与客户端浏览器所用的字符集一样;对于其中的字符串常量,不需做任何内码转换;对于字符串变量,要求能根据ContentType中指定的字符集还原成客户端能识别的字节流,简单地说,就是“字符串变量是基于字符集的”;

2、 在Servlet中,必须用HttpServletResponse.setContentType()设置charset,且设置成与客户端内码一致;对于其中的字符串常量,需要在Javac编译时指定encoding,这个encoding必须与编写源文件的平台的字符集一样,一般说来都是GB2312或GBK;对于字符串变量,与JSP一样,必须“是基于字符集的”。

凌众科技专业提供服务器租用、服务器托管、企业邮局、虚拟主机等服务,公司网站:http://www.lingzhong.cn 为了给广大客户了解更多的技术信息,本技术文章收集来源于网络,凌众科技尊重文章作者的版权,如果有涉及你的版权有必要删除你的文章,请和我们联系。以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!

分享到: 更多

Copyright ©1999-2011 厦门凌众科技有限公司 厦门优通互联科技开发有限公司 All rights reserved

地址(ADD):厦门软件园二期望海路63号701E(东南融通旁) 邮编(ZIP):361008

电话:0592-5908028 传真:0592-5908039 咨询信箱:web@lingzhong.cn 咨询OICQ:173723134

《中华人民共和国增值电信业务经营许可证》闽B2-20100024  ICP备案:闽ICP备05037997号