tomcat乱码根源

应用系统经常受到中文问题的困挠,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要考虑性能可靠性等因素)
复制代码
  1. import java.io.UnsupportedEncodingException;
  2. public class ConvertHttp2String
  3. {  
  4.         public static void main(String[] args) throws UnsupportedEncodingException  
  5.         {  
  6.                byte[] input = { 0x25, 0x44, 0x36, 0x25, 0x44, 0x30 }; 
  7.                System.out.println("http协议提交的数据为:" + new String(input, "ISO-8859-1"));  
  8.                // 1 转换为16进制数,应该有两个16进制数
  9.                byte[] code = new byte[2]; 
  10.                for (int i = 0; i <= 1; i++)  
  11.                {            
  12.                       String temp = new String(input, i * 3, 3, "ISO-8859-1");
  13.                       // 去掉百分号 
  14.                       temp = temp.substring(1, 3);
  15.                       // 计算字符代表的16进制数  
  16.                       code[i] = (byte) Integer.parseInt(temp, 16); 
  17.                 }  
  18.                 // 2 创建字符串,第一个参数为数据,第二个参数为编码类型 
  19.                 String result = new String(code, "GB2312"); 
  20.                 System.out.println("代表字符串:" + result);    
  21.          }
  22. }


    输出结果:
    http协议提交的数据为:%D6%D0
    代表字符串:中

    3.2. 转换过程的参数设置
    在以上的转换过程中可以看到,new String(code, "GB2312");这一处的第二个参数非常重要,它告诉java,这个字符串的原始编码是什么,如果写成其他的编码,必然会发生错误。

    因此,转换过程中的参数,java必须有途径得知正确的参数。 
    举例说,让java从文本文件里面读取一段中文。Java最终得到的字符串肯定是Unicode的,但是它需要程序员告诉它,这个文本文件到底是GB2312的,还是UTF-8的。如果程序员不告诉它,它就会贸然选择一种编码,进行解释,其结果可能就是乱码了
又举例说,如果一个Servlet得到了一个http的请求(request),需要程序员告之,这个请求里面的字符串是UTF-8,GB2312,还是纯粹的英文?当然,在得不到信息的时候,它又会自己胡乱猜一个。

    怎么?你不记得自己在代码里面指定过?对了,也许这就是中文问题的原因。 
    多数应用平台(应用服务器[/url]或其他)都不是关心中文问题的人编写的,所以不同的应用平台之间,设置参数的方式并不一样,可以说千奇百怪,甚至根本错误。

    在最新版本的tomcat上,推荐的处理方法涉及4个参数
    1. 网页必须制定http content type
    2. JSP必须制定JSP content type
    3. servlet/jsp的请求,必须设定字符集request.setCharacterEncoding.
    4. 修改tomcat的server.xml文件中URIEncoding的设置
    正如文章开头提到的,不同应用 服务器的方式并不一致。前3个参数都是规范规定的,而第4的参数并没有规定,所以不同的服务器要用不同的方法。比如tomcat4不需要这个参数,它会直接使用参数3。

    有人认为修改tomcat的配置文件麻烦,而自己写转码的函数进行处理,这不是一个很好的方法,如果转换服务器,因为服务器本身的处理方式变化了,这段“转码”函数很有可能得到面目全非的结果,比如在tomcat4上。

    这里面似乎存在很多重复,但是没有办法。有时候给一段字符给java,java确实没有办法猜测其字符集。如参数3本来是没有的,J2EE规范认为浏览器提交的数据会描述数据的字符集,但是实际上没有浏览器遵守,所以才不得不引入这个参数。 

    4. 中文问题的非技术原因
    以上分析了中文问题出现的技术原因,除此以外,还存在两个不是纯技术方面的因素 
    
    4.1. 错误做法的信心从何而来
    早在几年前的tomcat3年代,网上流传的种种解决方法中,就有一种声称不能给JSP指定字符集,否则会造成乱码。这是一个很奇怪的说法,似乎SUN设计的机制完全错误一样,但是何以有不少人相信并且深受其害呢?
