7 e0 v; T/ M: f3 C Servlet源文件是以“.java”结尾的文本文件。本节将讨论Servlet的编译过程并跟踪其中的中文变化。) Q6 {: h- B; P W7 p: v$ Z
$ J d" n# j9 j+ w4 v
用“javac”编译Servlet源文件。javac可以带“-encoding <Compile-charset>”参数,意思是“用< Compile-charset >中指定的编码来解释Serlvet源文件”。 * ?5 B% Z4 O/ ], g; B! A( S5 V " p, z; K* t" M# [" n) Q 源文件在编译时,用<Compile-charset>来解释所有字符,包括中文字符和ASCII字符。然后把字符常量转变成Unicode字符,最后,把Unicode转变成UTF。 / V2 k4 O ^" x, Y3 l4 p" i . Q5 @0 ?6 W$ q& i7 x/ R6 s 在Servlet中,还有一个地方设置输出流的CharSet。通常在输出结果前,调用HttpServletResponse的setContentType方法来达到与在JSP中设置<Jsp-charset>一样的效果,称之为<Servlet-charset>。 , W$ G3 l) D' u/ D0 L 0 K g6 K+ t9 R; b' ]- R 注意,文中一共提到了三个变量:<Jsp-charset>、<Compile-charset>和<Servlet-charset>。其中,JSP文件只与<Jsp-charset>有关,而<Compile-charset>和<Servlet-charset>只与Servlet有关。$ F" `/ U s9 t: z: L9 l- Y) Y
& ]6 H: {+ H$ f: F3 L9 P; Q8 Q
看下例: 5 } E5 }* L# @% t$ ?9 s7 U7 K 2 S- H; a5 B2 x7 i, ], @import javax.servlet.*;, [6 N2 L# ~ M. k T( n( ~
: F/ v" d& ]) K$ v7 _9 K
import javax.servlet.http.*;3 i8 Z4 s0 {# G1 n# G
. R3 n7 ~5 q3 M1 sclass testServlet extends HttpServlet 8 {1 K* s9 z9 ?: V) f* U7 p* q9 N9 e% E+ {{" ]8 k+ \6 h% X5 k2 N: W
public void doGet(HttpServletRequest req,HttpServletResponse resp) l! V4 c. G1 T
throws ServletException,java.io.IOException 9 h9 ]4 z$ X! l { 7 S9 m* B! F( f; V/ Z$ _) }; p resp.setContentType("text/html; charset=GB2312"); $ n, e, l$ D/ b( f java.io.PrintWriter out=resp.getWriter();: H* y, O A0 Z- A
out.println("<html>");2 n$ q" G$ w5 n
out.println("#中文#"); 9 e5 M2 Y ^3 A: q8 ?/ j out.println("</html>"); 9 W8 G0 c/ r+ P% w0 u } ]& ^7 s: r: p9 o. l} 0 c1 ?3 y% g( T' T4 ` 该文件也是用UltraEdit for Windows编写的,其中的“中文”两个字保存为“D6 D0 CE C4”(GB2312编码)。$ q+ O3 e5 j1 [+ L+ B8 F0 a
# S% P0 k8 r5 f: L* K 开始编译。下表是<Compile-charset>不同时,CLASS文件中“中文”两字的十六进制码。在编译过程中,<Servlet-charset>不起任何作用。<Servlet-charset>只对CLASS文件的输出产生影响,实际上是<Servlet-charset>和<Compile-charset>一起,达到与JSP文件中的<Jsp-charset>相同的效果,因为<Jsp-charset>对编译和CLASS文件的输出都会产生影响。 1 F7 J% _8 }: @" J2 G* H1 @3 a+ T; u' t5 ]4 {4 Z& S
表3 “中文”从Servlet源文件到Class的转变过程1 r- q6 ?; g1 J; F2 L
# P% G: |6 G O9 l8 F
Compile-charsetServlet源文件中Class文件中等效的Unicode码GB2312D6 D0 CE C4 4 W$ `3 p' Z( x( m
(GB2312)E4 B8 AD E6 96 87 (UTF)\u4E2D\u6587 (在Unicode中=“中文”)ISO-8859-1D6 D0 CE C4 0 @( k" A: r( _7 a
(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 ) u* P0 I6 i/ R& ~) q1 @# \* K 普通Java程序的编译过程与Servlet完全一样。 * l+ @* y% `/ ?( Y6 J . n8 Z, @7 H1 r! g CLASS文件中的中文表示法是不是昭然若揭了?OK,接下来看看CLASS又是怎样输出中文的呢?* U3 ?9 T; Y, U+ R! n
0 _5 _3 c* x y4 p/ s8 g Class:输出字符串 5 a6 @! g4 u" S- U! l3 D * y6 w, h- `1 _$ ~& A6 ]2 S 上文说过,字符串在内存中表现为Unicode编码。至于这种Unicode编码表示了什么,那要看它是从哪种字符集映射过来的,也就是说要看它的祖先。这好比在托运行李时,外观都是纸箱子,里面装了什么就要看寄邮件的人实际邮了什么东西。: _ o/ h. u" s% q9 V
7 p4 ~7 V* R# S; v5 u' _, ~% U$ s 看看上面的例子,如果给一串Unicode编码“00D6 00D0 00CE 00C4”,如果不作转换,直接用Unicode码表来对照它时,是四个字符(而且是特殊字符);假如把它与“ISO8859-1”进行映射,则直接去掉前面的“00”即可得到“D6 D0 CE C4”,这是ASCII码表中的四个字符;而假如把它当作GB2312来进行映射,得到的结果很可能是一大堆乱码,因为在GB2312中有可能没有(也有可能有)字符与00D6等字符对应(如果对应不上,将得到0x3f,也就是问号,如果对应上了,由于00D6等字符太靠前,估计也是一些特殊符号,真正的汉字在Unicode中的编码从4E00开始)。/ |! G5 g6 V8 v+ I; i
; L$ I, y* r- k$ Y) X" P
各位看到了,同样的Unicode字符,可以解释成不同的样子。当然,这其中有一种是我们期望的结果。以上例而论,“D6 D0 CE C4”应该是我们所想要的,当把“D6 D0 CE C4”输出到IE中时,用“简体中文”方式查看,就能看到清楚的“中文”两个字了。(当然了,如果你一定要用“西欧字符”来看,那也没办法,你将得不到任何有何时何地的东西)为什么呢?因为“00D6 00D0 00CE 00C4”本来就是由ISO8859-1转化过去的。4 R0 j$ h; H1 q, l$ S+ z4 H8 v, M
2 Q6 L/ ~9 W9 G/ ~( S6 b给出如下结论: 2 b$ q+ S' Y: E" a4 x. p* z6 m7 Y2 | a& I8 m/ _
在Class输出字符串前,会将Unicode的字符串按照某一种内码重新生成字节流,然后把字节流输入,相当于进行了一步“String.getBytes(???)”操作。???代表某一种字符集。 ) h/ }/ D. L+ p& i) K& }! l' I+ ^ i , x: T. l& d+ `( Y, N( H 如果是Servlet,那么,这种内码就是在HttpServletResponse.setContentType()方法中指定的内码,也就是上文定义的<Servlet-charset>。 , t4 r% i) ?7 b% A2 W" U) V& ]" Q- V* ?( M8 m' @$ o, V
如果是JSP,那么,这种内码就是在<%@ page contentType=""%>中指定的内码,也就是上文定义的<Jsp-charset>。 & A3 x# N+ U# r. e5 o; O, R 6 o3 u2 f+ ^, c 如果是Java程序,那么,这种内码就是file.encoding中指定的内码,默认为ISO8859-1。 1 G4 M& P0 M5 d$ H1 U 9 L o' c) L( G% X/ P- ^( ~6 L 当输出对象是浏览器时 , a3 M7 D$ h' l4 X2 T7 d - l4 K5 N3 W/ P2 A& @/ B 以流行的浏览器IE为例。IE支持多种内码。假如IE接收到了一个字节流“D6 D0 CE C4”,你可以尝试用各种内码去查看。你会发现用“简体中文”时能得到正确的结果。因为“D6 D0 CE C4”本来就是简体中文中“中文”两个字的编码。 6 x0 C$ q" Q( y6 g, N) t& `* W2 }1 d$ [
OK,完整地看一遍。 - Q. Z8 S& F, { / L0 E& x8 t7 J JSP:源文件为GB2312格式的文本文件,且JSP源文件中有“中文”这两个汉字 % k8 ^6 P& p) [; n0 C, K 0 B0 Y& O' ^2 E6 j5 X5 X 如果指定了<Jsp-charset>为GB2312,转化过程如下表。 7 J/ F! w ~1 k" }+ m! k7 d2 {0 J
表4 Jsp-charset = GB2312时的变化过程 6 h" L4 u4 d' J( @7 B5 m; n& G0 E9 c' C- |
序号步骤说明结果1编写JSP源文件,且存为GB2312格式D6 D0 CE C45 ?' M4 s6 A9 U2 X. `8 o9 e s& G
(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用“简体中文”查看结果“中文”(正确显示) ) ~7 \% T+ D6 {& E* o! t+ A: y/ d 如果指定了<Jsp-charset>为ISO8859-1,转化过程如下表。 ; e2 {* p: W) }5 F; L 8 d; v. m2 P! E7 i0 _ 表5 Jsp-charset = ISO8859-1时的变化过程3 k! z1 p# ^' S! A$ \' m( v; k
$ m* R5 c* q( f0 W/ k序号步骤说明结果1编写JSP源文件,且存为GB2312格式D6 D0 CE C4 ( |$ U* b0 J/ U1 \(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 8E C3 844运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码00 D6 00 D0 00 CE 00 C41 {9 K$ f' I. N- @3 R
(啥都不是!!!)5根据Jsp-charset=ISO8859-1把Unicode转化为字节流D6 D0 CE C46把字节流输出到IE中,并设置IE的编码为ISO8859-1(作者按:这个信息隐藏在HTTP头中)D6 D0 CE C47IE用“西欧字符”查看结果乱码,其实是四个ASCII字符,但由于大于128,所以显示出来的怪模怪样8改变IE的页面编码为“简体中文”“中文”(正确显示)$ t2 ^7 L3 t3 ?6 E5 [- J. W
奇怪了!为什么把<Jsp-charset>设成GB2312和ISO8859-1是一个样的,都能正确显示?因为表4表5中的第2步和第5步互逆,是相互“抵消”的。只不过当指定为ISO8859-1时,要增加第8步操作,殊为不便。# n9 n! b" P: O! e
1 o2 Y, ?' [* o' }4 z
再看看不指定<Jsp-charset> 时的情况。 ! }1 D+ L# s2 B7 V2 Q6 x $ n% y$ h% d& V2 }2 i 表6 未指定Jsp-charset 时的变化过程 6 Y/ j1 l: Q" c9 H 3 q; s7 h8 z! Q4 E1 Z) A序号步骤说明结果1编写JSP源文件,且存为GB2312格式D6 D0 CE C4 $ n+ H& v/ K0 r }1 [6 \$ F. @- E(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 8E C3 844运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码00 D6 00 D0 00 CE 00 C45根据Jsp-charset=ISO8859-1把Unicode转化为字节流D6 D0 CE C46把字节流输出到IE中D6 D0 CE C47IE用发出请求时的页面的编码查看结果视情况而定。如果是简体中文,则能正确显示,否则,需执行表5中的第8步 ) H2 H! Z. `9 P6 E Servlet:源文件为JAVA文件,格式是GB2312,源文件中含有“中文”这两个汉字& B7 _: V: K/ R
' C* `# l5 ] X, _+ b& M
如果<Compile-charset>=GB2312,<Servlet-charset>=GB2312+ S3 x0 t _% F: h3 \* F
8 J4 y# d7 C7 O* x 表7 Compile-charset=Servlet-charset=GB2312 时的变化过程5 d+ P7 l, M/ [% w! U: n2 j
( U5 V/ t: z. t9 M: X
序号步骤说明结果1编写Servlet源文件,且存为GB2312格式D6 D0 CE C42 _ s9 M2 D" x9 }
(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=GB2312D6 D0 CE C4 (GB2312)6IE用“简体中文”查看结果“中文”(正确显示): o6 `: ~* n' s5 B* W+ G7 Y
如果<Compile-charset>=ISO8859-1,<Servlet-charset>=ISO8859-1: K# j& F" i6 b! R# f( n
" e$ m$ Z" F& Q8 K; G4 | H. R2 f
表8 Compile-charset=Servlet-charset=ISO8859-1时的变化过程 6 n8 c- V; I. n; I: ?/ @$ b: O9 B, A3 m1 x1 Y/ j' h
序号步骤说明结果1编写Servlet源文件,且存为GB2312格式D6 D0 CE C4 1 a6 y O% s. W! r(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 C44根据Servlet-charset=ISO8859-1把Unicode转化为字节流D6 D0 CE C45把字节流输出到IE中并设置IE的编码属性为Servlet-charset=ISO8859-1D6 D0 CE C4 (GB2312)6IE用“西欧字符”查看结果乱码(原因同表5)7改变IE的页面编码为“简体中文”“中文”(正确显示) 4 h2 t/ x. l! d+ _( V1 v 如果不指定Compile-charset或Servlet-charset,其默认值均为ISO8859-1。& V3 A" x) g" [7 N# d/ x" b" H; v
) c; G$ N' ?8 m. u
当Compile-charset=Servlet-charset时,第2步和第4步能互逆,“抵消”,显示结果均能正确。读者可试着写一下Compile-charset<>Servlet-charset时的情况,肯定是不正确的。 3 a6 E2 t0 F% x - F0 k6 q* B' a; Q6 _ 当输出对象是数据库时- O; G8 _1 s4 p+ A/ p7 w
1 n& T) ~+ ?5 s
输出到数据库时,原理与输出到浏览器也是一样的。本节只是Servlet为例,JSP的情况请读者自行推导。* m" p1 }3 r/ d9 {! O" C1 y2 ]$ r
4 l* c2 ~5 l- R
假设有一个Servlet,它能接收来自客户端(IE,简体中文)的汉字字符串,然后把它写入到内码为ISO8859-1的数据库中,然后再从数据库中取出这个字符串,显示到客户端。 6 h4 O% k& Y6 c2 M9 e- r: y, _7 e+ n0 [3 K! P! M* n. T
表9 输出对象是数据库时的变化过程(1), k6 ~# _( O0 W. G
9 ~1 K0 Q5 J3 W0 r) p# T序号步骤说明结果域1在IE中输入“中文”D6 D0 CE C4IE2IE把字符串转变成UTF,并送入传输流中E4 B8 AD E6 96 873Servlet接收到输入流,用readUTF读取4E 2D 65 87(unicode)Servlet4编程者在Servlet中必须把字符串根据GB2312还原为字节流D6 D0 CE C45编程者根据数据库内码ISO8859-1生成新的字符串00 D6 00 D0 00 CE 00 C46把新生成的字符串提交给JDBC00 D6 00 D0 00 CE 00 C47JDBC检测到数据库内码为ISO8859-100 D6 00 D0 00 CE 00 C4JDBC8JDBC把接收到的字符串按照ISO8859-1生成字节流D6 D0 CE C49JDBC把字节流写入数据库中D6 D0 CE C410完成数据存储工作D6 D0 CE C4 数据库以下是从数据库中取出数的过程11JDBC从数据库中取出字节流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)Servlet14编程者必须根据数据库的内码ISO8859-1还原成原始字节流D6 D0 CE C4 15编程者必须根据客户端字符集GB2312生成新的字符串4E 2D 65 87 8 v4 E5 }' M2 ^+ p(Unicode) Servlet准备把字符串输出到客户端16Servlet根据<Servlet-charset>生成字节流D6D0 CE C4Servlet17Servlet把字节流输出到IE中,如果已指定<Servlet-charset>,还会设置IE的编码为<Servlet-charset>D6 D0 CE C418IE根据指定的编码或默认编码查看结果“中文”(正确显示)IE % H: L$ k6 ~4 [) A" ^: T 解释一下,表中第4第5步和第15第16步是用红色标记的,表示要由编码者来作转换。第4、5两步其实就是一句话:“new String(source.getBytes("GB2312"), "ISO8859-1")”。第15、16两步也是一句话:“new String(source.getBytes("ISO8859-1"), "GB2312")”。亲爱的读者,你在这样编写代码时是否意识到了其中的每一个细节呢? 6 b: ~* l8 [0 X/ {1 b, f* \ m, R n3 H% Q* R
至于客户端内码和数据库内码为其它值时的流程,和输出对象是系统控制台时的流程,请读者自己想吧。明白了上述流程的原理,相信你可以轻松地写出来。' g* T% F. D& K: g
, k# T4 L" g$ j2 t# M 行文至此,已可告一段落了。终点又回到了起点,对于编程者而言,几乎是什么影响都没有。 ( u. [2 f. C n/ i k4 u. v % s3 ]1 O& v+ K$ F7 | 因为我们早就被告之要这么做了。 - J" c5 Q3 M7 _$ X+ }+ C; ]3 r6 a: ~; y5 O& ~4 y% o
以下给出一个结论,作为结尾。1 W$ Q! M1 G/ B$ N, V
/ c6 ~3 `+ m0 v f1 c
1、 在Jsp文件中,要指定contentType,其中,charset的值要与客户端浏览器所用的字符集一样;对于其中的字符串常量,不需做任何内码转换;对于字符串变量,要求能根据ContentType中指定的字符集还原成客户端能识别的字节流,简单地说,就是“字符串变量是基于<Jsp-charset>字符集的”;1 E+ w7 Y/ O, J- d) W0 Q
/ d: G/ t. D- c. V" ^; X+ N 2、 在Servlet中,必须用HttpServletResponse.setContentType()设置charset,且设置成与客户端内码一致;对于其中的字符串常量,需要在Javac编译时指定encoding,这个encoding必须与编写源文件的平台的字符集一样,一般说来都是GB2312或GBK;对于字符串变量,与JSP一样,必须“是基于<Servlet-charset>字符集的”。