乱码引发的编码思考

转载请声明:http://blog.csdn.net/softmanfly/article/details/43611985

乱码是软件开发中的常见问题,程序员如果对码不清楚的话经常会被各种码搞得晕头转向,我在开发一个JavaWeb项目时也遇到了一些乱码的问题,百思不得其解,最后通过阅读源码和一定的猜测,对编码和乱码问题有了一定的心得体会,故记录下来(如果只想深入了解Java中的编码相关内容的话可以直接看红字下面的部分):

问题来由:在http get方法中url后面添加query string,使用中文作为参数,提交到服务器导致乱码,比如一个请求:

http://localhost:8080/register?userName=小波波,最后到达服务器时调用request.getParameter("userName")就变成了乱码。

问题分析:

网上查阅了一大堆的方法,有设置charsetEncoding的,有设置URIEncoding的,有new String(params.getBytes("ISO-8859-1"), "utf-8")转化一下的;

然后我就开始各种尝试,发现有时候能扭转乱码,有时候变成其他的乱码,虽然能够解决一时的问题,但是还是不明白本质的原因,所以我决定先从源码入手,看看getParameter函数是如何取得我们需要的参数的值得,以下是getParameter函数的源码:

public String getParameter(String name) {

        parseParameters();

        Object value = parameters.get(name);
        if (value == null)
            return (null);
        else if (value instanceof String[])
            return (((String[]) value)[0]);
        else if (value instanceof String)
            return ((String) value);
        else
            return (value.toString());

    }

发现是在parseParamters里面进行的处理:

<span style="font-size:18px;">if (parsedParams) {
   return;//如果已经解码过,就直接返回
}
parameters = new HashMap<>();
parameters = copyMap(getRequest().getParameterMap());
mergeParameters();</span><pre name="code" class="java"><span style="font-family: Arial, Helvetica, sans-serif;"><span style="font-size:14px;">parsedParams = true;</span></span>

发现应该是继续在mergeParamters里进行处理:

private void mergeParameters() {

        if ((queryParamString == null) || (queryParamString.length() < 1))
            return;

        HashMap<String, String[]> queryParameters = new HashMap<>();
        String encoding = getCharacterEncoding();
        if (encoding == null)
            encoding = "ISO-8859-1";
        RequestUtil.parseParameters(queryParameters, queryParamString,
                encoding);//问题出在这
        Iterator<String> keys = parameters.keySet().iterator();
        while (keys.hasNext()) {
            String key = keys.next();
            Object value = queryParameters.get(key);
            if (value == null) {
                queryParameters.put(key, parameters.get(key));
                continue;
            }
            queryParameters.put
                (key, mergeValues(value, parameters.get(key)));
        }
        parameters = queryParameters;

    }

大致理解以下这个函数,应该是在将通过get方法中?后面的query string携带的参数和通过addParameter方法添加的参数进行合并(merge),所以乱码问题应该来自对queryParameters的处理,也就是RequestUtil.parseParameters函数

打开这个函数:

 public static void parseParameters(Map<String,String[]> map, String data,
            String encoding) {

        if ((data != null) && (data.length() > 0)) {

            // use the specified encoding to extract bytes out of the
            // given string so that the encoding is not lost.
            byte[] bytes = null;
            try {
                bytes = data.getBytes(B2CConverter.getCharset(encoding));
                parseParameters(map, bytes, encoding);
            } catch (UnsupportedEncodingException uee) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("requestUtil.parseParameters.uee",
                            encoding), uee);
                }
            }

        }

    }

发现首先将data转化为对应编码的bytes数组(data就是query string也就是?后面的userName=小波波),然后再调用

 parseParameters(map, bytes, encoding);

对byte数组进行处理,那么再次进入这个parseParameters函数:

public static void parseParameters(Map<String,String[]> map, byte[] data,
            String encoding) throws UnsupportedEncodingException {

        Charset charset = B2CConverter.getCharset(encoding);

        if (data != null && data.length > 0) {
            int    ix = 0;
            int    ox = 0;
            String key = null;
            String value = null;
            while (ix < data.length) {
                byte c = data[ix++];
                switch ((char) c) {
                case '&':
                    value = new String(data, 0, ox, charset);
                    if (key != null) {
                        putMapEntry(map, key, value);
                        key = null;
                    }
                    ox = 0;
                    break;
                case '=':
                    if (key == null) {
                        key = new String(data, 0, ox, charset);
                        ox = 0;
                    } else {
                        data[ox++] = c;
                    }
                    break;
                case '+':
                    data[ox++] = (byte)' ';
                    break;
                case '%':
                    data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4)
                                    + convertHexDigit(data[ix++]));
                    break;
                default:
                    data[ox++] = c;
                }
            }
            //The last value does not end in '&'.  So save it now.
            if (key != null) {
                value = new String(data, 0, ox, charset);
                putMapEntry(map, key, value);
            }
        }

大家可以清楚的看到这里就是在进行实际的解析,分别得到参数的key(userName)和value(小波波),然后将其放入到一个Map<Key,Value>中,我们注意到这一行:

 value = new String(data, 0, ox, charset);

这里就是真正将data数组中关于小波波的部分按照charset转化成String value,所以乱码问题的出现应该是这个charset的问题!从上面的源码中我们可以知道charset值是通过这样来设置的:

String encoding = getCharacterEncoding();
        if (encoding == null)
            encoding = "ISO-8859-1";

那么把charset设置成什么编码才不会导致乱码呢?要想搞清楚这个问题还真不容易,你必须得对编码有一个比较清晰的了解,以下内容才是本文的精华,能够让你对Java中的编码有一个很好的认识,以及为什么上面源码中会多次出现ISO8859-1这个编码种类,它有什么特点能够让他在众多的编码方式中脱颖而出成为Java源码中多次用来当做默认的编码:

首先我们来看看Java中的String类,String类理解了,玩转Java中的编码就不是难事了。

String类中有2个比较常用的操作:

一个是str.getBytes(String charsetName)

一个是new String(s.getBytes("GBK"), "UTF-8");

搜索了很多网上的资料,关于这两个操作的深入分析的文章还是很少的,反正我是没找到,所以还是得靠源码来说话,一开始我想既然String和byte数组联系如此紧密,那么String里面一定有2个成员变量把,一个是byte[] bytes负责存放0和1序列,一个是 Charset charset代表bytes的编码种类,假如一个String s="哈";那么在这个String里应该存放着

byte [] bytes = 11011011101...(这里纯属假设,目的是把原理阐述清楚就行) 然后还有Charset charset = Charset.getFromName("UTF-8");这就告诉系统这个String里的byte应该对应UTF-8的编码表进行解码,当系统要显示这个字给用户时,系统就会去UTF-8对应的编码表查找这个特定顺序的01序列,然后找到了“哈”这个字并将它显示出来,一开始认为这么想挺合理的,根据Charset种类的不同,解读bytes的方式也不同,所以当charset不对时,自然就解读不出正确的bytes所表达的内容,造成乱码。
而当调用s.getBytes("GBK")时,这个函数就会把bytes从UTF-8编码像GBK编码转化(我的想象是有一种编码转化的参照表)这样我们就得到了一个GBK编码方式的bytes数组,此时假如我们执行一个这样的操作:s =new String(s.getBytes("GBK"), "GBK"),然后再打印s会同样得到一个“哈”字,只不过此哈非彼哈,这是一个GBK编码的“哈”,他的bytes数组存放的01顺序是按照GBK编码的方式来的,结果我写了一个这样的demo:

package test;

import java.io.UnsupportedEncodingException;

public class TestString {

	public static void main(String[] args) throws UnsupportedEncodingException {
		String s = new String("哈".getBytes("UTF-8"), "GBK");
		System.out.println(s);
	}
}

期盼他输出哈字,一运行发现乱码了,原本以为我成功的将UTF-8编码的“哈”成功转化成了GBK编码的哈,然后输出也会得到一个GBK编码的哈字呢!可是居然得到的是这货:鍝?

这时候抱着迷惑的心情打开了String的源码,才彻底的拨云见雾,看到庐山真面了,我首先打开了

new String("哈".getBytes("UTF-8"), "GBK");

看看这个构造函数到底在做些什么事情:

public String(byte bytes[], int offset, int length, String charsetName)
	throws UnsupportedEncodingException
    {
	if (charsetName == null)
	    throw new NullPointerException("charsetName");
	checkBounds(bytes, offset, length);
	char[] v = StringCoding.decode(charsetName, bytes, offset, length);
	this.offset = 0;
	this.count = v.length;
	this.value = v;
    }

这才发现String里的根本没有什么byte数组,而是有一个char数组,那么这个char数组是通过

StringCoding.decode(charsetName, bytes, offset, length);

得来的,于是再打开decode函数:

static char[] decode(String charsetName, byte[] ba, int off, int len)
	throws UnsupportedEncodingException
    {
	StringDecoder sd = (StringDecoder)deref(decoder);
	String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
	if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
			      || csn.equals(sd.charsetName()))) {
	    sd = null;
	    try {
		Charset cs = lookupCharset(csn);
		if (cs != null)
		    sd = new StringDecoder(cs, csn);
	    } catch (IllegalCharsetNameException x) {}
            if (sd == null)
                throw new UnsupportedEncodingException(csn);
	    set(decoder, sd);
	}
	return sd.decode(ba, off, len);
    }

这里有2点值得关注:

1:sd = new StringDecoder(cs, csn);
2:<span style="font-family: Arial, Helvetica, sans-serif;">sd.decode(ba, off, len);</span>

打开StringDecoder的构造函数:

private StringDecoder(Charset cs, String rcn) {
            this.requestedCharsetName = rcn;
	    this.cs = cs;
	    this.cd = cs.newDecoder()
		.onMalformedInput(CodingErrorAction.REPLACE)
		.onUnmappableCharacter(CodingErrorAction.REPLACE);
	}

发现StringDecoder的一个参数cd(CharsetDecoder)是根据cs(Charset)的不同而new出来的Decoder种类也不同,调查后发现这里相当于是一个接口,然后又不同的CharsetDecoder的具体实现,比如UTF8Decoder,GBKDecoder等,所以sd.decode方法应该是具有多态效应的,也就是说要根据不同种类的Decoder实现不同的解码效果,打开decode函数看源码:

byte[] encode(char[] ca, int off, int len) {
<span style="white-space:pre">	</span>int en = scale(len, ce.maxBytesPerChar());
<span style="white-space:pre">	</span>byte[] ba = new byte[en];
 <span style="white-space:pre">	</span>if (len == 0)
<span style="white-space:pre">	</span>return ba;

<span style="white-space:pre">	</span>ce.reset();
<span style="white-space:pre">	</span>ByteBuffer bb = ByteBuffer.wrap(ba);
<span style="white-space:pre">	</span>CharBuffer cb = CharBuffer.wrap(ca, off, len);
<span style="white-space:pre">	</span>try {
<span style="white-space:pre">		</span>CoderResult cr = ce.encode(cb, bb, true);
<span style="white-space:pre">		</span>//...
    <span style="white-space:pre">	</span>}
   static byte[] encode(String charsetName, char[] ca, int off, int len) throws UnsupportedEncodingException
    {
<span style="white-space:pre">	</span>StringEncoder se = (StringEncoder)deref(encoder);
<span style="white-space:pre">	</span>String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
 <span style="white-space:pre">	</span>if ((se == null) || !(csn.equals(se.requestedCharsetName())
 <span style="white-space:pre">	</span>|| csn.equals(se.charsetName()))) {
<span style="white-space:pre">		</span>se = null;
<span style="white-space:pre">		</span>try {
<span style="white-space:pre">			</span>Charset cs = lookupCharset(csn);
<span style="white-space:pre">			</span>if (cs != null)
<span style="white-space:pre">			</span>se = new StringEncoder(cs, csn);
<span style="white-space:pre">		</span> } catch (IllegalCharsetNameException x) {}
<span style="white-space:pre">	</span>if (se == null)
                throw new UnsupportedEncodingException (csn);
<span style="white-space:pre">	</span>set(encoder, se);
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>return se.encode(ca, off, len);
    }

可以看到在上面根据cs(Charset)获得了相应的cd(CharsetDecoder)然后调用相应cd的decode函数把一个bb(ByteBuffer)解码为一个cb(CharBuffer)(这里一定要搞清楚byte和char的区别:byte无编码的说法,char有编码);那么我们就会想,为什么要把一个byte数组转成一个char数组呢,这要从java中的char类型说起,java中的char占用2个字节,遵循的是Unicode编码规范(注意规范2个字,这并不是一种具体的编码方式,而是一套规范,告诉你什么样的01序列对应什么符号)那么通过上面的源码解读,我不禁猜想java中的String中的char数组其实存放的是依照Unicode编码规范的01序列,然后系统是根据Unicode中01序列和具体符号的对应关系去显示String中的符号序列的。

那么我们在构造String的时候,其实就是在把按照其他编码方式编码的字符转化成Unicode编码方式的字符,然后用一个Char数组存放在String中,也就是说其他编码方式都可以通过一定的计算手段转化为Unicode编码的01序列,网上一搜资料,果不其然,UTF-8转Unicode只要简单的进行线性替换(不懂的自行搜索学习),而GBK转Unicode也只要进行一定的加减运算就可以得到,所以Unicode可以转化成各种其他的编码,同时各种其他编码也可以向Unicode转化,这样一来就实现了一个统一,这也是Java采用Unicode的原因吧,比如我们在执行如下操作时:

String s = new String(“哈”);其实是在将本机默认编码方式下的“哈”字(假如此时本机的本地默认编码是GBK),从GBK编码通过一定的计算转化成Unicode下“哈”对应的那2个字节,然后放到一个Char数组中,Java对String进行显示时是不用关心你原来的编码方式,因为他们都被统一成了Unicode规范,这样Java就能够根据Char数组去显示“哈”字了,又比如如下操作:String s = new String("哈".getBytes("GBK"),
"UTF-8")产生乱码的原因在于,首先我们获得了GBK编码下的“哈”字的byte数组,里面存放的是GBK编码下“哈”字对应的01序列,然后我们又根据“UTF-8”转化到Unicode的规则对一个本应该依照GBK到Unicode转化规则的byte数组进行了编码转化,使得获得的Char数组失去了本来的意思,而变成了乱码,比如哈字在GBK下编码是110,本来按照GBK到Unicode的转化规则,会转化成011,然后存放到Char数组中,而011在Unicode中正好对应哈这个字,然后此时却按照UTF-8到Unicode的转化规则,错误的转化成了101,这个时候就产生了乱码,更可怕的是有可能110这个序列根本不符合UTF-8的编码规则,这时候就会转化成?号这一类的符号,造成一种黑洞现象,编码被吞了。

所以String中其实存放的实质是:由在其他编码方式下的byte数组按照一定的转化规则转化成Unicode规范后的char数组,而String本身其实不具有Charset属性,也就是说“这个String是什么方式编码”这种说法是错误的,而应该说byte数组遵照什么样的编码方式。

回到最初提到的问题:

为什么Java对ISO8859-1这个编码方式如此情有独钟,动不动就拿来当默认的编解码方式。查询一下ISO8859-1编码相关知识不难知道,这个编码是一个单字节的编码,而且编码范围正好是从0x00-0xFF,涵盖了8个位的所有排列组合的情形,没有一个多余的位(与之相反的一个列子是UTF-8编码,其中有一些0和1的排列组合情况是没有对应任何符号的,比如当UTF-8用2个字节表示一个字符时,开头必须是110,而111这种情况就没有被纳入编码表中),ISO8859-1的一个好处就是,他比ASCII覆盖的更全面,00-FF全部都有对应的符号。

举些例子帮助理解:

假设本地默认编码是GBK

byte [] bytes = "哈".getBytes();

那么bytes里的01序列就是GBK编码下对应的哈的编码序列

String s = new String(bytes);//没有指明后一个参数charset代表采用本地默认编码进行byte到char的转化

然后bytes又按照GBK到Unicode的转化规则,转化为Unicode下的哈字的01序列存放到Char数组中。

而如果采用如下任何一种方式都会造成乱码(本机默认编码为GBK):

1:byte [] bytes = “哈”.getBytes("UTF-8"); String s = new String(bytes); //乱码原因是bytes是按照UTF-8编码的01序列,而构造String是遵照的是GBK到Unicode的转化方式。

2:byte[] bytes = "哈".getBytes(); String s = new String(bytes, "UTF-8");//打印s输出的是?乱码原因是bytes是按照GBK编码的01序列,比如是0101,而构造String是遵照的UTF-8到Unicode的变化规则,并且0101这个编码不在UTF-8的编码范围内,所以只能统一把这些无法解码的01序列转化成Unicode的?字符,如果要是哈在GBK编码下不是以0开头 而是以110开头,并且在UTF-8的编码范围之内,那么就可以完成UTF-8像Unicode的转化,只不过byte已经丢失了它原本的符号意义,变成了一堆其他字符。

byte [] bytes = "哈哈".getBytes("UTF-8"); //说明bytes遵照UTF-8编码,01序列的排列方式符号UTF-8编码的要求,如果假设有一个utf8Decoder(byte[] bytes)函数对其进行解码的话,能够得到“哈哈”

String s = new String(bytes, "UTF-8");//将遵照UTF-8编码的bytes数组按照UTF-8到Unicode的转化规则,转化为Unicode的char数组,此时s为“哈哈”;

byte[] bytes = "a中文".getBytes("ISO-8859-1");//由Unicode转ISO,对应下图第一步,由于中文二字在ISO中没有对应编码,所以统一转化成?符号

String s = new String(bytes, "ISO-8859-1");//由ISO转Unicode,遵照的规律就是高位的那一字节补0,低位8字节不变,导致变回来以后就成了a??而不是a中文,因为在第一步时,中文二字被黑洞现象吞掉了。

  1. String-ISO-8859-1〉ByteArray:/u0061/u4E2D/u6587(a中文)-〉0x61 0x3F 0x3F
  2. ByteArray-ISO-8859-1〉String:0x61 0x3F 0x3F-〉/u0061/u003F/u003F(a??)
时间: 2025-01-16 20:05:21

乱码引发的编码思考的相关文章

一个回车符引发的问题思考

在维护和开发通信类软件产品的过程中,经常需要处理一些软件故障问题.在问题刚出现的时候,大家可能显得手足无措,有一种天都要塌下来的感觉.但在问题原因找到之后,大家又会觉得问题原因非常的简单,要是当初开发的时候仔细一点,是不会犯这样的低级错误的.最近,本人就遇到了一个回车符引发的问题. 近日,在开发某软件版本时需要对文件进行操作,在代码中使用了Linux C语言中的opendir函数,但是该函数始终返回NULL,提示文件路径不存在. 本人查了一下该函数的具体情况.opendir函数的原型为: DIR

Android访问服务器(TOMCAT)乱码引发的问题(转载)

Android访问服务器(TOMCAT)乱码引发的问题 1.浏览器往服务器发送的请求主要可分为2种:get.post:delete.head等不赘述. GET方式: 从浏览器上直接敲地址,最大特点就是参数直接跟在地址后面. POST方式:表单提交等. 2.访问过程: 浏览器中输入地址 –> 浏览器对中文进行编码 –>发送到服务器 ->服务器进行解码 如何浏览器编码和服务器解码用的字符集不一致就会发生乱码问题. 3.乱码的解决 默认浏览器使用UTF-8编码(IE默认GBK当然可以通过met

解决Linux文档显示中文乱码问题以及编码转换

解决Linux文档显示中文乱码问题以及编码转换 使vi支持GBK编码 由于Windows下默认编码是GBK,而linux下的默认编码是UTF-8,所以打开会变成乱码.因此,我们只需要改变一下配置文件,使得vi支持GBK 打开~/.vimrc文件 加上 1.let &termencoding=&encoding2.set fileencodings=utf-8,gbk 然后:wq保存并退出 再次打开vi就显示正常了. 将其它的编码的文件转换成utf-8 另外,还可以将其它的编码的文件转换成u

asp.net 解决文本中文乱码 &nbsp; 各类文本编码转UTF-8 文本

/// <summary> /// 解决文本中文乱码   各类文本编码转UTF-8 文本 /// </summary> /// <param name="fullPath">原路径</param> /// <param name="savePath">临时保存路径</param> /// <param name="strnew">新的文件路径</param&

display:inline-block引发的间隙思考

一.导火线 没错,总有一类属性在助你轻松寻得捷径的同时,也可为你增添烦劳,比如本文的主谋display:inline-block.众前端们所诸知,其作用是将对象呈递为内联对象,但是对象的内容作为块对象呈递.旁边的内联对象会被呈递在同一行内,允许空格.然而不幸的是,它并没有得到所有浏览器的支持,比如ie6.7和古老一点的firefox完全无视它,由于firefox的老版本几乎已经从市场中消失,所以名义上firefox是支持display:inline-block的,除此之外,伟大的chrome.O

Linux中文显示乱码问题解决方法(编码查看及转换)

Linux中文显示乱码问题解决方法(编码查看及转换) 1,示例 图中名为一个.sql文件的一段内容,是一个数据库文件.其在windows中打开显示正常,在Linux中,中文部分显示为乱码. 注意:这个与数据库乱码的情况不同,属于文件内容的乱码. 2,分析 Linux系统与windows系统在编码上有显著的差别.Windows中的文件的格式默认是GBK(gb2312),而Linux系统中文件的格式默认是UTF-8.这两个系统就好比是中国和日本.文件就好比是一个人,如果要在另外的国家居住就要办理居住

PHP输出中文乱码解决:编码终结者

学习PHP之初,根本就没有意识到过有编码那回事儿,随着对中文的输出越来越多,遇到输出乱码的情况也就更加普遍了.最早我们怀疑是数据库编码的问题,于是在PHP代码中的解决的办法是:mysql_query("SET NAMES UTF8"); 但这样做了后,网页显示的仍然是乱码,于是有修改网页头部信息:header('Content-type:text/html;charset=UTF8'); 顺便还找到条其他命令,也不知道能起到什么作用,反正遇到乱码的时候就把它加进去,解决问题即可:dec

深入Struts2的过滤器FilterDispatcher--中文乱码及字符编码过滤器

引用 前几天在论坛上看到一篇帖子,是关于Struts2.0中文乱码的,楼主采用的是spring的字符编码过滤器(CharacterEncodingFilter)统一编码为GBK,前台提交表单数据到Action,但是在Action中得到的中文全部是乱码,前台的页面编码都是GBK没有问题.这是为什么呢?下面我们就通过阅读FilterDispatcher和CharacterEncodingFilter这两个过滤器的源代码,了解其实现细节,最终得出为什么中文还是乱码! 测试环境及其前置知识 Struts

解决 iOS NSDictionary 输出中文字符”乱码”(Unicode编码)问题

简单定义一个字典,输出结果: NSDictionary *dic = @{ @"我是中文字符": @"223333", @"aaa": @{ @"aaa": @"啦啦啦" } }; NSLog(@"%@", dic); 将会看到这样的"乱码",这种现象经常在调试服务端返回 JSON 结果的时候遇到: 2015-02-25 19:23:40.346 XXXX[13273