Java 中文编码问题深度解析

更新:11-03 民间故事 我要投稿 纠错 投诉

几种常见的编码格式

为什么要编码
不知道大家有没有想过一个问题,那就是为什么要编码?我们可以不编码吗?要回答这个问题,我们必须回到计算机如何表示我们人类可以理解的符号。这些符号也是我们人类使用的语言。由于人类语言太多,表示这些语言的符号也太多,无法用计算机中——字节的基本存储单元来表示。因此,必须对其进行拆分或翻译,计算机才能理解它。我们可以假设计算机可以理解的语言是英语。如果计算机中可以使用其他语言,则必须将其翻译成英语。这个翻译过程就是编码。所以可以想象,只要你不在英语国家,想要能够使用计算机就必须经过编码。这似乎有些霸道,但这就是目前的情况。这也和我们国家现在大力推广汉语是一样的。我们希望其他国家都说中文,将来其他语言也能翻译成中文。我们可以改变计算机中存储的最小信息单位。成中文字符,所以我们不存在编码问题。 所以总体来说,编码的原因可以总结为: 计算机存储信息的最小单位是字节,即8位,因此可以表示的字符范围是0~255 人类想要表示的符号太多了,不可能用一个字节来完整地表示。 为了解决这个矛盾,必须需要一个新的数据结构char,并且必须从char到byte进行编码。
如何“翻译”
我明白各种语言需要沟通,需要翻译,那么如何翻译呢?计算中提出了多种翻译方法,常见的有ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16等,它们都可以看作是字典,它们规定了转换规则。根据这些规则,计算机就可以正确地表示我们的字符。目前编码格式有很多种,例如GB2312、GBK、UTF-8、UTF-16等。这些格式都可以表示一个汉字。那么我们应该选择哪种编码格式来存储汉字呢?这就需要考虑其他因素,是存储空间更重要还是编码效率更重要。编码格式的正确选择是基于这些因素。下面简单介绍一下这些编码格式。 学过计算机的人都知道ASCII 码。 ASCII码一共有128个,用一个字节的低7位来表示。 0~31为换行、回车、删除等控制字符; 32~126为打印字符,可以通过键盘输入。输入并能够显示出来。 ISO-8859-1128个字符显然是不够的,因此ISO组织在ASCII码的基础上制定了一系列标准来扩展ASCII码。它们是ISO-8859-1~ISO-8859-15,其中ISO-8859-1涵盖了大部分西欧语言字符,也是使用最广泛的。 ISO-8859-1仍然是单字节编码,总共可以表示256个字符。 GB2312的全称是《信息交换用汉字编码字符集 基本集》。它是双字节编码。总的编码范围是A1-F7。 A1-A9为符号区,共包含682个符号。 B0-F7为汉字区,共6763个汉字。 GBK的全称是《汉字内码扩展规范》。它是国家技术监督局为Windows 95制定的新的汉字内码规范。它似乎是对GB2312的扩展,增加了更多的汉字。其编码范围为8140~FEFE(除去XX7F),共有23940个码点,可表示21003个汉字。它的编码兼容GB2312,也就是说用GB2312编码的汉字可以用GBK解码,不会出现乱码。 GB18030的全称是《信息交换用汉字编码字符集》,是我国的强制性标准。它可以是单字节、双字节或四字节编码。其编码兼容GB2312编码。虽然这是国家标准,但在实际应用系统中使用。不广泛。 UTF-16 说到UTF,就必须提到Unicode(通用代码)。 ISO正在尝试创建一个新的超语言词典,通过该词典世界上所有语言都可以相互翻译。你可以想象这本词典有多么复杂。 Unicode的详细规范请参考相应的文档。 Unicode 是Java 和XML 的基础。下面详细介绍Unicode在计算机中的存储形式。 UTF-16 专门定义了计算机中如何访问Unicode 字符。 UTF-16使用两个字节来表示Unicode转换格式。这是一种固定长度的表示方法。无论什么字符,都可以用两个字节来表示。两个字节就是16位,所以称为UTF-16。 UTF-16 非常方便表示字符。每两个字节代表一个字符。这大大简化了字符串操作时的操作。这也是Java使用UTF-16作为内存中字符存储格式的一个很重要的原因。 UTF-8UTF-16统一使用两个字节来表示一个字符。虽然它的表示非常简单方便,但它也有其缺点。一个字节可以表示大量的字符。现在它们需要用两个字节来表示。存储空间增加了一倍。在网络带宽还非常有限的今天,这会增加网络传输的流量,而且是没有必要的。 UTF-8采用变长技术,每个编码区都有不同的字符长度。不同类型的字符可由1~6个字节组成。 UTF-8有以下编码规则: 如果一个字节的最高位(位8)为0,则表示它是一个ASCII字符(00 - 7F)。可以看到所有的ASCII编码都已经是UTF-8了。 如果一个字节以11开头,则连续1的个数表示该字符的字节数,例如:110xxxxx

