`

JSP/SERVLET中文编码问题的研究总结

阅读更多

在进行网页编程中我们经常会遇到一些乱码问题,诸如GET/POST提交的数据在服务器端处理时出现乱码,浏览器显示服务器端响应出现乱码等,ajax交互过程中出现乱码,而更糟糕的是每次遇到问题我们都要到网上去搜索一堆的解决方案,但是这些方案大都都捉襟见肘,只能解决部分问题。

分析主要原因,是我们对编码基本知识,java语言对编码处理,浏览器/服务器端在请求响应的过程中对编码的处理,URL的编码规则,还有ajax应用编码规则了解不够透彻等导致的。

本文将从以上这几个原因出发,分析乱码出现的原因和解决方案。

 

一、编码基础知识

1.1            字符、字符集和编码

字符:是文字与符号的总称,包括文字、图形符号、数学符号等。

字符集:就是一组抽象字符的集合。字符集常常和一种具体的语言文字对应起来,该文字中的所有字符或者大部分常用字符就构成了该文字的字符集,比如英文字符集。一组有共同特征的字符也可以组成字符集,比如繁体汉字字符集、日文汉字字符集。字符集的子集也是字符集。

    字符和字符集之间的关系可以用下图表示:

1.2            常用字符集

    ASCII

       American Standard Code for Information Interchange,美国信息交换标准码。

       目前计算机中用得最广泛的字符集及其编码,由美国国家标准局(ANSI)制定。它已被国际标准化组织(ISO)定为国际标准,称为ISO 646标准。 ASCII字符集由控制字符和图形字符组成。在计算机的存储单元中,一个ASCII码值占一个字节(8个二进制位),其最高位(b7)用作奇偶校验位。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b71。偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b71

 

    ISO 8859-1:

ISO 8859,全称ISO/IEC 8859,是国际标准化组织(ISO)及国际电工委员会(IEC)联合制定的一系列8位字符集的标准,现时定义了15个字符集。

ASCII收录了空格及94个“可印刷字符”,足以给英语使用。但是,其他使用拉丁字母的语言(主要是欧洲国家的语言),都有一定数量的变音字母,故可以使用ASCII及控制字符以外的区域来储存及表示。除了使用拉丁字母的语言外,使用西里尔字母的东欧语言、希腊语、泰语、现代阿拉伯语、希伯来语等,都可以使用这个形式来储存及表示。

       * ISO 8859-1 (Latin-1) - 西欧语言

       * ISO 8859-2 (Latin-2) - 中欧语言

       * ISO 8859-3 (Latin-3) - 南欧语言。世界语也可用此字符集显示。

       * ISO 8859-4 (Latin-4) - 北欧语言

       * ISO 8859-5 (Cyrillic) - 斯拉夫语言

       * ISO 8859-6 (Arabic) - 阿拉伯语

       * ISO 8859-7 (Greek) - 希腊语

       * ISO 8859-8 (Hebrew) - 希伯来语(视觉顺序)

       * ISO 8859-8-I - 希伯来语(逻辑顺序)

       * ISO 8859-9 (Latin-5  Turkish) - 它把Latin-1的冰岛语字母换走,加入土耳其语字母。

       * ISO 8859-10 (Latin-6  Nordic) - 北日耳曼语支,用来代替Latin-4

       * ISO 8859-11 (Thai) - 泰语,从泰国的 TIS620 标准字集演化而来。

       * ISO 8859-13 (Latin-7  Baltic Rim) - 波罗的语族

       * ISO 8859-14 (Latin-8  Celtic) - 凯尔特语族

       * ISO 8859-15 (Latin-9) - 西欧语言,加入Latin-1欠缺的法语及芬兰语重音字母,以及欧元符号。

       * ISO 8859-16 (Latin-10) - 东南欧语言。主要供罗马尼亚语使用,并加入欧元符号。

很明显,iso8859-1编码表示的字符范围很窄,无法表示中文字符。但是,由于是单字节编码,和计算机最基础的表示单位一致,所以很多时候,仍旧使用iso8859-1编码来表示。而且在很多协议上,默认使用该编码。

    Unicode:

Unicode(统一码、万国码、单一码)是一种在计算机上使用的字符编码。 它是http://www.unicode.org制定的编码机制, 要将全世界常用文字都函括进去。 它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。 1990年开始研发,1994年正式公布。随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。 但自从unicode2.0开始,unicode采用了与ISO 10646-1相同的字库和字码,ISO也承诺ISO10646将不会给超出0x10FFFFUCS-4编码赋值,使得两者保持一致。 Unicode的编码方式与ISO 10646的通用字符集(Universal Character SetUCS)概念相对应,目前的用于实用的Unicode版本对应于UCS-2,使用16位的编码空间。 也就是每个字符占用2个字节,基本满足各种语言的使用。实际上目前版本的Unicode尚未填充满这16位编码,保留了大量空间作为特殊使用或将来扩展。

UTF:

Unicode 的实现方式不同于编码方式。

一个字符的Unicode编码是确定的,但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。

Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,简称为 UTF)

     * UTF-8: 8bit变长编码,对于大多数常用字符集(ASCII0~127字符)它只使用单字节,而对其它常用字符(特别是朝鲜和汉语会意文字),它使用3字节。

        * UTF-16: 16bit编码,是变长码,大致相当于20位编码,值在00x10FFFF之间,基本上就是unicode编码的实现,与CPU字序有关。

    汉字编码:
* GB2312
字集是简体字集,全称为GB2312(80)字集,共包括国标简体汉字6763个。
* BIG5
字集是台湾繁体字集,共包括国标繁体汉字13053个。
* GBK
字集是简繁字集,包括了GB字集、BIG5字集和一些符号,共包括21003个字符。
* GB18030
是国家制定的一个强制性大字集标准,全称为GB18030-2000,它的推出使汉字集有了一个大一统的标准。

1.3            为什么会有乱码

     在下面的描述中,将以"中文"两个字为例,经查表可以知道其GB2312编码是"d6d0 cec4"Unicode编码为"4e2d 6587"UTF编码就是"e4b8ad e69687"。注意,这两个字没有iso8859-1编码,但可以用iso8859-1编码来"表示",例如GB2312编码的"中文"可以用iso8859-1表示成:"d6 d0 ce c4"utf-8编码的"中文"可以用iso8859-1表示成:" e4 b8 ad e6 96 87"

     Java中出现乱码主要有两个原因:

l        Unicode-->Byte, 如果目标代码集不存在对应的代码,则得到的结果是0x3f

    如:"/u00d6/u00ec/u00e9/u0046/u00bb/u00f9".getBytes("GBK") 的结果是 "?ìéF?ù", Hex 值是3fa8aca8a6463fa8b4.

仔细看一下上面的结果,你会发现/u00ec被转换为0xa8ac, /u00e9被转换为/xa8a6... 它的实际有效位变长了!这是因为GB2312符号区中的一些符号被映射到一些公共的符号编码,由于这些符号出现在ISO-8859-1或其它一些SBCS字符集中,故它们在 Unicode中编码比较靠前,有一些其有效位只有8位,和汉字的编码重叠(其实这种映射只是编码的映射,在显示时仔细不是一样的。Unicode 中的符号是单字节宽,汉字中的符号是双字节宽) . Unicode/u00a0--/u00ff 之间这样的符号有20个。了解这个特征非常重要!由此就不难理解为什么JAVA编程中,汉字编码的错误结果中常常会出现一些乱码(其实是符号字符), 而不全是'?'字符就比如上面的例子。

l        Byte-->Unicode, 如果Byte标识的字符在源代码集不存在,则得到的结果是0xfffd.

如:

     Byte ba[] = {(byte)0x81,(byte)0x40,(byte)0xb0,(byte)0xa1};

new String(ba,"gb2312");( new String(ba,"gbk");输出为"丂啊")

结果是"?", hex 值是"/ufffd/u554a". 0x8140 GBK字符,按GB2312转换表没有对应的值,取/ufffd. (请注意:在显示该uniCode时,因为没有对应的本地字符,所以也适用上一种情况,显示为一个"?"

 

二、Java语言对编码的处理

java应用软件中,会有多处涉及到字符集编码,有些地方需要进行正确的设置,有些地方需要进行一定程度的处理。

2.1            Byte[] getBytes(String charset)

    这是java字符串处理的一个标准函数,其作用是将字符串所表示的字符按照charset编码,并以字节方式表示。注意字符串在java内存中总是按unicode编码存储的。比如"中文",正常情况下(即没有错误的时候)存储为"4e2d 6587",如果charset"gbk",则被编码为"d6d0 cec4",然后返回字节"d6 d0 ce c4"。如果charset"utf8"则最后是"e4 b8 ad e6 96 87"。如果是"iso8859-1",则由于无法编码,最后返回 "3f 3f"(两个问号)。

2.2            new String(byte[] bytes, String charset)

        这是java字符串处理的另一个标准函数,和上一个函数的作用相反,将字节数组按照charset编码进行组合识别,最后转换为unicode存储。参考上述getBytes的例子,"gbk""utf8"都可以得出正确的结果"4e2d 6587",但iso8859-1最后变成了"003f 003f"(两个问号)。因为utf8可以用来表示/编码所有字符,所以new String( str.getBytes( "utf8" ), "utf8" ) === str,即完全可逆。

 

三、JSP/Servlet 汉字编码问题

网上常出现的 JSP/Servlet encoding 问题一般都表现在 browser 或应用程序端,如:

l        浏览器中看到的 Jsp/Servlet 页面中的汉字怎么都成了 ’?’。

l        浏览器中看到的 Servlet 页面中的汉字怎么都成了乱码。

l        JAVA 应用程序界面中的汉字怎么都成了方块。

l        Jsp/Servlet 页面无法显示 GBK 汉字。

l        JSP 页面中内嵌在<%...%>,<%=...%>Tag包含的 JAVA code 中的中文成了乱码,但页面的其它汉字是对的。

l        Jsp/Servlet 不能正确接收 get/post提交的汉字。

l        Jsp/Servlet 不能正确接收直接URL中的中文参数

l        JSP/Servlet 数据库读写无法获得正确的内容。

隐藏在这些问题后面的是各种错误的字符转换和处理(除第3个外,是因为 Java font 设置错误引起的)。解决类似的字符 encoding 问题,需要了解 Jsp/Servlet 的运行过程,检查可能出现问题的各个点。

111.jpg

(1)    jsp编译成.java

JSP 编译。Java 应用服务器将根据 JVM  file.encoding 值读取 JSP 源文件,编译生成JAVA 源文件,再根据 file.encoding 值写回文件系统。如果当前系统语言支持 GBK,那么这时候不会出现 encoding 问题。如果是英文的系统,如 LANG  en_US  Linux, AIX Solaris,则要将 JVM  file.encoding 值置成 GBK 

这里可以设置<%@ page pageEncoding="GBK"%>中的pageEncoding参数来告诉java应用服务器(不同的服务器实现方式可能有所差异)用什么编码来编译JSP文件

(2)    .java编译成.class文件

      第二阶段是由JAVACJAVA源码至java byteCode的编译,不论JSP编写时候用的是什么编码方案,经过这个阶段的结果全部是UTF-8encodingjava源码.JAVACUTF-8encoding读取java源码,编译成unicode encoding的二进制码(即.class),这是JVM对常数字串在二进制码(java encoding)内表达的规范.

(3)    客户端请求

客户端请求主要分为getpost方式,通过这两种方式请求的参数中含有汉字,servlet将无法正确解析,这是由于SUN J2SDK 中,HttpUtils.parseName 在解析参数时根本没有考虑browser 的语言设置,而是将得到的值按 byte 方式解析。

这是网上讨论得最多的 encoding 问题。Servlet在默认情况下使用ISO-8859-1URL提交的数据和表单中提交的数据进行重新编码的,因此通过request.getParameter(“name”)将得到乱码。

对于表单提交方式:

当通过post提交时候,我们可以使用request.setCharacterEncoding(charset)来对请求数据使用charset重新编码。因此可以用request.getParameter(“name”)直接输出。

但当用GET方式提交时则不能用此方法进行请求的重新编码。因此即使在取参数之前调用了request.setCharacterEncoding(charset),也不能直接使用request.getParameter(“name”)输出正确的中文字符。

这里有一种比较通用的解决方法,即通过new String(request.getParameter(“name”).getBytes(“iso8859-1”),”charset”)根据被提交请求的初始编码方式(charset)来重新编码输出,而初始编码方式通常指的就是请求表单所在页面的编码。

对于URL的提交方式:

请求也可以通过url的方式来进行,url中的中文是由操作系统的默认编码方式进行编码的。如果使用的是中文操作系统,那么url中的中文将以GBK编码。编码后的数据被发送到服务器,tomcat服务器在默认的情况下,把由GBK编码后的url中的中文参数按照iso8859-1重新编码输出。因此直接使用request.getParameter(“name”)获取参数时产生乱码,这个时候可以使用

new String(request.getParameter(“name”).getBytes(“iso8859-1”),”GBK”)来对中文参数进行重新编码,从而获得正确结果。

由于不同操作系统的默认编码不同,因此对url的编码也不相同,因此一般情况下url中尽可能不要使用中文,从而使得程序无法正确解析url中的中文,从而产生乱码。

 

下面以一个例子来讲解从用户请求到最后服务器响应输出的过程中所经历的编码变化。

字符“中文”的gbk编码为d6d0 cec4

User input *(gbk:d6d0 cec4)

browser *(gbk:d6d0 cec4)

web server iso8859-1(00d6 00d 000ce 00c4) class

因此需要在class中进行处理:getbytes("iso8859-1")d6 d0 ce c4,最后再new String("gbk")d6d0 cec4

而内存中的unicode编码则为4e2d 6587

 

用户输入的编码方式和页面指定的编码有关,也和用户的操作系统有关,所以是不确定的,上例以gbk为例。

 

browserweb server,可以在表单中指定提交内容时使用的字符集,否则会使用页面指定的编码。而如果在url中直接用?的方式输入参数,则其编码往往是操作系统本身的编码,因为这时和页面无关。上述仍旧以gbk编码为例。

 

l Web server接收到的是字节流,默认时(getParameter)会以iso8859-1编码处理之,结果是不正确的,所以需要进行处理。但如果预先设置了编码(通过request. setCharacterEncoding ()),则能够直接获取到正确的结果。

 

在页面中指定编码是个好习惯,否则可能失去控制,无法指定正确的编码。

(4)    服务器端响应

      Servlet 需要将 HTML 页面内容转换为 browser 可接受的 encoding 内容发送出去。依赖于各 JAVA App Server 的实现方式,有的将查询 Browser  accept-charset  accept-language 参数或以其它猜的方式确定 encoding 值,有的则不管。因此采用固定encoding也许是最好的解决方法。对于中文网页,可在 JSP  Servlet 中设置contentType="text/html; charset=GB2312";如果页面中有GBK字符,则设置为contentType="text/html; charset=GBK",由于IE  NetscapeGBK的支持程度不一样,作这种设置时需要测试一下。因为16 JAVA char在网络传送时高8位会被丢弃,也为了确保Servlet页面中的汉字(包括内嵌的和servlet运行过程中得到的)是期望的内码,可以用 PrintWriter out=res.getWriter() 取代 ServletOutputStream out=res.getOutputStream(). PrinterWriter 将根据contentType中指定的charset作转换(ContentType需在此之前指定!); 也可以用OutputStreamWriter封装ServletOutputStream 类并用write(String)输出汉字字符串。对于 JSPJAVA Application Server 应当能够确保在这个阶段将嵌入的汉字正确传送出去。

      对于tomcat服务器,可以通过

response.setHeader()

response.setCharacterEncoding()

response.setContentType()

<%@page contentType="text/html; chareset=gbk"%>

<meta http-equiv="content-type"

content="text/html; charset=gb2312" />

      按照从上往下的优先级来对输出进行重新编码,告诉浏览器以什么编码显示。

四、Ajax的编码问题

Ajax分为Get方式和Post方式两种

1)请求参数编码问题

在使用post方式提交时,ajax默认是以utf-8对被提交参数进行编码的,因此在服务器端只需要直接使用request.getParameter()输出即可。

在使用get方式提交时,ajax是按照页面的编码方式对被提交参数进行编码的,因此在输出时,要按照页面编码进行编码转换,即

new String(request.getParameter(“name”).getBytes(“iso8859-1”),”charset”)

2)响应参数问题

AJAX的响应是用UTF-8进行编码的,因此在响应输出时候必须指定为UTF-8编码,否则将出现乱码。

在解决ajax乱码问题时候,推荐前后台都使用utf-8编码,这样就不会有乱码问题了。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics