1 乱码的根源
在计算机中所有的数据最终都必须序列化成字节序列,如果数据恰好表示就是字符,这种序列化的过程称作编码 , 反之当要读取序列化的数据,要先反序列化,对字符数据这种过程称之为解码 。 编码和解码过程有着紧密的关联,都由编码 (前述的’编码’是一个动词,这里是一个名词)决定, 一个编码(如UTF-8,GBK)就决定了一个编码过程和一个解码过程。 只有当编码过程和解码过程使用相同的编码时,字符数据才能被正确解释,否则乱码就产生了。 乱码问题的根源就在于对字符数据的编码(写入)和解码(读取)使用了不同的编码 。
为了便于说明,我们称字符数据第一次被序列化(写入或编码)时使用的编码称为初始编码 , 最后一次被序列化时使用的编码称为当前编码 。 要反序列化(读取或解码)字符数据一般来说要使用和初始编码相同的编码。 以文本文件为例,初始编码为GBK的文本文件一般只能使用GBK来解码才能读到正确的内容, 否则就会得到乱码(不正确或毫无意义的内容)。 但是也有例外,初始编码为GB2312的文本文件能够使用GBK编码来读取,这是因为GBK兼容GB2312, 类似的,初始编码为ISO-8859-1的文本文件可以使用UTF-8编码来读取,反之却不行,初始编码 为UTF-8的文本文件不能使用ISO-8859-1来读取。假设一个初始编码为GB2312的文本文件用UTF-8 编码读取,然后再使用UTF-8编码写入到同一个文件, 下次读取时无论使用GB2312编码还是使用UTF-8编码都无法回复到以前的内容。 使用与初始编码不兼容的编码先读取然后写入一般会导致初始内容遭到永久性破坏从而表现出乱码 。 这里的例外是ISO-8859-1,它是个特殊的编码,恰好有256(一个字节能够容纳的数)个字符。 对任意初始编码的文本文件,使用ISO-8859-1读取它的内容(虽然读取的内容可能表现出乱码), 然后再以ISO-8859-1写入原文件,文件内容保持不变,下次仍能以初始编码来读取文件的正确内容。 这或许是为什么JSP的许多默认编码设置成ISO-8859-1的原因。
这种乱码出现在需要多次序列化和反序列的情况下,而这在Java程序中非常常见。 为了避免乱码需要确保所有的序列化和反序列化过程使用相同的编码 ,任何一个过程 使用了不正确的编码都将导致乱码,这使得乱码问题变得十分复杂,这首先要求 我们必须对程序所有的编码和解码过程都十分清楚。下面我们就要探讨常见Java程序的编码和解码过程。
2 编码和解码过程
2.1 Java独立应用程序
在考虑Java应用程序的编码和解码过程时不仅仅要考虑运行时编码和解码过程,而且要考虑编译时的编码和 解码过程。通常Java独立应用程序的第一个编码过程是将Java程序写入到一个后缀为”.java”的文本文件中, 在编译java程序时,编译器需要知道Java文件的编码,其默认使用系统平台的编码,Windows一般是“GBK” ,Linux一般是“UTF-8”,可以使用-encoding选项来指定java文件的编码。 如果编译器使用的编码和Java文件使用的编码不相同,就会造成乱码问题。 单个人使用单个平台开发时一般不会遇到这种问题,多人开发时必须统一Java源文件的编码, 我一般喜欢使用UTF-8,编译时也要指定编码,即使源文件编码与平台默认编码相同也是如此, 这样才不会依赖某个平台。使用不同的方式来编译Java程序时,它们都有自己的方式来指定 源文件的编码,javac通过”-encoding”选项,ant通过javac任务的encoding属性,maven 通过设置compiler插件的encoding元素值。值得注意的是,当指定了错误的编码时,编译 可能都会通不过。
运行时的编码和解码过程主要是对文本文件的读和写。在Java中通过下面的方式的获得一个Writer(字符写入器):
当不指定文件编码时,默认编码是系统属性file.encoding的值,可以在java启动时通过”-Dfile.encoding=utf-8”来设置默认 编码为utf-8。最好在创建Writer时指定文件编码,这可以避免对平台的依赖。 通过下面的方式获得一个Reader(字符读取器):
同样不指定编码时,默认编码是系统属性file.encoding的值,创建Reader时最好显式指定编码值,这样可以避免对平台的依赖。 对于配置文件(如xml文件)我的建议是统一使用“UTF-8”编码。 一般来说这种乱码问题很容易解决,只需要指定和文件对应的编码就可以了。 一个例外是properties文件的读取,在jdk6之前,没有对属性文件指定编码的API, Properties只有一个load(InputStream)方法,jdk6中增加一个load(Reader)方法, 这样就可以对属性文件指定编码。jdk6之前properties文件的编码必须是ISO-8859-1, 不能表示的字符只能使用Unicode转义符来表示,需要使用native2ascii工具将某种编码 的属性文件转换成属性文件所需的格式。
另一个比较常见的运行时的编码和解码过程是对数据库的读和写。这和文件的读写类似,只不过Java没有提供公开 的API来设置数据库的编码,这一般由驱动器自动检测,在MySQL中可以通过连接参数来设置数据库的编码。
2.2 Java Web应用程序
Java独立应用程序中的编码和解码过程对于Java Web应用程序也是适用的,这里只探讨和Java Web应用程序相关的 编码和解码过程。同对Java源文件的编译一样,我们先要讨论JSP的编译。对JSP的编译一般分为两步,首先要将 JSP文件转换成Java文件,然后再编译这个Java文件得到class文件,因此这有两个编码和解码过程,但是这个过程是 透明的(读取JSP文件和Java文件使用的编码是相同的),我们可以认为JSP编译器直接将JSP文件编译成class文件。 同样我们必须保证JSP编译器读取JSP文件使用的编码是这个JSP文件的初始编码。和Java编译器不同,JSP编译器 使用的编码是在JSP文件中指定的,例如要使JSP编译器使用UTF-8的编码来读取JSP文件,可以使用:
如果不指定页面编码,默认值为ISO-8859-1,由于上面所说的ISO-8859-1编码的特殊性,只要正确设置响应编码, 一般来说不会观察到乱码。但是在某些特殊情况下,它可能导致原本正确的JSP文件无法编译,更重要的是编译成 Java文件可能是乱码,我们可能有时需要查看这些Java文件,这会造成稍许不便,因而最好还是设置正确的JSP页面 编码。
当响应是字符数据时,这里假设是HTML内容,需要使用某种编码将它序列化成字节序列,浏览器 (或者其它的客户端)读取响应时必须使用相同的编码,否则页面可能就会显示乱码, 一般浏览器会根据响应头Content-Type的值自动检测响应的编码。为了使浏览器能够检测到 正确的编码,可以在JSP中通过如下方式设置Content-Type响应头:
上面的代码设置响应头Content-Type的值为“text/html;charset=utf-8”,浏览器会自动使用 UTF-8编码来读取响应内容。如果浏览器自动决定的编码不正确(这是由于没有设置正确的Content-Type响应头), 主流浏览器(如IE和Firefox)都允许手动选择编码。如果在JSP中没有指定ContentType,则会根据 pageEncoding的值来设置响应编码,如果pageEncoding也没有设置,那么响应编码的值就是ISO-8859-1。 另外响应的编码的值可以在运行时调用方法response.setCharacterEncoding(“your_encoding”)来动态改变。
和响应编码对应的是请求编码。请求一般是由浏览器发送的,Firefox和IE中使用的编码是当前页面的编码, Firefox和IE中都可以通过菜单”查看→字符编码”得到当前页面的编码,它通常就是响应编码(如果没有手工设置编码)。 为了读取到正确的内容,服务器端也必须使用同样的编码(通常是响应编码)来读取请求内容。在JSP中无法直接设置请求编码, 但是可以通过request.setCharacterEncoding(“your_encoding”)来设置。不幸的是,在Tomcat中并没有使用请求编码来解码URI, 默认使用ISO-8859-1编码,要修改URI的编码需要在tomcat的配置文件中设置。关于更多的请求编码的内容,可以参考这篇文章 。