标签:
转载请声明: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()); }
<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); } }
这时候抱着迷惑的心情打开了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中文,因为在第一步时,中文二字被黑洞现象吞掉了。
标签:
原文地址:http://blog.csdn.net/softmanfly/article/details/43611985