将其表示为双字节UTF-8 字符的第一个字节。

如果一个字节以10开头,则说明它不是第一个字节,需要向前寻找才能得到当前字符的第一个字节。

返回顶部

Java 中需要编码的场景

上面已经介绍了几种常见的编码格式。接下来我们将介绍Java中如何支持编码以及什么场合需要编码。

I/O 操作中存在的编码

我们知道编码涉及到字符到字节或者字节到字符的转换,而需要这种转换的场景主要是在I/O过程中,其中包括磁盘I/O。 O 和网络I/O。网络I/O部分稍后介绍,主要以Web应用为例。下图是Java中处理I/O问题的接口:

图xxx。需要一个headingReader类是Java I/O中读取字符的父类,InputStream类是读取字节的父类。 InputStreamReader 类是将字节与字符关联起来的桥梁。它负责在I/O过程中读取数据。处理读取的字节到字符的转换,字节到字符的具体解码是由StreamDecoder实现的。 Charset编码格式必须由用户在StreamDecoder解码过程中指定。值得注意的是,如果不指定Charset,则会使用本地环境中的默认字符集。例如,在中文环境下,将使用GBK编码。

写作情况也类似。字符的父类是Writer,字节的父类是OutputStream。通过OutputStreamWriter将字符转换为字节。如下图:

图xxx。需要标题类似地,StreamEncoder 类负责将字符编码为字节。编码格式和默认编码规则与解码一致。

例如,下面的代码实现了文件读写功能:

清单1. I/O 涉及的编码示例

字符串文件="c:/stream.txt";字符串字符集="UTF-8"; //将写入的字符转换为字节流FileOutputStream outputStream=new FileOutputStream(file); OutputStreamWriter writer=new OutputStreamWriter(outputStream, 字符集); try { writer.write("这是要保存的汉字"); } 最后{ writer.close(); } //读取字节并将其转换为字符FileInputStream inputStream=new FileInputStream(file); InputStreamReader reader=new InputStreamReader(inputStream, charset); StringBuffer 缓冲区=new StringBuffer(); char[] buf=新的char[64];整数计数=0;尝试{ while ((count=reader.read(buf)) !=-1) { buffer.append(buffer, 0, count); } } 最后{ reader.close(); }

当我们应用程序中进行I/O操作时,只要注意指定统一的codec Charset字符集,一般就不会出现乱码问题。如果有些应用程序不注意指定字符编码,那么在中文环境下就会使用操作系统的默认编码。如果编码和解码都是在中文环境下,通常是没有问题的,但是仍然强烈不建议使用操作系统的默认编码,因为这样的话,你的应用程序的编码格式就和你的应用程序绑定了。运行环境。在跨环境下可能会出现乱码问题。

内存操作中的编码

在Java开发中,除了涉及编码的I/O之外,最常用的方法就是在内存中将字符转换为字节数据类型。 String在Java中用来表示字符串,因此String类提供了到字节的转换。还支持将字节转换为字符串的方法、构造函数。下面的代码示例:

String s="这是一个中文字符串"; byte[] b=s.getBytes("UTF-8");字符串n=new String(b,"UTF-8");

另一种是已经废弃的ByteToCharConverter和CharToByteConverter类,它们分别提供了convertAll方法来实现byte[]和char[]的相互转换。如下代码所示:

ByteToCharConverter charConverter=ByteToCharConverter.getConverter("UTF-8"); char c[]=charConverter.convertAll(byteArray); CharToByteConverter byteConverter=CharToByteConverter.getConverter("UTF-8"); byte[] b=byteConverter.convertAll(c );

