JAVA乱码问题分析参考文章

前言

本文章主要讨论了在Java web 系统中乱码产生的内在原理, 是认识和解决乱码问题的基础. 如果您对乱码问题还没有一个清晰的概念, 请尝试阅读本文. 另外, 本文也讨论了最近流行的Ajax 技术中的乱码问题, 如果您在使用Ajax 技术中遇到了乱码, 本文对您也有一定的参考价值<1>

为什么会出现乱码

我们都知道, 在冯· 诺伊曼(Neumann János)<2> 体系的 计算机中, 任何数据都是以二进制的形式存在的。我们在键盘上输入以及我们在屏幕上看到的中文、日文、英文字符,最终都是内存或者硬盘上的二进制数据。那么,这种二进 制数据和屏幕上的中文、日文、英文字符之间相互的关系就需要通过一种映射来表达,我们把这种关系称之为 字符映射表<3>

乱码产生的原因

1-1 乱码产生的原因

譬如说, 字符集Shift_JIS 规定 这个字保存到存储介质中为“82A4” ;而字符集GBK 规定存储介质中的“82A4” 代表字符 。 因此,我们在日文系统<4> 中把 あいうえお 这个字符串保存在某个文件中,然后这个文件被带到一个中文系统上,读取这个文件后就产生了乱码,如图1-1

在计算机的存储介质<5> 中,保存的皆为二进制的数据。计算机本身并没有一种方法知道当前的数据是日文的 还是中文的<6>

任何一段文本(或者字符串)被保存到存储介质中的时候都需要有一个字符映射表与之相对应。我们在处理文本(或者字符串)的时候需要清楚地知道当前文本(或者字符串)的编码方式<7> 是什么。

javac –encoding …

Java 作为现在最流行的一种开发语言运行在Java 虚拟机上。 运行前,需要把Java 的源代码编译成byte code ,编译后的byte codeJava 虚拟机解释执行。<8>

Java 虚拟机认为运行在其上的byte code 的编码方式是Unicode<9> ,而Java 源代码以本地的(local) 文本文件的形式存在,那么Java 编译器( 通常是javac.exe) 就需要知道当前的Java 源文件对应的字符映射表,然后把其中的字符转化为Unicode 的字符。

非常幸运地是,一般情况下,Java 有一套很好的机制帮助我们完成了后面的种种编码转换工作,而使得编程人员不需要太在意代码的字符集以便把注意力集中在应用程序的逻辑实现上。

假 设我们使用Java 编写一个小程序,在控制台打印 あいうえお 五个字符。( 如图2-1) 由于 任何一段文本(或者字符串)被保存到存储介质中的时候都需要有一个字符映射表与之相对应 ,所以当我们使用文本编辑器(包括Eclipse 等,非 Microsoft Office )把一段文本保存到硬盘上的时候,我们需要指定当前这段文本所对应的编码方式。( 一般地,如果不指定字符映射表,文本编辑器会采用系统默认的 字符映射表保存文本。) 也就是说,我们在日文Windows XP 下编写的Java 源代码会采用MS932<4> 这个字符映射表保存到文件中(如图2-1 处)。

 

 编写、编译并运行Java代码

 

2-1 编写、编译并运行Java 代码

 

因 为Java 虚拟机认为运行在其上的byte code 的编码方式是Unicode (如图2-1 处),所以Java 编译器会把Java 源代码编译成Unicode 形式的byte code 。但是因为计算机本身并没有一种方法知道当前Java 源文件的编码方式,所以,如果不指定编码方式的话,默认地,Java 编译器会采用系统默认的 字符映射表读取Java 源文件。即,如果在Windows XP 日文版下编译此Java 程序,那么就会使用MS932 格式转化其中的字符串,如果在Windows XP 中文版下编译此Java 程序,那么就会使用MS936 格式转化其中的字符串。如果把Windows XP 日文版下编写的Java 源代码拿到Windows XP 中文版下编译自然就会出现错误了。

