日期:2014-05-19  浏览次数:20838 次

实现J2EE中的多字节字符处理

中文是世界上最复杂、最完备的语言之一。有时我会为自己是个中国人而感到幸运,非凡是当我看到我的一些外国朋友为学习这门语言(尤其是写汉字)而绞尽脑汁的时候。可当我用J2EE开发本地化Web应用时,又会感到很不幸。下面我就说说为什么。
  
  尽管Java平台和大多数J2EE服务器都能很好地支持国际化,我在开发中文或日文应用时仍会碰到许多有关多字节字符方面的问题:
  
  ·编码和字符集之间有什么区别?
  ·为什么多字节字符应用从一种操作系统迁移到另一种上时显示会有差异?
  ·为什么多字节字符应用从一种应用服务器迁移到另一种上时显示会有差异?
  ·为什么我的多字节字符应用在IE浏览器里显示正常,可到了Mozila浏览器里却又不行?
  ·为什么以UTF-16(通用转换格式)编码的应用在大多数J2EE服务器上都不能很好地显示?
  
  假如你也有同样的问题,本文将有助于你找到答案。
  
  字符的基础知识
  
  字符在计算机出现之前就早已存在。大约3,000年前,古代中国就出现了一些非凡的文字符号(即甲骨文)。这些文字符号有特定的外形和含义,它们中的大部分都有名字和发音。所有这些文字符号汇集成了字码表,一套从属于特定语言的独特字符集合,它们与计算机没有任何关系。几千年过去了,许多语言都在发展,数以千计的字符被创造出来。如今我们要将所有这些字符都数字化为0和1,这样计算机才能理解它们。
  
  用键盘输入单词的时候要用到字符输入法。对于简单的字符,键盘和字符间存在着一对一的映射关系;而对于较复杂的语言,需要多次敲击键盘才能输入一个字符。
  
  在你能从屏幕上看到字符之前,操作系统必须先将字符存放在内存里。实际上操作系统在字码表的字符与一系列非负整数之间定义了一一对应的关系,它们被存放在内存里并被操作系统调用,这些整数被称为字符代码。
  
  字符可以用文件存储或通过网络传输。软件用字符编码来定义每个字符的字符代码与八进制序列数之间的对应方法(算法)。有些字符代码对应一个字节,如ASCII码;还有一些字符代码需要对应两个或更多的字节(如中文和日文),这种对应关系依靠于不同的字符编码方式。
  
  不同的语言使用不同的字码表,每个字码表都有一些特定的编码方式。在某些情况下,当你选用某种语言时就已经不自觉地选择了某种字符编码方式。例如当你选用中文的时候,在默认情况下你用的可能就是GBK中文字码表及称作GBK的特定字符编码方式。
  
  为了不致混淆,我避免使用字符集这个词。显然,字符集与字码表是同义词。字符集在HTTP Mime(多用途网际邮件扩充协议)页头里被误用,其实这里的“charset(字符集)”是指“encoding(编码)”。
  
  Java的特征之一是字符是16位的,这样就能支持Unicode(一种表示各种语言中许多不同种类的字符的标准方式)。不幸的是,这个特征在开发多字节J2EE应用中也引发了许多问题,本文将就此进行讨论。
  
  开发阶段引起的显示问题
  
  J2EE应用开发包括若干个阶段(如图1所示),每个阶段都可能导致多字节字符显示问题。
  

 
  图1 J2EE应用开发生命周期


  
  编码阶段
  
  当你开始J2EE应用编码时,大多数情况下你会用JBuilder、NetBean之类的IDE,或者是UltraEdit、Vi之类的编辑器。无论你选择了哪一个,只要在jsp(JavaServer Pages)、Java或Html文件中有文字字符串,而且这些字符串是像中文或日文这样的多字节字符,那么你要是不小心的话就很可能会碰到显示问题。
  
  文字字符串是存储于文件中的静态信息,不同语言的字符采用不同的编码方式。大多数IDE的默认编码方式是ISO-8859-1,这种编码方式适合ASCII字符,但会使多字节字符丢失信息。例如,中文版的NetBean在对文件编码时其默认编码方式就不幸为ISO-8859-1。在我编写带有中文字符的JSP文件时(如图2所示),看上去一切正常。我前面提到,屏幕上显示的所有字符都在内存中,与编码方式没有直接的关系。在保存文件后,假如关闭IDE再重新打开,这些字符就显示为乱码(如图3所示),这是因为ISO-8859-1编码方式在存储中文字符时会丢失一些信息。
  

 
  图3 中文字符成了乱码


  
  字符编码API
  
  在servlet和JSP规范里有几个API用来控制J2EE应用的字符编码过程。对于servlet请求,setCharacterEncoding()方法对当前HTTP请求设定编码方式;对于servlet响应,setContentType()方法和setLocale()方法对HTTP响应输出设置Mime头的编码方式。
  
  这些API本身不会引发问题,但假如你忘了用它们就有问题了。例如在有些服务器上你可以正确无误地显示多字节字符而不必在代码中使用上述的任何一个API,但在其它的服务器上运行应用时字符却变成了乱码。多字节字符显示问题的成因在于服务器在处理HTTP请求和响应期间如何对字符进行编码。以下是服务器确定请求和响应的编码方式的规则,对大多数服务器都适用:
  
  在处理servlet请求时,服务器按以下次序(自上而下)来确定请求的字符编码方式:
  
  ·代码中指定的设置(如setCharacterEncoding()方法中指定的编码方式)
  ·厂商的初始设置
  ·默认的设置
  
  在处理servlet响应时,服务器按以下次序(自上而下)来确定响应的字符编码方式:
  
  ·代码中指定的设置(如setContentType() 方法和setLocale()方法中指定的编码方式)
  ·厂商的初始设置
  ·默认的设置
  
  按照上述规则,假如在代码中用API进行了指定,所有的服务器都会按指定的字符编码方式编码,否则服务器就会各行其道。有些厂商用HTTP表单的隐藏字段(hidden fields)来确定请求的编码方式,还有些厂商则采用它们自己配置文件中的特定设置。即使是默认设置也不尽相同,大多数厂商采用ISO-8859-1作为默认设置,还有少数厂商采用操作系统的本地设置值。因此,一些带有多字节字符的应用在迁移到另一个厂商的J2EE服务器上时就会出现显示问题。
  
  编译阶段
  
  假如设置正确,在编辑的时候就能在源文件中存储多字节的文字字符串,但这些源文件不能直接执行。假如编写的是servlet代码,这些Java文件在部署到应用服务器之前必须先被编译成类文件。对于JSP文件,应用服务器在执行前会自动将其编译成类文件。在编译阶段,字符编码问题仍有可能存在。为了运行下面这个简单示例,请下载本文的源代码。
  
  程序清单1 EncodingTest.java
  1   import java.io.ByteArrayOutputStream;
  2   import java.io.OutputStreamWriter;
  34   public class EncodingTest {
  5public static void main(String[] args) {
  6OutputStreamWriter out = new OutputStreamWriter(new ByteArrayOutputStream());
  7   System.out.println("Current Encoding: "+out.getEncoding());
  8  System.out.println("Literal output: ??o?£?");
  // You may not see this Chinese String
  9}
  10   }
  
  有关这段源代码的说明如下:
  ·?  我们用下面的代码确定系统当前的编码方式:
  6  OutputStreamWriter out = new OutputStreamWriter(new ByterrayOutputStream());
  7  System.out.println("Current Encoding: "+out.getEncoding());
  ·第8行包含直接打印输出中文文字字符串(由于操作系统语言设置的原因可能造成该字符串不能正常显示)的代码。
  ·