这两个类已被Charset 类取代。 Charset提供了对应于char[]到byte[]的编码和byte[]到char[]的解码的encode和decode。如下代码所示:

字符集charset=Charset.forName("UTF-8"); ByteBuffer byteBuffer=charset.encode(string); CharBuffer charBuffer=charset.decode(byteBuffer);

编码和解码在一个类中完成。通过forName设置编码和解码字符集,这样更容易统一编码格式,比ByteToCharConverter和CharToByteConverter类更方便。

Java中还有一个ByteBuffer类,它提供了char和byte之间的软转换。它们之间的转换不需要编码和解码。它只是将16 位字符格式拆分为两个8 位字节表示形式。实际值没有被修改,只是数据类型被转换了。下面的代码是这样的:

ByteBuffer heapByteBuffer=ByteBuffer.allocate(1024); ByteBuffer byteBuffer=heapByteBuffer.putChar(c);

上面提供了字符和字节之间的相互转换。只要我们统一设置编码和解码格式,一般不会有问题。

返回顶部

Java 中如何编解码

前面介绍过几种常见的编码格式。这里我们将通过实际例子来介绍如何用Java实现编码和解码。接下来我们以字符串“我是君山”为例,介绍如何在Java中将其转换为ISO-8859。 -1。采用GB2312、GBK、UTF-16、UTF-8编码格式进行编码。

清单2. 字符串编码

String name="我是君山";

toHex(name.toCharArray());

尝试{ byte[] iso8859=name.getBytes("ISO-8859-1");

toHex(iso8859); byte[] gb2312=name.getBytes("GB2312"); toHex(gb2312); byte[] gbk=name.getBytes("GBK");

toHex(GBK);

byte[] utf16=name.getBytes("UTF-16");

toHex(utf16);

byte[] utf8=name.getBytes("UTF-8");

toHex(utf8);

}

捕获(UnsupportedEncodingException e)