因为中文编码本身的特性,有时候几个错误的操作,反而会得到似乎正确的结果。
比如,“中”的GB2312编码有两个字节D6 D0
    1. 如果要把它作为ISO8859-1来处理,实际上是对应两个字符的,即Ö 和Ð。
    2. 如果不指定用户提交数据的字符集,tomcat会以为提交的是这两个字符。
    3. 经过tomcat处理之后,再次输出到相应页面,tomcat当然仍然以ISO8859-1的方式输出两个字符Ö 和Ð
    4. 这样,客户端得到了ISO8859-1的ÖÐ,也就是和GB2312的“中”一样的两个字节。因此他会觉得,自己提交的数据被正确的处理了。

    实际上这样的做法是非常危险的
    首先,这个时候在tomcat的内部,代表这个数据的String的值不是Unicode的“中”,而是Unicode的Ö和Ð也就是4个字节(不要忘记,java的字符串既不是ISO8859-1,也不是GB2312),尽管把这两个错误的字符转为ISO8859-1的字节输出后得到了似乎正确的结果,但是如果系统中还有其他不是来自于用户输入的字符串呢?比如说来自于数据库?tomcat如果把正确的字符串按照同样的方法处理,自然就会造成乱码。
其次,因为String的内容并不是应用程序要处理的内容本身,甚至连长度都变了,所以很多字符串操作都会出现莫名其妙的错误。

    最简单的例子:
    下载最新的tomcat,运行
    1. 访问 http://localhost/servlets-examples/servlet/RequestParamExample
    2. 点击右键把字符集改成GB2312
    3. 分别在两个框输入中 文
    4. 提交
    5. 得到一个显示乱码的页面(左)
    6. 再点击右键把字符集改成GB2312
    7. “中文”两个字出现了(右)

    结果似乎正确,除了要手工改浏览器字符集以外(某些版本的浏览器并不需要)。但是只要使用调试器,或者直接在代码中打印用户输入的字符,就可以看到,得到的用户输入不是“中文”,而是ÖÐ ÎÄ。
这就是为什么有些应用程序“界面显示正确,但是控制台打印的都是乱码”的原因。当你知道自己看起来正确的系统中正在运行着这些奇怪的字符的时候,该考虑放弃这些错误的“捷径”了
   4.2. 并不可靠的工具和服务器
    习惯上,我们总是认为我们使用的开源工具或者商业 服务器是正确的,起码是没有大问题的。这个假设在很多时候正确,但是在中文问题方面,并非如此。别忘了多数 软件的作者都是欧美人,他们中的很多并不懂也不关心中文或者其他双字节字符集的问题。因此有些工具或者应用服务器在中文处理方法上模糊不清,或者根本错误(真正可怕的是某些国产商业软件都是错误的)。
    这给我们的工作增加了很多难度,因为你的代码在别人的应用服务器上运行,有时候你必须适应他们代码里面的错误。
有很多工具有提供所谓的转码功能,在你弄清楚它到底是做什么之前,不要贸然使用,起码要做一个备份。已经有不少人使用这种“电子 碎纸机”杀死了自己的中文数据。
就连java的规范本身,也存在漏洞,比如String.getBytes()这个函数。其设计就是根本错误的,尽管新版的jdk已经声明废止这个函数,但是很多人还在使用。又如URI的编码方式,servlet规范并没有指出应该如何设置,这才导致了tomcat实用自定义的URIEncoding参数。

   5. 小结
    作为一般的应用开发者,没有必要去深究中文字节的每个转换过程。但是,我们需要记住,中文存在很多种形式,而Java如何处理这些形式,需要程序员告知。
简单的说,最可靠的处理办法,就是保证中文在每一个环节都正确。这并不是废话。如果程序员严格按照规范的推荐,设置所需要的各种参数,最后即使遗漏了什么,也是很容易找到原因的。相反,如果走所谓的捷径,有意的疏漏或者错误设置字符集,或用大串的getBytes()进行可疑的转换,也许可以得到似乎正确的结果,但是当问题出现的时候,就很可能成为死结了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值