您的位置:首页 > 编程语言 > Java开发

深入 Java Web 开发中的乱码问题

2015-08-22 18:17 495 查看
不久前被 WebSphere 的乱码问题折磨的头痛欲裂,之后就对 Java Web 开发过程中的乱码问题做了细致的研究和学习。

在学习和讨论的过程中,我发现有很多名词都被误用了或者说被不严谨的理解了,所以在开始前,我需要规定一下我对以下名词的一些理解(不是为了给这些词下一个合适的定义,只是为了能够让读者和写者在这片文章中有相同的理解而避免意思模糊)。

编码:是信息从一种形式或格式转换为另一种形式的过程。

解码:编码的逆转换过程。

字符编码:将字符转换成二进制数据的过程称为字符编码。

URL编码:将非 ASCII 字符和在URL中有特殊含义的字符转换成ASCII字符字符的过程。

那么现在开始正题。

乱码的成因,不用说大家都知道是解码和编码时使用字符集不一致造成的(更广义的说是:编码和解码时使用的标准不同)。可关键问题是在一个HTTP请求过程中将会涉及到多次编码和解码的过程,想要彻底明白乱码的成因,需要对整个过程进行详细的分析。

我们知道,在计算机的世界中只有 0和1,是不存在那么多丰富多彩的字符的,计算机之所以能够呈现出那么多的字符,完全得益于 字符编码 和 字符解码 技术。在字符编码解码技术中的核心说白了就是一个码表,这个码表规定了一组二进制数到一个字符的映射,当然这个映射是一对一的,因此它也是可逆的。例如在 ASCII 码表中有’35’ 到 ‘#’ 这么一个映射关系。那么当你向计算机中输入一个 ‘# ’ 时,计算机就会在内存中记录上 ‘00100101’这么串数据(在字符编码解码过程中最小单位是8个二进制位),这个过程就叫做字符编码。当浏览器要显示这串二进制数据时,需要通过映射的逆运算再反向找到 ‘#’ 这个字符并显示在屏幕上,这个过程称之为字符解码。

从这里可以看出,当我们提交一个表单时,我们只是提交了一串二进制数据到了服务器。那么是不是当我们提交表单时和服务器将这串二进制数解析成字符串时使用相同的字符集(码表)就能够避免乱码了?

显然不是,因为 http 协议对于数据传输有着特殊的规定:报文头只能用ASCII编码(get方法的请求参数在报文头中),application/x-www-form-urlencoded 消息类型的报文体也需要使用ASCII编码(POST请求默认就是这种消息类型)。

现在就有矛盾了,因为我们都是中国人使用的是汉字而ASCII码完全不能满足我们的正常需要!假设当我们浏览一个网页时使用了 UTF8 字符集,刚好网页上有个表单输入框,于是我们填写了’大哥’这个词,在浏览器进行编码时通过utf8字符集编码出了一串二进制数,我们不妨叫它 btu8。在 ASCII 中最大的编码是 ‘01111111’,不用想,对 btu8 这串二进制数据进行8个一位的分组后,绝对会有比 ‘01111111’ 大的数出现!这个时候就发现我们填写的东西已经不能传输了,于是就有了 URL编码和URL解码的出现。

URL编码可以将那些超过了ASCII码表最大值的八位二进制数转换成多个在ASCII范围内的八位二进制数。如:某utf8编码值为 ‘10010021’通过转化后就是 ‘00100101 00000000 00001001’将它用ASCII进行字符解码会得到 ‘%09’,我相信大家对这个形式的字符串肯定都非常熟悉。

好了,现在让我们看看我们填写的 ‘大哥’ 都经历的哪些转换。



现在我们知道了,其实我们提交的数据在浏览器就已经涉及到了两次编码,一次是字符编码,一次是 URL编码。那么在服务器端我们就需要对报文中的被URL编码部分进行 URL解码 以获得字符串原始的二进制流,并将该二进制流解码成字符串。

在Java中有 URLDecoder.decode(String uri, String charset) 方法进行一次性执行这两个动作。其中 uri 是通过URL编码后再用ASCII解码出的字符串,本例中就是 “%E5%A4%A7%E5%93%A5”,而charset就是对二进制流解码用的字符集,本例中是 “utf8”。

但是在 Java Web 服务器端编程过程中,我们自己没有机会执行这个解码的方法,因为这方法将会被Web服务器(例如 Tomcat)先执行了,并且将解码后的字符串放在了 Request 对象中。由于是服务器执行这段代码,因此我们只能通过配置的方式对 charset 这个参数进行设置(不同的Web服务器有不同的设置,详细请查看服务器的相关配置文档)。本例中,我们的Web服务器应该设置为’utf8’,那么我们就可以通过request.getParameter 方法很愉快的拿到没有乱码的字符串对象。

但是如果我们服务器设置的编码和浏览器的编码不一致怎么办?比如浏览器是 utf8 的但是 Tomcat 是 ISO-8859-1 的。这个时候网上有很多帖子给出了完美解决方案:

//如果是 Get 请求
String name = request.getParameter("name");
name = new String(name.getBytes("ISO-8859-1") , "utf8");

//如果是 Post 请求
request.setCharacterEncoding("utf8");
String name = request.getParameter("name");


诚然这个方法可以解决大部分问题,但是如果服务器配置是 utf8 而客户端是 GBK 的时候,上面的Get请求就将会失效!

下面是一段我用来模拟这两个过程的代码:

//客户端为 GBK 经过 URL编码后的字符串
String uri = "%B4%F3%B8%E7";

//服务器设置位 utf8
String ui = URLDecoder.decode(uri , "utf8");
System.out.println(new String(ui.getBytes("utf8") , "GBK"));

//服务器设置 ISO-8859-1
String ii = URLDecoder.decode(uri , "ISO-8859-1");
System.out.println(new String(ii.getBytes("ISO-8859-1") , "GBK"));

//输出结果:
//锟斤拷
//大哥


是不是很奇怪?详细原因我还没有探究出来,但是通过实验我发现,通过URLDecoder.decode(uri , “utf8”) 代码解码时,原本的二进制数据流被改动了,而下面一段没有,因此通过 getBytes 方法获取到的二进制流已经不是客户端的那个了,出现了意料之外的结果。那为什么Post方法确可以正常执行呢?因为对于Post方法Web服务器不会帮我们先执行URL解码方法,而是在我们调用了setCharacterEncoding之后再执行,执行时的字符集将会使用我们指定的那个,因此可以正常工作(当然这种Post方法能够正常工作还是要取决于所选用的Web服务器的)。

所以,为了避免这样的尴尬,建议Web服务器的编码方式设置成’ISO-8859-1’ 或者直接和浏览器统一设置成一样的。我个人认为还是设置成“ISO-8859-1”比较好,假如你的应用中出现了如下情况:你的应用是统一成了UTF8的编码,但是有一个第三方的应用它希望能够通过将表单直接提交到你的服务器上,但是他们使用的页面编码却是GBK的!在这个情形下,你们的应用基本是无法实现对接了。(回到开头,我就是被这样的情形给搞头大了的)

最后我通过以下方式成功解决了这个问题:

客户端代码:

var name = "大哥";//浏览器为 GBK 的
var uName = encodeURIComponent(name);
$.get("/test.do" , {uName} , function(data){});


服务器端代码:

String uName = request.getParameter("name");
String name = URLDecoder.decode(uName , "GBK");


这个方案是利用了,不管任何字符集它们在 ASCII 码段都是兼容的!

而URL解码编码都是通过我自己手动控制的,就可以屏蔽掉了Web服务器的瞎捣乱。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  乱码 java web