{ e.printStackTrace(); } }```

我们按照上面提到的编码格式将名称字符串编码成字节数组,然后以十六进制输出。我们先来看看Java是如何编码的。

以下是Java编码所需的类图

图1. Java 编码类图

![图1.Java编码类图](http://upload-images.jianshu.io/upload_images/1353074-54e5a4dd71448213.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)首先根据指定传入Charset charsetName .forName(charsetName) 设置Charset 类,然后根据Charset 创建CharsetEncoder 对象,然后调用CharsetEncoder.encode 对字符串进行编码。不同的编码类型会对应一个类,实际的编码过程是在这些类中完成的。下面是String.getBytes(charsetName)编码过程的时序图

图2.Java编码时序图

!【图2.Java编码时序图】(http://upload-images.jianshu.io/upload_images/1353074-6d3ab72f384f076d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 从上面可以看出图中,根据charsetName找到Charset类,然后根据这个字符集编码生成CharsetEncoder。该类是所有字符编码的父类。它定义了如何在其子类中针对不同的字符编码集实现编码。一旦有了CharsetEncoder对象,就可以调用encode方法来实现编码。这就是String.getBytes编码方法,其他方法如StreamEncoder也类似。我们看看不同的字符集是如何将前面的字符串编码成字节数组的?

例如字符串“我是君山”的char数组为49 20 61 6d 20 541b 5c71。接下来根据不同的编码格式转换成相应的字节。

根据ISO-8859-1 编码

字符串“我是Junshan”采用ISO-8859-1编码。编码结果如下:

![图xxx。需要一个标题](http://upload-images.jianshu.io/upload_images/1353074-7a0132326aaeac15.gif?imageMogr2/auto-orient/strip) 从上图中我们可以看到7个char字符是通过ISO-8859-1编码转换的成7字节数组,ISO-8859-1是单字节编码,中文“君山”转成字节,值为3f。 3f 是“?”字符,所以汉字经常变成“?”,这可能是由于ISO-8859-1 编码使用不正确造成的。汉字经过ISO-8859-1编码后会丢失信息。我们通常称其为“黑洞”,它会吸收未知的字符。由于大多数基础Java框架或系统的默认字符集编码都是ISO-8859-1,因此很容易出现乱码。后面我们会分析不同形式的乱码是如何出现的。

按GB2312编码

字符串“我是君山”采用GB2312编码。编码结果如下:

![图xxx。需要一个标题](http://upload-images.jianshu.io/upload_images/1353074-bcda8d9da8857731.gif?imageMogr2/auto-orient/strip) GB2312对应的Charset是sun.nio.cs.ext。 EUC_CN 对应的CharsetDecoder 编码类是sun.nio.cs.ext。双字节。 GB2312字符集有一个从char到byte的码表。不同的字符编码都是查这个码表找到每个字符对应的字节,然后组装成字节数组。查表规则如下:

c2b[c2bIndex[字符8] + (字符0xff)]

如果找到的码位值大于oxff,则为双字节,否则为单字节。双字节的高8位作为第一个字节,低8位作为第二个字节,如下代码所示:

if (bb0xff) { //DoubleByte if (dl - dp 2) return CoderResult.OVERFLOW; da[dp++]=(字节) (bb 8); da[dp++]=(字节) bb; } else { //单字节if (dl - dp 1) return CoderResult.OVERFLOW; da[dp++]=(字节) bb; }

从上图可以看出,前5个字符编码后仍然是5个字节,而汉字则编码成双字节。正如第一节介绍的,GB2312只支持6763个汉字,所以并不是所有的汉字都可以使用。 GB2312编码。

按照GBK编码

字符串“我是君山”采用GBK编码。编码结果如下:

![图xxx。需要一个标题](http://upload-images.jianshu.io/upload_images/1353074-c96197f80c8a4f0e.gif?imageMogr2/auto-orient/strip) 你可能发现上图的结果和GB2312编码的结果是一样的,没有错误GBK和GB2312的编码结果是一样的。由此可见,GBK编码与GB2312编码是兼容的,而且它们的编码算法也是相同的。不同的是它们的码表长度不同,GBK包含的汉字较多。因此,只要GB2312编码的汉字能够被GBK解码,反之则不然。

编码为UTF-16

字符串“我是君山”采用UTF-16编码。编码结果如下:

![图xxx。需要一个标题](http://upload-images.jianshu.io/upload_images/1353074-abcaac4c2357ba8a.gif?imageMogr2/auto-orient/strip) 使用UTF-16编码将char数组加倍,单字节对于范围内的字符,0为加上高位就变成两个字节,汉字也变成两个字节。从UTF-16编码规则来看,只是将字符的高位和位置分割成两个字节。它的特点是编码效率非常高,规则非常简单。由于不同的处理器对2个字节的处理方式不同,采用Big-endian(高位字节在前,低位字节在后)或Little-endian(低位字节在前,高位字节在后)编码,因此在对字符串进行编码时,需要指定是Big-endian还是Little-endian,所以前面有两个字节来保存BYTE_ORDER_MARK值,而UTF-16使用固定长度的UCS-2的16位(2-byte)表示或Unicode转换格式,通过代理对访问BMP以外的字符编码。

编码为UTF-8

字符串“我是君山”采用UTF-8编码。编码结果如下:

![图xxx。需要一个标题](http://upload-images.jianshu.io/upload_images/1353074-e9b58f5eb5a11183.gif?imageMogr2/auto-orient/strip)UTF-16虽然编码效率很高,但是不适合单字节范围。字符大小也加倍,无形中浪费了存储空间。另外,UTF-16采用顺序编码,无法验证单个字符的编码值。如果中间一个字符的代码值被损坏,则后续的所有代码值都将丢失。做作的。 UTF-8 不存在这些问题。 UTF-8仍然使用一个字节来表示单字节范围内的字符,并使用三个字节来表示汉字。其编码规则如下:

清单3.UTF-8编码代码片段

private CoderResultencodeArrayLoop(CharBuffer src, ByteBuffer dst){ char[] sa=src.array(); int sp=src.arrayOffset() + src.position(); int sl=src.arrayOffset() + src.limit( ); byte[] da=dst.array(); int dp=dst.arrayOffset() + dst.position(); int dl=dst.arrayOffset() + dst.limit(); int dlASCII=dp + 数学。分钟(sl - sp,dl - dp); //仅ASCII 循环while (dp dlASCII sa[sp] "u0080") da[dp++]=(byte) sa[sp++]; while (sp sl) { char c=sa[sp]; if (c0x80) { //最多7 位if (dp=dl) return Overflow(src, sp, dst, dp); da[dp++]=(字节)c; } else if (c0x800) { //2 个字节,11 位if (dl - dp 2) return Overflow(src, sp, dst, dp); da[dp++]=(字节)(0xc0 | (c 6)); da[ dp++]=(字节)(0x80 | (c0x3f)); } else if (Character.isSurrogate(c)) { //有一个代理对if (sgp==null) sgp=new Surrogate.Parser(); int uc=sgp.parse(c, sa, sp, sl); if (uc 0) { updatePositions(src, sp, dst, dp);返回sgp.error(); } if (dl - dp 4) ret

urn overflow(src, sp, dst, dp); da[dp++] = (byte)(0xf0 | ((uc >>18))); da[dp++] = (byte)(0x80 | ((uc >>12) & 0x3f)); da[dp++] = (byte)(0x80 | ((uc >>6) & 0x3f)); da[dp++] = (byte)(0x80 | (uc & 0x3f)); sp++; // 2 chars } else { // 3 bytes, 16 bits if (dl - dp< 3) return overflow(src, sp, dst, dp); da[dp++] = (byte)(0xe0 | ((c >>12))); da[dp++] = (byte)(0x80 | ((c >>6) & 0x3f)); da[dp++] = (byte)(0x80 | (c & 0x3f)); } sp++; } updatePositions(src, sp, dst, dp); return CoderResult.UNDERFLOW; } UTF-8 编码与 GBK 和 GB2312 不同,不用查码表,所以在编码效率上 UTF-8 的效率会更好,所以在存储中文字符时 UTF-8 编码比较理想。 几种编码格式的比较 对中文字符后面四种编码格式都能处理,GB2312 与 GBK 编码规则类似,但是 GBK 范围更大,它能处理所有汉字字符,所以 GB2312 与 GBK 比较应该选择 GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来说 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,想比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以 UTF-8 在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。 [回页首](http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/#ibm-pcon) Java Web 涉及到的编码 对于使用中文来说,有 I/O 的地方就会涉及到编码,前面已经提到了 I/O 操作会引起编码,而大部分 I/O 引起的乱码都是网络 I/O,因为现在几乎所有的应用程序都涉及到网络操作,而数据经过网络传输都是以字节为单位的,所以所有的数据都必须能够被序列化为字节。在 Java 中数据被序列化必须继承 Serializable 接口。 这里有一个问题,你是否认真考虑过一段文本它的实际大小应该怎么计算,我曾经碰到过一个问题:就是要想办法压缩 Cookie 大小,减少网络传输量,当时有选择不同的压缩算法,发现压缩后字符数是减少了,但是并没有减少字节数。所谓的压缩只是将多个单字节字符通过编码转变成一个多字节字符。减少的是 String.length(),而并没有减少最终的字节数。例如将“ab”两个字符通过某种编码转变成一个奇怪的字符,虽然字符数从两个变成一个,但是如果采用 UTF-8 编码这个奇怪的字符最后经过编码可能又会变成三个或更多的字节。同样的道理比如整型数字 1234567 如果当成字符来存储,采用 UTF-8 来编码占用 7 个 byte,采用 UTF-16 编码将会占用 14 个 byte,但是把它当成 int 型数字来存储只需要 4 个 byte 来存储。所以看一段文本的大小,看字符本身的长度是没有意义的,即使是一样的字符采用不同的编码最终存储的大小也会不同,所以从字符到字节一定要看编码类型。 另外一个问题,你是否考虑过,当我们在电脑中某个文本编辑器里输入某个汉字时,它到底是怎么表示的?我们知道,计算机里所有的信息都是以 01 表示的,那么一个汉字,它到底是多少个 0 和 1 呢?我们能够看到的汉字都是以字符形式出现的,例如在 Java 中“淘宝”两个字符,它在计算机中的数值 10 进制是 28120 和 23453,16 进制是 6bd8 和 5d9d,也就是这两个字符是由这两个数字唯一表示的。Java 中一个 char 是 16 个 bit 相当于两个字节,所以两个汉字用 char 表示在内存中占用相当于四个字节的空间。 这两个问题搞清楚后,我们看一下 Java Web 中那些地方可能会存在编码转换? 用户从浏览器端发起一个 HTTP 请求,需要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据都可能存在编码问题,当 Servlet 处理完所有请求的数据后,需要将这些数据再编码通过 Socket 发送到用户请求的浏览器里,再经过浏览器解码成为文本。这些过程如下图所示: 图 3. 一次 HTTP 请求的编码示例([查看大图](http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/image021-lg.png)) ![图 3. 一次 HTTP 请求的编码示例](http://upload-images.jianshu.io/upload_images/1353074-5cc89d7aabacf5e5.gif?imageMogr2/auto-orient/strip)如上图所示一次 HTTP 请求设计到很多地方需要编解码,它们编解码的规则是什么?下面将会重点阐述一下: URL 的编解码 用户提交一个 URL,这个 URL 中可能存在中文,因此需要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL: 图 4.URL 的几个组成部分 ![图 4.URL 的几个组成部分](http://upload-images.jianshu.io/upload_images/1353074-ba5727338a198b9c.gif?imageMogr2/auto-orient/strip)上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中: Port 对应在 Tomcat 的中配置,而 Context Path 在中配置,Servlet Path 在 Web 应用的 web.xml 中的junshanExample/servlets/servlet/*中配置,PathInfo 是我们请求的具体的 Servlet,QueryString 是要传递的参数,注意这里是在浏览器里直接输入 URL 所以是通过 Get 方法请求的,如果是 POST 方法请求的话,QueryString 将通过表单方式提交到服务器端,这个将在后面再介绍。 上图中 PathInfo 和 QueryString 出现了中文,当我们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的我们选择 FireFox 浏览器并通过 HTTPFox 插件观察我们请求的 URL 的实际的内容,以下是 URL:HTTP://localhost:8080/examples/servlets/servlet/ 君山 ?author= 君山在中文 FireFox3.6.12 的测试结果 图 5. HTTPFox 的测试结果 ![图 5. HTTPFox 的测试结果](http://upload-images.jianshu.io/upload_images/1353074-ec1ec9a1afcb20ad.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码,至于为什么会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”,所以最终的 URL 就成了上图的格式了。 默认情况下中文 IE 最终的编码结果也是一样的,不过 IE 浏览器可以修改 URL 的编码格式在选项 ->高级 ->国际里面的发送 UTF-8 URL 选项可以取消。 从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对服务器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。 解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的: protected void convertURI(MessageBytes uri, Request request) throws Exception { ByteChunk bc = uri.getByteChunk(); int length = bc.getLength(); CharChunk cc = uri.getCharChunk(); cc.allocate(length, -1); String enc = connector.getURIEncoding(); if (enc != null) { B2CConverter conv = request.getURIConverter(); try { if (conv == null) { conv = new B2CConverter(enc); request.setURIConverter(conv); } } catch (IOException e) {...} if (conv != null) { try { conv.convert(bc, cc, cc.getBuffer().length - cc.getEnd()); uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength()); return; } catch (IOException e) {...} } } // Default encoding: fast conversion byte[] bbuf = bc.getBuffer(); char[] cbuf = cc.getBuffer(); int start = bc.getStart(); for (int i = 0; i< length; i++) { cbuf[i] = (char) (bbuf[i + start] & 0xff); } uri.setChars(cbuf, 0, length); } 从上面的代码中可以知道对 URL 的 URI 部分进行解码的字符集是在 connector 的中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。 QueryString 又如何解析? GET 方式 HTTP 请求的 QueryString 与 POST 方式 HTTP 请求的表单参数都是作为 Parameters 保存,都是通过 request.getParameter 获取参数值。对它们的解码是在 request.getParameter 方法第一次被调用时进行的。request.getParameter 方法被调用时将会调用 org.apache.catalina.connector.Request 的 parseParameters 方法。这个方法将会对 GET 和 POST 方式传递的参数进行解码,但是它们的解码字符集有可能不一样。POST 表单的解码将在后面介绍,QueryString 的解码字符集是在哪定义的呢?它本身是通过 HTTP 的 Header 传到服务端的,并且也在 URL 中,是否和 URI 的解码字符集一样呢?从前面浏览器对 PathInfo 和 QueryString 的编码采取不同的编码格式不同可以猜测到解码字符集肯定也不会是一致的。的确是这样 QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。 从上面的 URL 编码和解码过程来看,比较复杂,而且编码和解码并不是我们在应用程序中能完全控制的,所以在我们的应用程序中应该尽量避免在 URL 中使用非 ASCII 字符,不然很可能会碰到乱码问题,当然在我们的服务器端最好设置中的 URIEncoding 和 useBodyEncodingForURI 两个参数。 HTTP Header 的编解码 当客户端发起一个 HTTP 请求除了上面的 URL 外还可能会在 Header 中传递其它参数如 Cookie、redirectPath 等,这些用户设置的值很可能也会存在编码问题,Tomcat 对它们又是怎么解码的呢? 对 Header 中的项进行解码也是在调用 request.getHeader 是进行的,如果请求的 Header 项没有解码则调用 MessageBytes 的 toString 方法,这个方法将从 byte 到 char 的转化使用的默认编码也是 ISO-8859-1,而我们也不能设置 Header 的其它解码格式,所以如果你设置 Header 中有非 ASCII 字符解码肯定会有乱码。 我们在添加 Header 时也是同样的道理,不要在 Header 中传递非 ASCII 字符,如果一定要传递的话,我们可以先将这些字符用 org.apache.catalina.util.URLEncoder 编码然后再添加到 Header 中,这样在浏览器到服务器的传递过程中就不会丢失信息了,如果我们要访问这些项时再按照相应的字符集解码就好了。 POST 表单的编解码 在前面提到了 POST 表单提交的参数的解码是在第一次调用 request.getParameter 发生的,POST 表单参数传递方式与 QueryString 不同,它是通过 HTTP 的 BODY 传递到服务端的。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到服务器端,在服务器端同样也是用 ContentType 中字符集进行解码。所以通过 POST 表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。 另外针对 multipart/form-data 类型的参数,也就是上传的文件编码同样也是使用 ContentType 定义的字符集编码,值得注意的地方是上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及到字符编码,而真正编码是在将文件内容添加到 parameters 中,如果用这个编码不能编码时将会用默认编码 ISO-8859-1 来编码。 HTTP BODY 的编解码 当用户请求的资源已经成功获取后,这些内容将通过 Response 返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,并且通过 Header 的 Content-Type 返回客户端,浏览器接受到返回的 socket 流时将通过 Content-Type 的 charset 来解码,如果返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的中的 charset 来解码。如果也没有定义的话,那么浏览器将使用默认的编码来解码。 其它需要编码的地方 除了 URL 和参数编码问题外,在服务端还有很多地方可能存在编码,如可能需要读取 xml、velocity 模版引擎、JSP 或者从数据库读取数据等。 xml 文件可以通过设置头来制定编码格式Velocity 模版设置编码格式: services.VelocityService.input.encoding=UTF-8 JSP 设置编码格式:<%@page contentType="text/html; charset=UTF-8"%>访问数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如 MySQL:url="jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK"。 [回页首](http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/#ibm-pcon) 常见问题分析 在了解了 Java Web 中可能需要编码的地方后,下面看一下,当我们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题唯一的原因都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于往往一次操作涉及到多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题,下面就几种常见的现象进行分析。 中文变成了看不懂的字符 例如,字符串“淘!我喜欢!”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程如下图所示 ![Figure xxx. Requires a heading](http://upload-images.jianshu.io/upload_images/1353074-094a1b82207deb56.gif?imageMogr2/auto-orient/strip)字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符。 一个汉字变成一个问号 例如,字符串“淘!我喜欢!”变成了“??????”编码过程如下图所示 ![Figure xxx. Requires a heading](http://upload-images.jianshu.io/upload_images/1353074-c99544f8b243936b.gif?imageMogr2/auto-orient/strip)将中文和中文符号经过不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是因为用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是通常所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。 一个汉字变成两个问号 例如,字符串“淘!我喜欢!”变成了“????????????”编码过程如下图所示 ![Figure xxx. Requires a heading](http://upload-images.jianshu.io/upload_images/1353074-fe773d27b9119422.gif?imageMogr2/auto-orient/strip)这种情况比较复杂,中文经过多次编码,但是其中有一次编码或者解码不对仍然会出现中文字符变成“?”现象,出现这种情况要仔细查看中间的编码环节,找出出现编码错误的地方。 一种不正常的正确编码 还有一种情况是在我们通过 request.getParameter 获取参数值时,当我们直接调用 String value = request.getParameter(name); 会出现乱码,但是如果用下面的方式 String value = String(request.getParameter(name).getBytes(" ISO-8859-1"), "GBK"); 解析时取得的 value 会是正确的汉字字符,这种情况是怎么造成的呢? 看下如所示: ![Figure xxx. Requires a heading](http://upload-images.jianshu.io/upload_images/1353074-8d1cfc3d23d9a990.gif?imageMogr2/auto-orient/strip)这种情况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码可以保持编码数值“不变”。虽然中文字符在经过网络传输时,被错误地“拆”成了两个欧洲字符,但由于输出时也是用 ISO-8859-1,结果被“拆”开的中文字的两半又被合并在一起,从而又刚好组成了一个正确的汉字。虽然最终能取得正确的汉字,但是还是不建议用这种不正常的方式取得参数值,因为这中间增加了一次额外的编码与解码,这种情况出现乱码时因为 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而造成第一次解析式用 ISO-8859-1 来解析才造成乱码的。 [回页首](http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/#ibm-pcon) 总结 本文首先总结了几种常见编码格式的区别,然后介绍了支持中文的几种编码格式,并比较了它们的使用场景。接着介绍了 Java 那些地方会涉及到编码问题,已经 Java 中如何对编码的支持。并以网络 I/O 为例重点介绍了 HTTP 请求中的存在编码的地方,以及 Tomcat 对 HTTP 协议的解析,最后分析了我们平常遇到的乱码问题出现的原因。

用户评论

岁岁年年

Java 对中文的支持一直是我比较关注的点。

    有19位网友表示赞同!

落花忆梦

这篇博客讲的很有道理,之前处理中文确实遇到过不少麻烦。

    有6位网友表示赞同!

暮染轻纱

了解不同的编码方式对中文解析的影响非常重要啊!

    有18位网友表示赞同!

夜晟洛

原来是这样啊,以后做中文相关的项目要特别注意这个问题。

    有15位网友表示赞同!

久爱不厌

这个链接挺好用的,收藏了!

    有9位网友表示赞同!

青山暮雪

看标题感觉很有料,期待深入学习Java中文编码的知识点。

    有12位网友表示赞同!

念安я

我的项目里也用到了Java处理中文,这篇文章正好可以借鉴一下经验。

    有18位网友表示赞同!

柠夏初开

中文编码问题确实是个难题,希望能学习到一些解决方法。

    有16位网友表示赞同!

若他只爱我。

了解不同平台下对中文编码的支持也很重要啊,这个博客也许有相关的介绍。

    有5位网友表示赞同!

命该如此

最近在学习Java开发,遇到中文编码的问题可以参考一下这篇博文。

    有15位网友表示赞同!

◆残留德花瓣

我对Java的编码机制一直不太了解,以后可以好好研究一下。

    有5位网友表示赞同!

心安i

原来编码之间存在差异导致中文显示不正确,得仔细理解各个编码的特性了!

    有9位网友表示赞同!

旧事酒浓

希望能从这篇文章学到一些处理中文编码问题的技巧吧!

    有13位网友表示赞同!

屌国女农

Java 中文处理确实是个需要花时间去学习的知识点。

    有5位网友表示赞同!

凉凉凉”凉但是人心

以前遇到过类似的问题,现在看来是编码问题啊!

    有5位网友表示赞同!

枫无痕

期待这篇博文能详细讲解Java中文编码的相关问题和解决方案。

    有5位网友表示赞同!

别悲哀

对于学 Java 的同学们来说,了解中文编码问题很有必要!

    有13位网友表示赞同!

陌颜幽梦

学习编程的同时也需要了解相关的编码知识,这篇文章应该很专业。

    有12位网友表示赞同!

烟花巷陌

这篇文章肯定会帮助我更好地理解Java中的中文编码问题!

    有7位网友表示赞同!

为爱放弃

希望这篇文章能让我对 Java 编解码的原理有更深的理解。

    有13位网友表示赞同!

【Java 中文编码问题深度解析】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活

上一篇:《地面师》日剧高清1080P/4K中文字幕全集8集在线免费观看下载【百度/夸克网盘】 下一篇:高效精力管理:揭秘提升生活质量的秘诀