因此,Java 编译器( 特指javac.exe) 向我们提供了一个-encoding 的参数,我们可以使用-encoding 告诉Java 编译器采用哪种字符映射表来读取Java 源代码。

(如 图2-1 处)那么在控制台上可以正确地显示 あいうえお 又是为什么呢?JavaSystem.out.println 函数,默认地采用当前系统的 默认字符映射表来输出字符串。即,在WindowsXP 日文版下会把Unicode あいうえお 按照MS932 的格式输出出来;在WindowsXP 中文版下会把Unicode あいうえお 按照MS936 的格式输出出来。所以,编译后的Java byte code 可以在任何系统上正确地运行。也就是Java 所谓的“Write Once, Run Anywhere.”

另外,我们因此也知道,如果使用System.out.println 输出的字符串是乱码的话,也并不能说明此字符串是有问题的。

 

web 相关的编码问题

Java web 系统中,我们主要使用HTTP 协议在网络上通讯。 我们把浏览器称为HTTP 客户端,把web 服务器称为HTTP 服务端。两者通过请求(request) 和响应(response) 的方式传递数据。

 

 Web中的编码方式

 

3-1 Web 中的编码方式

 

设 想在Windows XP 日文版上使用IE 浏览器提交 あいうえお 几个字符,然后HTTP 服务端再把这几个字符打印在HTTP 客户端的屏幕上。( 如图3-1) 其中可能在五处发生了字符集的转换,一处是输入的时候,二处是把字符串通过网络提交到服务器的时候,三处是在服务器端处理字符串的时候,四处是把字符串通 过网络返回给客户端的时候,五处是在客户端显示的时候。

因为HTTP 协议是一个文本传输协议,所以通过HTTP 协议在网络上传输的数据一定是有一个对应的字符集的。一般地,这个字符集是ISO-8859-1<11> 。所以在2 处和4 あいうえお 的编码方式是ISO-8859-1 。我们通过实验也可以证明,在1 处和5 处是HTTP 客户端指定的编码方式<28> ,在3 处是服务器转码后的编码方式<29>

由于现有的HTTP 客户端和服务器端已经帮我们很好的封装了HTTP 协议的实现,所以一般我们在做Java Web Programming 程序的时候不考虑在网上传递的数据格式。

 

Java web 系统中指定编码方式

Java web 系统中, 我们遇到的最多的项目就是采用JSPServlet 技术的项目了。那么,在使用JSPServlet 技术的web 系统中,设置字符集的地方可能有五处。 下面我们先来讨论和响应相关的四处。

一、 pageEncoding<12>

我们可以在JSP 页面上加入指令(directives)

<%@page pageEncoding=“Shift_JIS”%>

JSP 在运行前会被JSP 编译器编译成Servlet ,然后服务器加载此Servlet 处理客户端请求。 JSP 中,指令是传递给JSP 编译器的参数,即告诉JSP 编译器如何编译JSP Page 指令中的pageEncoding 属性即告诉JSP 编译器使用的是哪种字符映射表来读取当前JSP 文件的源代码。<30> 如果没有指定pageEncoding 属性,默认地,JSP 编译器采用当前系统的默认字符映射表来读取JSP 页面。

 

 Page指令的pageEncoding属性

 

3-1 Page 指令的pageEncoding 属性

譬如,我们经常遇到的在Windows 下编写的Java web 应用程序发布到Solaris 后,JSP 不能编译,通常是由于没有指定pageEncoding 造成的。

二、 ContentType

另外,我们可以在JSP 页面上加入含有ContentTypepage 指令

<%@ page contentType=“text/html; charset=Shift_JIS”%>

这个指令的效果等同于

response.setContentType(“text/html; charset=Shift_JIS”);

达到的目的有两个。

其一,在响应的HTTP 文本中加入Content-type 报头(header) 。客户端会根据Content-type 来读取网络上传输的数据。<13>

其二,通知web 容器如何把文本( 或者字符串) 转化为网络上传输的二进制数据。<14>

