09年在原来公司做的一篇文章,现在共享出来。
此次研究主要针对Linux操作系统中Java环境下可能产生的中文乱码问题展开一些试验,目的在于寻求一套无乱码的解决方案。
此文档目的在于详细介绍《2 过程记录文档》中得出的相关结论,以及描述建立一个统一编码环境的具体过程。
一、 准备知识
1. 字节、字符、编码
从计算机对多国语言的支持角度看,大致可以分为三个阶段:
|
系统内码 |
说明 |
系统 |
阶段一 |
ASCII |
计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。 |
英文 DOS |
阶段二 |
ANSI编码 |
为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。 |
中文 DOS,中文 Windows 95/98,日文 Windows 95/98 |
阶段三 |
UNICODE |
为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。 |
Windows NT/2000/XP,Linux,Java |
字符串在内存中的存放方法:
在 ASCII 阶段,单字节字符串使用一个字节存放一个字符(SBCS)。比如,"Bob123"在内存中为:
42 |
6F |
62 |
31 |
32 |
33 |
00 |
B |
o |
b |
1 |
2 |
3 |
\0 |
在使用 ANSI 编码支持多种语言阶段,每个字符使用一个字节或多个字节来表示(MBCS),因此,这种方式存放的字符也被称作多字节字符。比如,"中文123" 在中文 Windows 95 内存中为7个字节,每个汉字占2个字节,每个英文和数字字符占1个字节:
D6 |
D0 |
CE |
C4 |
31 |
32 |
33 |
00 |
中 |
文 |
1 |
2 |
3 |
\0 |
在 UNICODE 被采用之后,计算机存放字符串时,改为存放每个字符在 UNICODE 字符集中的序号。目前计算机一般使用 2 个字节(16 位)来存放一个序号(DBCS),因此,这种方式存放的字符也被称作宽字节字符。比如,字符串 "中文123" 在 Windows 2000 下,内存中实际存放的是 5 个序号:
2D |
4E |
87 |
65 |
31 |
00 |
32 |
00 |
33 |
00 |
00 |
00 |
中 |
文 |
1 |
2 |
3 |
\0 |
一共占 10 个字节。
理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:
|
概念描述 |
举例 |
字符 |
人们使用的记号,抽象意义上的一个符号。 |
'1', '中', 'a', '$', '¥', …… |
字节 |
计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。 |
0x01, 0x45, 0xFA, …… |
ANSI |
在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串。 |
"中文123" |
UNICODE |
在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串。 |
"中文123" |
各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的“字符”。比如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:
各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。
“UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。
2. 常用字符集
简单介绍一下常用的编码规则,为后边的章节做一个准备。在这里,我们根据编码规则的特点,把所有的编码分成三类:
分类 |
编码标准 |
说明 |
单字节字符编码 |
ISO-8859-1 |
最简单的编码规则,每一个字节直接作为一个 UNICODE 字符。比如,[0xD6, 0xD0] 这两个字节,通过 iso-8859-1 转化为字符串时,将直接得到 [0x00D6, 0x00D0] 两个 UNICODE 字符,即 "ÖÐ"。 |
ANSI 编码 |
GB2312, |
把 UNICODE 字符串通过 ANSI 编码转化为“字节串”时,根据各自编码的规定,一个 UNICODE 字符可能转化成一个字节或多个字节。 |
UNICODE 编码 |
UTF-8, |
与“ANSI 编码”类似的,把字符串通过 UNICODE 编码转化成“字节串”时,一个 UNICODE 字符可能转化成一个字节或多个字节。 |
我们实际上没有必要去深究每一种编码具体把某一个字符编码成了哪几个字节,我们只需要知道“编码”的概念就是把“字符”转化成“字节”就可以了。对于“UNICODE 编码”,由于它们是可以通过计算得到的,因此,在特殊的场合,我们可以去了解某一种“UNICODE 编码”是怎样的规则。
二、 乱码产生过程分析
为了让使用Java语言编写的程序能在各种语言的平台下运行,Java在其内部使用Unicode字符集来表示字符,这样就存在了Unicode字符集和本地字符集进行转换的过程。当在Java中读取字符数据的时候,需要将本地的字符集编码的数据转换为Unicode的编码,而在输出字符数据的时候,则需要将Unicode编码转换为本地字符集编码。
例如,在中文系统下,从控制台读取一个字符“中”,实际上读取的是“中”的GBK编码0xD6D0,在Java语言下要将GBK编码转换为Unicode编码0x4E2D,此时在内存中字符“中”的对应数值就是0x4E2D,当我们向控制台输出字符时,Java语言将Unicode编码再转换为GBK编码,输出到控制台,中文系统再根据GBK字符集画出相应的字符。
从上述过程来看,读取和写入的过程是可逆的,那么理应不会出现中文问题。然而,实际应用的情况,却复杂的多。在web应用中,通常都包括了浏览器、web服务器、web应用程序和数据库等部分,每一个部分都可能使用不同的字符集,从而导致字符数据在各种不同的字符集之间转换时出现乱码问题。
当从Unicode编码像某个字符集转换时,如果在该字符集中没有对应的编码,则得到0x3f(即问号字符?)。这就是为什么有时候我们输入的中文会在输出时却变成了问号。从其他字符集向Unicode编码转换时,如果这个二进制数在该字符集中没有标识任何字符,则得到的结果是0xfffd。
从上述所知,,由于存在着不同的字符集,在各种字符集之间进行转换,就可能出现乱码,同样是中文字符集GB2312和GBK,由于编码范围不同某些字符转换时也会出现乱码。
由于浏览器会根据本地系统默认的字符集提交数据,而web容器默认采用的是ISO-8859-1的编码方式解析POST数据,另外JDBC驱动也多采用ISO-8859-1的编码格式,因此,在web应用程序运行中,输入的中文字符往往需要在不同的字符集之间来回转换,这也就导致了中文乱码问题的频繁出现。
下图描述了在web应用的请求响应过程中,发生编码转换的过程,其中浏览器是IE,web容器时Tomcat。
从下图的描述过程中可以看到,如果在web应用程序不指定任何字符集,从浏览器传来的中文字符,输出回浏览器时,可以正常显示(以简体中文的方式查看网页)。然后,事情并没有这么简单,在servlet/jsp中,可能存在直接写入的或者从其他来源读取的中文字符,如果这些字符对应的Unicode码是从GB2312编码转换来的,那么以ISO-8859-1编码方式输出,这些字符将不能正常显示。所以对于图中的中文处理,应在②和⑤的位置明确使用GB2312或者GBK字符集。
三、 编码转换过程分析
我们常见的JAVA程序包括以下类别:
l 直接在console上运行的类(包括可视化界面的类)
l JSP代码类(注:JSP是Servlets类的变型)
l Servelets类
l EJB类
l 其它不可以直接运行的支持类
这些类文件中,都有可能含有中文字符串,并且我们常用前三类JAVA程序和用户直接交互,用于输出和输入字符,如:我们在JSP和Servlet中得到客户端送来的字符,这些字符也包括中文字符。无论这些JAVA类的作用如何,这些JAVA程序的生命周期都是这样的:
l 编程人员在一定的操作系统上选择一个合适的编辑软件来实现源程序代码并以.java扩展名保存在操作系统中,例如我们在中文win2k中用记事本编辑一个java源程序;
l 编程人员用JDK中的javac.exe来编译这些源代码,形成.class类(JSP文件是由容器调用JDK来编译的);
l 直接运行这些类或将这些类布署到WEB容器中去运行,并输出结果。
那么,在这些过程中,JDK和JVM是如何将这些文件如何编码和解码并运行的呢?
这里,我们以中文win2k操作系统为例说明JAVA类是如何来编码和被解码的。
第一步,我们在中文win2k中用编辑软件如记事本编写一个Java源程序文件(包括以上五类JAVA程序),程序文件在保存时默认采用了操作系统默认支持GBK编码格式(操作系统默认支持的格式为file.encoding格式)形成了一个.java文件,也即,java程序在被编译前,我们的JAVA源程序文件是采用操作系统默认支持的file.encoding编码格式保存的,java源程序中含有中文信息字符和英文程序代码;要查看系统的file.encoding参数,可以用以下代码:
public class ShowSystemDefaultEncoding {
public static voidmain(String[] args) {
String encoding =System.getProperty("file.encoding");
System.out.println(encoding);
}}
第二步,我们用JDK的javac.exe文件编译我们的Java源程序,由于JDK是国际版的,在编译的时候,如果我们没有用-encoding参数指定我们的JAVA源程序的编码格式,则javac.exe首先获得我们操作系统默认采用的编码格式,也即在编译java程序时,若我们不指定源程序文件的编码格式,JDK首先获得操作系统的file.encoding参数(它保存的就是操作系统默认的编码格式,如WIN2k,它的值为GBK),然后JDK就把我们的java源程序从file.encoding编码格式转化为JAVA内部默认的UNICODE格式放入内存中。然后,javac把转换后的unicode格式的文件进行编译成.class类文件,此时.class文件是UNICODE编码的,它暂放在内存中,紧接着,JDK将此以UNICODE编码的编译后的class文件保存到我们的操作系统中形成我们见到的.class文件。对我们来说,我们最终获得的.class文件是内容以UNICODE编码格式保存的类文件,它内部包含我们源程序中的中文字符串,只不过此时它己经由file.encoding格式转化为UNICODE格式了。
这一步中,对于JSP源程序文件是不同的,对于JSP,这个过程是这样的:即WEB容器调用JSP编译器,JSP编译器先查看JSP文件中是否设置有文件编码格式,如果JSP文件中没有设置JSP文件的编码格式,则JSP编译器调用JDK先把JSP文件用JVM默认的字符编码格式(也即WEB容器所在的操作系统的默认的file.encoding)转化为临时的Servlet类,然后再把它编译成UNICODE格式的class类,并保存在临时文件夹中。如:在中文win2k上,WEB容器就把JSP文件从GBK编码格式转化为UNICODE格式,然后编译成临时保存的Servlet类,以响应用户的请求。
第三步,运行第二步编译出来的类,分为三种情况:
A、直接在console上运行的类
B、 EJB类和不可以直接运行的支持类(如JavaBean类)
C、 JSP代码和Servlet类
D、 JAVA程序和数据库之间
下面我们分这四种情况来看。
1. 直接在console上运行的类
这种情况,运行该类首先需要JVM支持,即操作系统中必须安装有JRE。运行过程是这样的:首先java启动JVM,此时JVM读出操作系统中保存的class文件并把内容读入内存中,此时内存中为UNICODE格式的class类,然后JVM运行它,如果此时此类需要接收用户输入,则类会默认用file.encoding编码格式对用户输入的串进行编码并转化为unicode保存入内存(用户可以设置输入流的编码格式)。程序运行后,产生的字符串(UNICODE编码的)再回交给JVM,最后JRE把此字符串再转化为file.encoding格式(用户可以设置输出流的编码格式)传递给操作系统显示接口并输出到界面上。
对于这种直接在console上运行的类,它的转化过程可用下图更加明确的表示出来:
以上每一步的转化都需要正确的编码格式转化,才能最终不出现乱码现象。
2. EJB类和不可以直接运行的支持类
由于EJB类和不可以直接运行的支持类,它们一般不与用户直接交互输入和输出,它们常常与其它的类进行交互输入和输出,所以它们在第二步被编译后,就形成了内容是UNICODE编码的类保存在操作系统中了,以后只要它与其它的类之间的交互在参数传递过程中没有丢失,则它就会正确的运行。
这种EJB类和不可以直接运行的支持类, 它的转化过程可用下图更加明确的表示出来:
3. JSP代码和Servlet类
经过第二步后,JSP文件也被转化为Servlets类文件,只不过它不像标准的Servlets一校存在于classes目录中,它存在于WEB容器的临时目录中,故这一步中我们也把它做为Servlets来看。
对于Servlets,客户端请求它时,WEB容器调用它的JVM来运行Servlet,首先,JVM把Servlet的class类从系统中读出并装入内存中,内存中是以UNICODE编码的Servlet类的代码,然后JVM在内存中运行该Servlet类,如果Servlet在运行的过程中,需要接受从客户端传来的字符如:表单输入的值和URL中传入的值,此时如果程序中没有设定接受参数时采用的编码格式,则WEB容器会默认采用ISO-8859-1编码格式来接受传入的值并在JVM中转化为UNICODE格式的保存在WEB容器的内存中。
Servlet运行后生成输出,输出的字符串是UNICODE格式的,紧接着,容器将Servlet运行产生的UNICODE格式的串(如html语法,用户输出的串等)直接发送到客户端浏览器上并输出给用户,如果此时指定了发送时输出的编码格式,则按指定的编码格式输出到浏览器上,如果没有指定,则默认按ISO-8859-1编码发送到客户的浏览器上。
这种JSP代码和Servlet类,它的转化过程可用下图更加明确地表示出来:
4. JAVA程序和数据库之间
对于几乎所有数据库的JDBC驱动程序,默认的在JAVA程序和数据库之间传递数据都是以ISO-8859-1为默认编码格式的,所以,我们的程序在向数据库内存储包含中文的数据时,JDBC首先是把程序内部的UNICODE编码格式的数据转化为ISO-8859-1的格式,然后传递到数据库中,在数据库保存数据时,它默认即以ISO-8859-1保存,所以,这是为什么我们常常在数据库中读出的中文数据是乱码。
对于JAVA程序和数据库之间的数据传递,我们可以用下图清晰地表示出来:
四、 分析乱码的原则
首先,经过上面的详细分析,我们可以清晰地看到,任何JAVA程序的生命期中,其编码转换的关键过程是在于:最初编译成class文件的转码和最终向用户输出的转码过程。
其次,我们必须了解JAVA在编译时支持的、常用的编码格式有以下几种:
l ISO-8859-1,8-bit, 同8859_1,ISO-8859-1,ISO_8859_1等编码
l Cp1252,美国英语编码,同ANSI标准编码
l UTF-8,同unicode编码
l GB2312,同gb2312-80,gb2312-1980等编码
l GBK , 同MS936,它是gb2312的扩充
及其它的编码,如韩文、日文、繁体中文等。同时,我们要注意这些编码间的兼容关体系如下:
Unicode和UTF-8编码是一一对应的关系。GB2312可以认为是GBK的子集,即GBK编码是在gb2312上扩展来的。同时,GBK编码包含了20902个汉字,编码范围为:0x8140-0xfefe,所有的字符可以一一对应到UNICODE2.0中来。
再次,对于放在操作系统中的.java源程序文件,在编译时,我们可以指定它内容的编码格式,具体来说用-encoding来指定。注意:如果源程序中含有中文字符,而你用-encoding指定为其它的编码字符,显然是要出错的。用-encoding指定源文件的编码方式为GBK或gb2312,无论我们在什么系统上编译含有中文字符的JAVA源程序都不会有问题,它都会正确地将中文转化为UNICODE存储在class文件中。
然后,我们必须清楚,几乎所有的WEB容器在其内部默认的字符编码格式都是以ISO-8859-1为默认值的,同时,大多数的浏览器在传递参数时都是默认以UTF-8的方式来传递参数的(IE有时候是根据本地系统的语言环境来定编码)。所以,虽然我们的Java源文件在出入口的地方指定了正确的编码方式,但其在容器内部运行时还是以ISO-8859-1来处理的。
五、 统一编码的解决方案
综上所述,建立一个统一编码的环境是避免乱码问题产生的最好方法。而为了支持更多的语言,我们最好选择UTF-8来作为统一的编码字符集。以下是Linux下建立统一编码环境的步骤。
1. Linux的设置
按照教程指导的安装过程会缺少中文字体安装包,所以首先安装中文支持:
# rpm -ivh --forcefonts-ISO8859-2-100dpi-1.0-17.1.noarch.rpm
# rpm -ivh --force fonts-ISO8859-2-1.0-17.1.noarch.rpm
# rpm -ivh --force fonts-ISO8859-2-75dpi-1.0-17.1.noarch.rpm
# rpm -ivh --forcefonts-chinese-3.02-12.el5.noarch.rpm
# fc-cache -f -v
然后设置语言环境:
编辑/etc/sysconfig/i18n,修改内容如下:
LANG="zh_CN.UTF-8"
SUPPORTED="zh_CN.UTF-8:zh_CN:zh:en_US.UTF-8:en_US:en"
SYSFONT="latarcyrheb-sun16"
编辑/etc/profile,加入:
export LC_ALL=zh_CN.UTF-8
2. JVM的设置
JVM的缺省编码方式由系统的“本地语言环境Locale”设置确定。JVM的file.encoding属性,这个属性确定了JVM的缺省的编码/解码方式:从而影响应用中所有字节流==>字符流的解码方式 ,字符流==>字节流的编码方式。JVM会根据操作系统的Locale设置自动调整语言环境。基本不用设置。
3. Tomcat的设置
为了使Tomcat支持中文文件名并且让Tomcat能够自己处理Get方式传递过来的参数,那么我们已经在Tomcat安装目录下的conf/server.xml里头修改如下:
connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> 注意一种特殊情况,如果在程序中调用response.sendRedirect()方法重定向到中文文件名的页面,需要以如下方式调用: response.sendRedirect(java.net.URLEncoder.encode(“中文.html”,”UTF-8”)) 也就是在使用重定向语句的时候,需要用java.net.URLEncoder的静态方法 encoder() 按照指定的编码手动转码。 4. JSP页面编码设置 关于JSP页面中的pageEncoding和contentType两种属性的区别: pageEncoding是jsp文件本身的编码 contentType的charset是指服务器发送给客户端时的内容编码 JSP要经过两次的“编码”,第一阶段会用pageEncoding,第二阶段会用utf-8至utf-8,第三阶段就是由Tomcat出来的网页, 用的是contentType。 第一阶段是jsp编译成.java,它会根据pageEncoding的设定读取jsp,结果是由指定的编码方案翻译成统一的UTF-8 JAVA源码(即.java),如果pageEncoding设定错了,或没有设定,出来的就是中文乱码。 第二阶段是由JAVAC的JAVA源码至java byteCode的编译,不论JSP编写时候用的是什么编码方案,经过这个阶段的结果全部是UTF-8的encoding的java源码。 JAVAC用UTF-8的encoding读取java源码,编译成UTF-8 encoding的二进制码(即.class),这是JVM对常数字串在二进制码(java encoding)内表达的规范。 第三阶段是Tomcat(或其的application container)载入和执行阶段二的来的JAVA二进制码,输出的结果,也就是在客户端见到的,这时隐藏在阶段一和阶段二的参数contentType就发挥了功效。 对于每个JSP页面,为了让它使用Utf-8的编码去返回给浏览器,那么加入下面一行在页面开始是必不可少的: <%@page contentType="text/html;charset=utf-8"%> 5. 使用过滤器 过滤器是用来统一设置一个web项目中request的处理编码的方便快捷的方法,它相当于在每个JSP页面上都设置了request.setCharacterEncoding("utf-8")。 其中一种过滤器的代码如下: package filters; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.UnavailableException; public class SetCharacterEncodingFilter implements Filter { protected String encoding = null; protected FilterConfig filterConfig = null; protected boolean ignore = true; public void destroy() { this.encoding = null; this.filterConfig = null; } public void doFilter(ServletRequest request, ServletResponseresponse,FilterChain chain) throws IOException, ServletException { // 设置正确的编码方式 if (ignore || (request.getCharacterEncoding() == null)) { String encoding = selectEncoding(request); if (encoding != null) request.setCharacterEncoding(encoding); } // 传递到下一层过滤器 chain.doFilter(request, response); } public void init(FilterConfig filterConfig) throws ServletException{ this.filterConfig = filterConfig; this.encoding = filterConfig.getInitParameter("encoding"); String value = filterConfig.getInitParameter("ignore"); if (value == null) this.ignore = true; else if (value.equalsIgnoreCase("true")) this.ignore = true; else if (value.equalsIgnoreCase("yes")) this.ignore = true; else this.ignore = false; } protected String selectEncoding(ServletRequest request) { return (this.encoding); } } 将它编译完成后放置到WEB-INF\classes\filters目录下,然后在修改WEB-INF\web.xml,加入: 6. 数据库和数据表编码设置 安装数据库时如果需要将其配置成Utf-8的默认编码,那么可以在安装步骤configure的时候加入参数--with-charset=utf8。 安装过数据库之后如果想修改数据库的默认编码,那么应该这么做: 修改mysql默认编码设置: # vi /usr/local/mysql/mysql3306/etc/my.cnf 在[client]下添加 default-character-set=utf8 在[mysqld]下添加 default-character-set=utf8 新建数据表时,如果在语句中设置DEFAULT CHARSET,那么将会使用数据库的默认编码,数据表的字段也可以单独设置编码,如果没有设置则是使用数据表的默认编码。 六、 参考资料 Linux下Java程序中文乱码问题研究 Java和J2EE的中文编码问题终极解决之道 Java的中文处理学习笔记:Hello Unicode Linux 下 Java 中文环境设置方法 Java 关于中文乱码问题的解决方案与经验 JSP中文乱码问题的解决 Java/JSP中文乱码问题解决心得 fc5启动提示cannot open font file none LINUX支持中文 centos 英文环境下安装中文输入法 在PC上搭建CentOSLinux系统 tomcat中文问题(转) java byte与char互转原理 UTF-8 and Unicode FAQ 用例子详细介绍各种字符集编码转换问题 WEB开发中的JAVA字符编码经验总结 JSP和Servlet中的几个编码的作用及原理 Java编码格式问题大总结 深入剖析Java编程中的中文问题及建议最优解决方法--上篇 深入剖析Java编程中的中文问题及建议最优解决方法---下篇 小谈MySQL字符集 MySQL字符集详解(一) MYSQL教程:MYSQL字符集支持 解決Tomcat 5.0.19 中文參數傳遞問題 字符,字节和编码 - Characters, BytesAnd Encoding