tomcat编码问题根源-1
应用系统经常受到中文问题的困挠,J2EE环境下的中文问题更是常见。目前缺乏对此问题的全景分析,更有一些不合理的解决方案流传在网络。本文目的在于:
分析中文问题的存在根源,解析完整的中文处理过程。
中文问题涉及面很广,因为篇幅的所限,这里不会罗列各种现象的处理办法,而是就问题产生的原因进行探讨。
1. 中文乱码问题的根源
在J2EE系统的开发过程中,稀奇古怪的中文问题很多,一些运行好好的系统,修改了一个文件,或者迁移了一个平台,换了一个服务器,就变得面目全非了。为什么只有中文问题?没有英文问题?要解决中文问题,就应该从解答这个问题开始。
1. 中文有多种编码。一个中文字符,在不同的环境下,有不同的二进制值
2. 某些应用系统,会把字符串转换成其他的字符串进行传递或保存
3. 在一个完整的软件中,各种编码形式存在很多的转换
4. 每个转换过程都需要一些参数,比如“数据到底是Unicode还是GB2312的”
5. 程序员往往会忘记设置这些参数,或者错误设置
6. 不同的应用服务器或者api,其提供的设置方式经常不一样,甚至本身就是错误
7. 另外,中文问题的泛滥还得益于一种有趣的现象:当程序中有两处出现编码错误的时候,错误可以相互抵消,产生看上去似乎正确的结果,从而成为随时可能爆发的定时**
,同时也使得程序员对错误做法产生错误的信心。
8.最后,由于J2EE平台的开放性,各种工具和服务器来源广泛,而这些提供者对中文的处理办法往往不尽相同,有些厂家自己都不知道中文怎么回事(他们不是中国人)。JDK和Servlet本身也不得不因为双字节字符问题而修改。这给J2EE环境下的中文问题处理又增加了难度。
2. 字符编码
这个字符串变量的值是什么?ABC,还是“中文”?实际上,计算机里面无法保存“中文”这个方块字,同样也无法保存ABC这样的字符,只能保存二进制的数据。
因此计算机必须有办法用一个二进制的编码来代表字符,由此产生了ISO8859-1编码(和ASCII码相似),用一个二进制字节来代表英文字母及其他常见的字符。
2.1. 中文的编码
遗憾的是,早期的计算机设计者并没有考虑到计算机将应用到全世界的各个领域,正如他们设计的日期只能到1999年为止一样。当计算机在我国应用的时候,问题就出现了。ISO8859-1并不包括中文,而且成千上万的汉字显然无法用一个字节来表示。
其解决办法是双字节的GB2312码。因为要跟ISO8859-1码兼容,所以GB码一般以一种变长的EUC模式处理,即字符a的编码仍然和ISO8859-1一致,而汉字的编码,则用两个字节来表示。
很麻烦吗?如果只有这么麻烦的话,恐怕就不会有那么多中文问题了。
首先,由于GB2312收录的字符有限,甚至连总理的名字都无法表示,因此又出现了GBK和GB18030编码(需要注意的是GBK并非国家标准)另外一些繁体字地区使用的BIG5编码,尽管原理类似,但是编码却不同,使得沟通非常困难。世界上其他地方,也有自己的双字节编码,字符编码成了“万马奔腾”的结局。如果大家各自用各自的,也就罢了,但是显然计算机专家不能容忍这种局面,于是,新的囊括几乎所以字符的双字节编码Unicode诞生了(其收录的字符仍然在增加中)。
Unicode?那UTF-8又是什么东东呢?因为Unicode的编码,和ISO8859-1并不兼容,a的ISO8859-1编码为0x41,而 Unicode为 0x00 0x41。这样一来,那些长久以来积累的英文txt文件岂不是无法使用?而UTF-8是一种变长的编码,字母的编码和ISO8859-1完全一样,中文的编码,则用三个字节表示。
通常在文本文件中使用的都是UTF-8编码,而jdk内部的String对象,才是Unicode编码。Java从设计之初,就明确了其所有的String对象,必然是Unicode编码。而字符串写到文件之中,或者供网络传输使用的时候,总之不是JVM的String对象的时候,则可能是UTF-8或者GB2312编码。
所以汉字编码,有Unicode,GB2312和BIG5等形式。一个中文字符,在不同的软件不同的环境下,有不同的二进制值。虽然java String对象总是Unicode,但是J2EE是一个需要和很多应用系统打交道的环境,比如说,用户提交的数据,并不是java的String,而是浏览器提交的几个字节,配置文件的内容,是文件系统中的几个字节。
然而即使如此,故事仍远未结束。
2.2. 另一个故事:把任意数据编码成其他ISO8859-1字符串
如果某协议只能使用可打印的ISO8859-1字符串(编码为0-127的字符,即字母,数字等),而现在需要处理图片数据,或中文数据,那怎么办?比如原本的邮件传输协议,就是如此。
这就出现了BASE64编码,简单的说,它总是把3个字节的数据,转换成4个字节来表示,而这每个字节,都是一个“可打印”的ISO8859-1字符。 BASE64只使用64个字符,即字母,数字和加号,斜杠。根据使用的字符数量的不同,还有BASE85等编码。类似的情况,在别的应用领域还有其他编码。
这些协议中最常见的,正是我们最“喜欢”的http。因为原本的http协议只能传递ISO8859-1字符串,所有的中文,不管是何种编码,都无法传递,其他任何双字节编码也一样。
因此,http的url参数使用如下的形式:
www.site.com/index.jsp?name=a&age=1&q=ÖÐÎÄ
ÖÐÎÄ “中”的GB2312码为十六进制的D6 D0,但是这两个字节根本无法放到http的协议中去,因此计算机用一个字符串来保存这些数据。把这两个字节用6个字符来表示:ÖÐ。接受方首先根据这个字符串还原字节数组D6 D0,加上知道使用的是GB2312码,就可以知道这6个字符代表的意思。
又如,xml是纯粹的字符串文档,有很多符号都被语法本身所保留,如&
因此在xml文本中,一些字符需要用特殊的方式来表示,如&用&来代替。而更统一的方法,是用以下的形式
中文 表示“中文”
中文 同样表示“中文”,注意20013=x4e2d
又如,JDK的native2ascii的运行结果(在java的properties文件中使用)
\u4e2d\u6587 表示“中文”
让事情更糟的是,这些编码方式和前面的汉字编码并非相加的关系,而是相乘的关系,也就是说,可以用来编码GB2312的字符串,同样也可以用来编码Unicode的字符串。
前面所列出的一些例子,分别是(文字都是“中文”):
q=ÖÐÎÄ
“中文”的GB2312码
中文
“中文”的Unicode码 0x4e 0x2d 0x65 0x87
显然并没有谁规定这些方式只能编码Unicode或者UTF-8的数据,当然还可以是GB2312或者BIG5或者其他什么东西
2.3. 简单应用系统中的中文编码
也许读者会问,这些跟我的中文问题有什么关系?我从来没有让我的程序使用过UTF-8或者那些古怪的字符串编码。
遗憾的是,每个程序员确实需要关心这些东西。不管你愿意不愿意,一个简单的java应用,已经在你并没有注意地情况下,使用了以上的很多编码。而这些编码过程在程序的处理过程中,就必须要经过很多次的转换,任何一个转换的错误,都可能是中文问题的根源!
一个玩具性质的JSP应用,就已经使用了GB2312,Unicode,UTF-8,还有这些字符串所转换出来的各种形式。一个普通的J2EE应用,涉及的编码更是繁多。
让我们来看一下在一个最简单的JSP页面中,“中文”两个字的各种表现形式。必须要说明一点的是,只有在应用程序完全按照tomcat的推荐方法设置的时候,以下的字符才会是这种形式,任何一个参数的不同,都将造成结果的不同。
3. 字符转换过程
我们已经看到,在一个过于简单的系统中,已经有3种以上的中文编码,繁多的字符串编码模式,组合出各种奇怪的数据。
而所有这些在程序中运转的时候,都会涉及很多转换的过程。
举简单的例子(以下的编码只是举例,并不是说在这个环节不能使用其他的编码):
1. 用户的输入是GB2312,java需要读取成Unicode的字符串
2. 如果在java代码中写了中文,java编译的类文件会用UTF-8来保存
3. 配置文件使用了java的编码模式(\ u4e2d)
4. 数据序列化XML的形式时,会把字符串转换成XML的字符编码(中)
读取的时候当然是相反的过程。
3.1. 一个真实的转换过程
经常听到某人说“我这个String是GB2312的”,实际上,String对象永远是Unicode。JVM要处理这些数据,首先从外部的IO流读取字节数据,然后将其还原为String对象。
举例来说,用户用http表单提交的数据中的“中”字,会用六个ISO8859-1字符表示:%D6%D0。注意这里的D6是两个字符,而不是十六进制的数字!他们的作用正是要代表D6 和 D0两个十六进制的数字。
当应用程序用request.getParameter来读取数据的时候,Tomcat处理字符转换大致是这样的原理(实际的处理过程当然决非这么简单,因为tomcat要考虑性能可靠性等因素)
import java.io.UnsupportedEncodingException; public class ConvertHttp2String { public static void main(String[] args) throws UnsupportedEncodingException { byte[] input = { 0x25, 0x44, 0x36, 0x25, 0x44, 0x30 }; System.out.println("http协议提交的数据为:" + new String(input, "ISO-8859-1")); // 1 转换为16进制数,应该有两个16进制数 byte[] code = new byte[2]; for (int i = 0; i <= 1; i++) { String temp = new String(input, i * 3, 3, "ISO-8859-1"); // 去掉百分号 temp = temp.substring(1, 3); // 计算字符代表的16进制数 code[i] = (byte) Integer.parseInt(temp, 16); } // 2 创建字符串,第一个参数为数据,第二个参数为编码类型 String result = new String(code, "GB2312"); System.out.println("代表字符串:" + result); } }
输出结果:
http协议提交的数据为:%D6%D0
代表字符串:中
上一篇: Java基础知识(不断更新)
下一篇: (转)Android模拟器的属性配置介绍