需要注意的是,我们使一个字符串在网络上传输和把一个字符串保存到文件中本质上是相同的,我们都需要一个字符映射表来映射字符和byte 之间的关系。

 

 关于设置ContentType的作用

 

3-2 关于设置ContentType 的作用

 

假 设我们需要把 あいうえお 这个字符串发送给客户端,那么我们可以通过上面两种方式(page 指令和response 对象) 设置ContentType “Shift_JIS” 。 设置后,服务器会认为是 あいうえお 是使用“Shift_JIS” 编码的字符串,并且以此变为比特流发送到客户端。

客户端在接收到HTTP 响应后并不知道服务器端是 あいうえお ,它得到的只是一堆比特数据,那么它会根据HTTP 响应的报头Content-type 中的设置,把这堆比特数据转化为 あいうえお,<15>

一、 Meta Data

最后一个设置字符集的地方就是HTML 页面的meta 标签。 一般地

<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">

这里设置的字符集是告诉浏览器如何显示HTML 页面。<16>

 

 关于meta data的作用

 

3-3 关于meta data 的作用

 

总 结, 对于客户端页面显示乱码,如果服务器端数据正常的话,那么可能是以上四种地方设置有误。如果pageEncoding 设置错误,一般表现为JSP 页面无法 编译,或者编译后JSP 页面中固有的字符串不能正常显示;如果Content Type 设置错误,一般表现为JSP 页面全部或者大部分为乱码,调整浏览器的显示编码格式后,仍然不能解决;如果meta data 设置错误,一般表现为JSP 页面全部为乱码,调整浏览器的显示编码格式后,可以解决。

每种变量如果不设置,则采用缺省值。如果不设置pageEncodingJSP 编译器采用当前系统默认的字符映射表来读取JSP 文件;如果不设置Content-type ,则采用ISO-8859-1<17> 来作为Content-Type

 

提交数据的编码方式

一般地,客户端提交给服务器的数据有两种形式,GETPOST 。 使用GET 方式提交数据的时候,HTTP 消息中没有报体(Message Body) ,提交的数据存在于URL<18> ;使用POST 提交数据的时候, 提交的数据存在于HTTP 消息的报体中。 ( 另外,最近比较流行使用XMLHtttpRequest 对象提交数据,我们将在下一节中讨论。)

无论以哪种方式提交数据,这些数据都要经过编码和URL Encoding<19> 两个过程。

 

 URL Character Encoding

 

4-1 URL Character Encoding<19>

 

在服务端会做一遍上述编码过程的逆过程,从而得到 あいうえお 。 那么,问题就是服务端如何知道客户端传递过来的字符串使用什么编码方式?

首先,假设我们在页面表单里输入了 あいうえお ,那么HTTP 客户端( 浏览器) 会使用什么编码方式对它进行编码?HTTP 客户端( 浏览器) 会使用当前页面的显示字符集 对它进行编码。

显示字符集是指显示某个页面的时候所使用的字符集。既不是meta 中指定的字符集<20> ,也非Content-type 中指定的字符集。

Microsoft Internet Explorer 中,显示字符集可以在下面这里看到。

 

image008

 

4-2 IE 的显示字符集

 

Mozilla Firefox 中,显示字符集可以在下面这里看到。

 

 Firefox的显示字符集

 

4-3 Firefox 的显示字符集

 

客户端会根据当前页面的显示字符集编码当前页面上表单中的数据,并提交到服务端。

或者说,可以通过改变页面的显示字符集来改变提交数据的编码方式。

其次,当数据提交到服务端,服务器端如何知道客户端传递过来的数据是采用什么编码方式?答案是因不同服务器不同而不同。

对 于Tomcat Tomcat 会认为客户端提交的数据全部采用ISO-8859-1 的方式编码,所以Tomcat 会采用ISO-8859-1 的形式解码;而 Weblogic 会采用响应客户端页面的编码方式解码。但是无论哪种服务器采用哪种方式,都不能保证是我们想要的!

那么我们如何来指定我们想要的解码方式呢?

Java web 系统中,我们可以采用request.setEncoding(<encoding_str>) 来指定客户端的编码方式。 这行代码即告诉request 对象,客户端的编码方式是<encoding_str> 。由此,我们就可以保证客户端提交的数据和服务器端接收 的数据采用同样的编码方式。

 

关于Ajax 系统编码方式的讨论

2005 年,Ajax 作为最热门的名词之一使使用Ajax 技术的项目一下多起来。 因为初次使用这种技术,所以其中最大的问题之一就是编码问题。

Ajax 的核心是XMLHttpRequest 对象。总体来说, XMLHttpRequest 对象是一个简单的对象。 我们可以调用它的send 方法向服务器端发送数据,以及可以调用它的responseTextresponseXML 来接收从服务器端返回的数据。

根据W3C 的定义<21> , 我们使用XMLHttpRequest 对象的send 方法发送的数据总是为Unicode(UTF-8) 的编码方式;使用XMLHttpRequest 对象的responseText 接收的数据根据Content-type<22> 中指定的不同而不同。使用XMLHttpRequest 对象的responseXML 接收的数据必须符合XML 规范,且Content-type 中的字符集必须和XML 的字符集相同。譬如指定返回Shift_JIS 编码的XML 数据。

response.setContentType("text/xml;charset=Shift_JIS ");

response.getWriter().write("<?xml version=/"1.0/" encoding=/"Shift_JIS /"?>");

如果没有指定XMLprolog , 则默认编码为UTF-8<23> 。 所以必须指定Content-type 也应该为UTF-8<24>

我 们在提交数据的编码方式中讨论过,使用表单提交数据需要通过两个阶段的编码,一个是使用某种字符集编码,然后再使用URL Character Encoding 编码。这样做是因为在HTTP 协议下网络中传输的是ISO-8859-1 的字符,我们通过这种方式可以把任何字符集转化为符合ISO- 8859-1 字符集的字符串。 但是,我们使用XMLHttpRequest 提交数据的时候,就没有URL Character Encoding 这一步。

 

 没有经过URL Character Encoding的数据

 

5-1 没有经过URL Character Encoding 的数据

 

当然,幸运地是现在的服务器都非常健壮,可以做上述操作的逆操作,这样我们编写的代码还是可以运行的。但是这不是一个好习惯。

一般地,我们使用Ajax 技术的时候,需要手工的进行URL Character Encoding 。 有三个Javascript 函数可以帮助我们做这一点。escape, encodeURIencodeURIComponent<25> 。推荐使用encodeURIComponent<26>

 

小结

Java 系统中,因为底层(byte code)UTF-8 的,所以我们的程序采用什么样的编码方式跟操作系统并没有什么关系。在Java web 系统中,无论客户端的操作系统或者服务器的操作系统,只要能保证数据传输的时候采用统一的编码方式,那么就不会有乱码问题出现。

当在某个特定的系统下发生乱码时,我们需要判断乱码发生的地方。此时,使用一些HTTP 分析工具<27> 是一个好办法。 有时候,我们使用一些服务器或者框架(framework) ,它们为我们做了一些编码转换的工作,这时候问题就变得复杂起来。解决这种问题有两点,其一是本文中提交的基本原理,其二是这种服务器(或者框架)的特性。

有时候我们开发的系统页面非常复杂,在上面使用了框架(frame)Ajax 甚至flash 等技术,这样有可能导致在一个浏览器窗口中存在几种编码方式,这种情况是可能存在的,遇到乱码问题就需要具体问题具体分析了。

 

THE END

 

参考

<1> 对这篇文章感兴趣的同学可以给我写邮件

<2> · 诺伊曼结构也称为普林斯顿结构,参考Wikipedia 中关于冯· 诺伊曼结构 的介绍

<3> 参考INNA 中关于的Character Sets 的定义

<4> 默认在Windows XP 日文版中系统的编码方式为MS932基本 等同于IANAShift_JIS ; 默认在Windows XP 中文版中系统的编码方式为MS936基本 等同于IANAGBK

<5> 这里的存储介质指内存中的变量,磁盘上的文件以及网络上传输的数据等

<6> 其实这种说法并不准确,有些系统有默认的编码方式,譬如XML 默认编码方式是UTF-8 ( 参考W3CXML 1.0 的规范文档Extensible Markup Language (XML) 1.0 (Third Edition) 2.2 Characters) ;有些系统在字符串中加入了编码方式,譬如邮件的标题前面带有“=?GB2312?” 这样制定的字符集(参考RFC2047 2. Syntax of encoded-words

<7> 下文中 编码方式 字符映射表 字符集 均指同一个意思。

<8> 参考Java 虚拟机规范 —— The JavaTM Virtual Machine Specification

<9> 参考Java 虚拟机规范 2.1 Unicode

<10> HTTP 协议的描述在RFC2616

<11> 根据RFC2616 HTTP 协议是基于文本的协议,在HTTP 层传递的是使用ISO-8859-1 编码的文本数据。关于这个问题的具体讨论在我讲的《Java Web Programming 》中第一章第二节。

<12> 本节内容可以参考JavaServer Pages Specification 。 目前最新版的JavaServer Pages Specification 2.1JSR245

<13> HTTP 1.0 规范中, 如何确定网络上传输的数据格式是采用Content-Type 的,但是在HTTP 1.1 规范中采用了Content-Language 这个报头。 另外,需要注意,编码方式(字符集)和报头Content-Encoding 没有关系。 参考RFC2616

<14> 这句话不准确,根据HTTP 协议(RFC2616 )HTTP 协议是基于文本的协议,虽然在TCP/IP 层传递的是比特流,但是在HTTP 层传递的应该是文本数据。所以这句话应该说是在HTTP 层传递的是使用 ISO-8859-1 编码的文本数据。但是这样会导致我们讨论的问题复杂化,所以我们简单认为在网络上传输的是比特流。进一步讨论可以给我写邮件

<15> 在客户端接收到服务器端传递过来的数据的时候,实际上是文本数据,采用ISO-8859-1 编码。 因为HTTP 报头完全符合ISO-8859-1 的编码格式,所以客户端可以正确的读取HTTP 报头中的信息。然后,客户端会把HTTP 报文按照ISO- 8859-1 转化为比特数据,接着把这些比特数据按照HTTP 报头中Content-type 的设置转化为相应的字符。 我们在讨论中为了简单起见省略了这些步骤。

<16> 如果有Content-type 报头,那么浏览器一般会优先采用Content-type 报头中定义的字符集。

<17> 对于Tomcat 服务器

<18> 虽然在RFC 中没有规定URL 的最大长度,但是使用Internet Explorer 支持的URL 最大长度为2083 个字符。 参考 KB208427

<19> 关于URL Character Encoding 可以参考RFC1738 2.2

<20> 但是设置meta data 会影响客户端的字符集设置

<21> 参考W3C 的《The XMLHttpRequest Object

<22> 此处为HTTP 1.0 规范的定义, 在HTP 1.1 规范中, 编码方式定义在Content-Language 中, 参考RFC2616

<23> 参考W3C 的《XML Base

<24> 如果没有指定,默认的是ISO-8859-1

<25> MSDN 中对这三个函数的描述分别如下 escape , encodeURI encodeURIComponent

<26> 在很多目前流行的Ajax 框架中都使用这个函数。而仍然有些旧的系统在使用escape ,我以为这是个误区

<27> 在我讲的《Java Web Programming 》中使用了一种叫Webscrab 的工具,另外我写这篇文章时使用一个叫做Http Analyzer 的工具

<28> 非客户端操作系统默认的编码方式, 提交数据的编码方式 一节中具体讨论

<29> 非服务器端操作系统的默认编码方式, 提交数据的编码方式 一节中具体讨论

<30> 参考我的讲座《Java Web Programming 》中第六章第一节的内容

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值