java小技巧-关于乱码的那些个破事

 这篇文章说难不难,说简单不简单,其实更多的在乎与经验,不过就本文来说,我更多的想阐述为什么会产生乱码,什么情况下会产生乱码,然后如何去解决乱码,对于有哪些乱码情况非常多,并不一定是那一种情况导致的,清楚了过程和原理,那么乱码都不在乎是什么大问题:

 

本文纲要:

1、乱码的来源与本质。

2、什么时候会产生乱码?

3、如何分析乱码和解决乱码?

4、我所遇到过的乱码情况。

 

第一部分:乱码的来源与本质:

其实,乱码的来源要追溯到语言文字在计算机中的表达方法了,也就是在计算机中存储和显示过程中,计算机本身并不识别文字,而只是识别数字本身,所以对文字的存储和显示以及转换我们都需要一个编码的过程,在常见的阿拉伯数字和英文字母,以及常见的英文符号,计算机在早期使用了256个字符就足以表达,那个时候也不存在什么乱码的问题;随着计算机的普及,需要适应越来越多的文字,所以,256个字符以已经不能满足大家的需求,所以为了适应更多的文字,出现了各种各样的编码,有些是为了本身的语言,有些是为了国际标准,类似可以表达中文的字符集就有:Unicode、GB2312、GBK、GB18030、UTF-8等等,以GB打头的都是支持基本的字符和中文的,GB2312只能支持六千多个字符,GBK可以支持两万多,GB18030可以支持两万七千多,他们向下兼容,采用2个字节表达中文,而UTF-8是采用3字节表达一个中文。

那么乱码产生的原因就可以简单归结为:在进行数据的编码、解码的过程中,编码和解码的所使用的方法不一样;这个说起来貌似简单但是抽象,我们先定位到这里,然后用下面的内容来充实它(我们通常将数据编码和解码的过程,如果是在程序中是面向对象的,就称之为序列化和反序列化的过程;就像webService也是基于RPC协议,在对象进行序列化和反序列化的时候也可能会出现乱码)。

编码本身也首先由对应的操作系统所能支持的字符集来决定,其实就目前来说,这些字符集几乎所有的操作系统都会支持,所以这个问题不用考虑太多(查看操作系统所能支持的字符集locale -e);

那么操作系统默认会使用一种字符集,所谓默认使用也就是启动进程的时候,或者说某个运行于这个操作系统的进程发生数据交换的过程中,如果没有指定字符集,就会使用默认的字符集。

操作系统本身的字符集可以通过环境变量来控制,对于一个类似于通过终端登录上去的用户,也可以默认指定字符集,就是在用户级别下面设置对应的环境变量即可。

同一个用户下面可以启动不同的进程可以在进程启动时通过export设置对应的默认字符集,这也是可以的;

如果某个进程要去进行某种IO操作,而这个IO操作的来源编码和进程默认字符集不一样,那么就要在读取的时候指定转换的字符集或手工将二进制数据转换为字符格式的数据。

 

我们再反过来说,也就是当一个程序发生某种交互的过程中,如果当前程序中有指定的字符集,就用指定的,如果没有就会逐层向上去找默认的:从当前进程、父进程、用户、OS逐层向上找;所以它的情况就会变得比较复杂了。

 

OK,可能看到这里你会更加的晕,那么一般情况下编码和解码同时发生一般只会发生在两个进程之间或者两个时间点上,因为同一个进程内部通信并且发生在同步调用上是没有必要编码的,就像你在同一段程序中,要传递一个String到子方法中是不需要发生编码的一样。

 

那么我们将乱码产生的原因再细化一层就是:在数据进行了两个进程之间的通信或发生在两个时间点的编码和解码工作,就有可能会发生乱码。

 

那么两个进程通信是什么意思?发生在两个时间点是什么意思?为什么需要编码和解码?

所谓两个进程通信就是指程序和程序之间的交互,例如:程序之间通过RPC、HTTP、RMI等传递数据,这些通信可能是网络交互,可能就是本机的交互,但是他们始终是进程与进程之间的通信,最简单的例子就是服务器向客户端浏览器通信,客户端的浏览器本身就是一个进程,每种浏览器可能会采用不同的线程、缓存处理方法来与服务器端通信,不过总体上会基于一个国际标准的协议规范。

而发生在两个时间点是指,将数据放在某个中间位置,这个中间位置是需要被编码的,另一个时间点有一个进程去读取这个中间数据(这个进程可能是同一个进程,可能不是);例如:程序将一个带有中文内容的信息保存到一个文件中,然后这个文件可以被用户所下载,下载的时候就需要读取这个文件的内容,读取这个文件的内容可以是另一个进程来完成,也可以是同一个,只要两者的编码和解码方法不一样,就会产生乱码。

 

第二部分:什么时候会产生乱码?

通过第一部分的阅读,相信大家在对乱码的认识上有一个原理上了解,我们没有必要纠结于原理上的非常细节的细节,最终你只需要知道他们在什么情况下产生乱码了;这样说好像很抽象,我们举一些常见的web应用系统的例子,可能对大家的理解会更加好一点,在一个web请求中,可能会发生以下动作:

1、请求普通的JSP或VM的渲染页面;

 

首先客户端发起请求,客户端的请求内容将会在提交前被浏览器所编码,这个编码和字符集以及协议有一些编码,而且会形成请求的http头部信息,如果没有手工去做编码的过程,那么浏览器为你的编码一般是浏览器在请求这个资源文件时默认的编码;然后服务器端接受的是一个二进制数据(注意,网络中传输的就是二进制数据,这个二进制数据就是被编码后的数据);

 

如果你的URL上面有中文的话,要注意了,你可能会遇到时而好用,时而不好用的情况,因为浏览器会自动将这个中文进行编码,至于它怎么编码,就又和浏览器本身和访问有些关系了;如果你需要指定一种编码集的话,需要提前将其编码掉,可以使用java的URLEncoding或者js中提供的一些编码方法。

 

然后服务器端需要进行反解析,这部分可能会被服务器进程所控制,也可能被应用本身所控制,也有可能会被应用中的某个框架所控制,也可能会被程序本身所控制,但是控制的基本原理都是实用request请求上设置的字符集所决定,也就是request设置这个字符集是告诉request对来源数据应当用什么样的字符集去解析,这部分可能会被框架本身所做,如果没做回到上面一章就是逐步向上找;

 

服务器处理完请求后,就开始在程序内部进行运行,这个时候可能会去读写取数据库,其实,应用程序和数据库交互是通过jdbc去交互的,jdbc本身是和数据库之间建立一种TCP协议,也是打开一个socket流,传输过程仍然是进程和进程之间的交互,所以在交付前,jdbc需要进行对应的编码工作,而返回时jdbc会进行相应的解码工作,同样数据库端也需要相应的编码工作才能返回数据;数据库内部也有进程和进程之间交互,包括和磁盘之间的交互;在Oracle内部一般是统一字符集,而MySQL会存在很多级别的字符集,所以如果MySQL表级别的字符集和数据库级别的字符集不一样的时候,需要在URL上设置字符集才能好用,jdbc本身的实现过程是由厂商来决定的,它中间可以自己指定字符集或和数据库之间通信拿到字符集都可以完成。

 

服务器端得到信息后,然后开始渲染数据到页面,这个过程也存在字符集的问题,这个也可能会被框架本身所决定,如果是jsp渲染一般有pageEncoding来决定渲染的字符集(但是这里并不代表浏览器就要用这种字符集来解析),velocity一般是由配置文件告诉框架,当然也可以自己response中将对应的中文字符串通过.getBytes("GBK")来进行编码,当response输出后,剩下就是浏览器的解析了。

 

浏览器会如果是首次请求这个站点或已经失效,那么会使用:<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>中指定的字符集,否则会去采用默认的某种字符集进行尝试,一般第一次请求这个站点也不会出现什么问题;如果你发现请求一个点偶尔出现乱码,清空缓存就没有了,那么就是浏览器记住了某些东西,这种问题一般是静态资源的编码导致了一些站点记忆并且服务器端没有告诉浏览器应该用什么方法来解析,此时即使在head部分加上上面那段信息也不好用,这个头部只是页面的头部,而并不是真正http交互的头部;所以对于这类情况,需要在服务器端输出的时候指定Content-Type,在JSP中就是通过:<%@ page contentType="text/html;charset=GBK" %>,在java中是通过:response.setContentType("text/html;charset=GBK");这句话是告诉浏览器,这是一个文本类型的html格式数据,请你通过GBK进行方解析。

 

2、浏览器请求一个静态资源(JS这一类)

其实CSS不太可能有乱码,因为这个里面几乎不会有非英文字符,而图片是二进制数据;JS有可能是用户自己编写,那么也有可能有乱码,请求的过程和上面差不多,差别有两点;其一是没有数据库操作,其二是JS如果没有用户自己处理的话,web容器在处理在调度时发现资源没有处理的servlet,那么就会向容器上层进行抛,每一种容器都会有自己的一个默认的servlet来处理静态资源,一般我们叫他:DefaultServlet,mapping的时候,就是使用 / 关键字来mapping,就是找不到的就走这里(换言之,如果你不想用服务器端默认的DefaultServlet,你可以自己写一个,最简单的写法就是,将文件直接用二进制读出来输出去(用二进制读出来不会进行编码,直接就是文件本身的编码,所以只需要浏览器能知道该怎么解码就可以了,这样性能也会更好),当然如果这个Servlet不是使用二进制处理的,那么它应该就会有一个参数让你设置它的defaltEncoding,这个要看具体的应用服务器的实现了)。

这个servlet一般会用二进制来处理这些静态资源,而且会判定资源的lastModified以判定文件是否需要重新加载,以及如果文件没有被修改过,那么就想客户端输出304状态(浏览器会认为这个文件没有被修改过),如果被修改则直接被装在输出,状态为200,但是静态文件在服务器端一般是不会被修改的。

如果是304状态,浏览器此时也有可能会采用站点以前的某种字符集,这个一般不会出现什么问题,除非你的JS文件本身的字符集发生了变化,其次就可能是浏览器的bug了;如果JS真的字符集有可能会发生变化,而且不想因为浏览器的问题导致乱码,那么js文件上可以增加一个charset告诉浏览器是什么字符集解析<script type="text/javascript" src="abc.js" charset="GBK"></script>注意,这里的字符集和文件本身的字符集关系好就可以;如果你觉得这样还不够帅,那么告诉你一个狠招,那么就是将DefaultServlet重写掉,冲写的时候,在输出文件时就需要设置:response.setContentType("text/javascript; charset=GBK");代表是一个文本类型javascript,头部告诉浏览器使用GBK进行解码。

 

3、服务器进程之间通信

其实这种问题上面有说过,就是程序和数据库之间就是类似的例子,不同的程序通信也是这个道理,在java方面,还存在一类特殊的操作就是对象序列化,其实所谓的对象序列化就是在数据结构方面做了一个特殊的标志而是,本身对象是没有序列化的能力的;它最终还是需要传递数据,只是结构和数据按照某种特定的格式传输,也就是说理解对象的序列化同样可以使用上面的说明来理解;

同样和文件的交互就是类似的IO操作,存储在磁盘上肯定是二进制的信息,所以需要在存储前将其编码,在java中如果使用默认的FiltReader和FileWriter,而没有进行编码那么就会像第一章所述采用一些默认的信息;这也是为什么有些人说自己的程序在自己调试程序的时候是好用的,为什么放到服务器上就不好用了,因为服务器上某些默认的环境信息和你的本地不一样,这种情况不仅仅针对于读写文件,在进程的通信各方面都有这种说明;要对文件进行字节转换字符,或字符转换为字节进行磁盘读写,有两种方法保证字符集的一致性,一种方法就是文件的内容提前转换为byte数据,通过InputStream和OutputStream数据来读写;另一种方法就是通过FileInputStream和FileOutputStream,转换为对应的Reader和Writer的时候,需要在参数上设置字符集,如:new InputStreamReader(new FileInputStream(file) , "GBK");至于是否进行Buffer是另外的问题了,这里就是告诉如何进行编码和解码的设置过程,同理在对象序列化的时候也是通过这类似的方法来进行包装。

 

 

第三部部分:如何分析乱码和解决乱码?

通过上面两章的说明,应该知道乱码的原因和常见的乱码情况,那么如何分析乱码呢,我想你无论是那一种乱码也逃不掉我们在第二部分所讨论的乱码大的种类,但是小的种类应当如何去定位呢,这个一个是逐步细化,一个是经验积累,有些乱码问题及时知道是怎么回事也未必知道如何去解决,尤其是面对一些客户端浏览器本身的问题,所以解决方法也是很重要的。

 

分析乱码你看到了,中间只要有发生任何两个进程通信或时间点差异的都有可能会发生乱码,最基本的你要学会断点跟踪,一般这类问题是首先要跟踪,也就是在那两个进程之间交互的时候发生了乱码,也许是一个小小的细节,也许是连锁反应,也许是蝴蝶效应,看具体情况而定,按照上面每种情况再加上应用场景中框架本身的情况,那么乱码的种类我们估计可以数出上千种、上万种,经验固然重要,不过如果头两次遇到乱码解决了总结经验是一种成长,遇到数十次还是没有理解为什么是乱码,那么永永远远都会出现乱码,而且可能在偶然的时候出现自己可能理解不了的乱码。

 

所以分析乱码刚开始只能靠自己碰到或者自己去模拟,强制将一些字符集设置得不一样;而有一些乱码遇到的经验,就可以解决一些,这种就是凭借经验了,但是遇到新的情况就又要慢慢去“猜”了;如果要成为乱码解决的高手,就要理解原理,而不是猜,乱码的情况多种多样,科学的理解乱码,原理是为了理解乱码出现的原因(其实就两点:不同的进程、不同的时间点),跟踪是为了定位乱码出现在哪里,认识本质一般就能定位到对应应用场景为什么会出现乱码,根据应用场景去做对应的调整就可以解决本质性的问题(所谓对应的场景,就是这种乱码出现的抽象粒度,在对应的抽象粒度去修改而不影响其他的代码;其次,如果是框架、服务器本身所提供,那么就从框架服务器本身所需要的配置信息上去解决即可,第二点也可以归结为抽象粒度,只是较高级别的抽象粒度)。

 

OK,这这一段貌似看起来像是废话,因为什么也没说,也没说怎么定位乱码,没说怎么解决乱码;但是我并不这么认为,因为乱码本身的定位就是场景所决定,这里宗旨是首先学会去理解原理,然后跟踪定位,通过本质认识原因和抽象级别,进而和对应场景结合来解决,就场景而言千变万化,没有说谁就是正确的,关键的是你需要有整个对应场景处理过程的理解,以及清晰冷静的跟踪到问题的点上来,进而通过原理去分析这个过程;乱码的分析和解决,切忌之处为妄下结论,过于依赖于经验本身,甚至于认为是java本身的问题,这样很难解决问题甚至自己不可能解决问题;下面第四部分给出一些常见的场景应用场景:

 

第四部分:我所遇到过的乱码情况。

我所遇到的乱码情况也不能一一列举,只能说在两年前自己通过一些较为底层的接触,了解了乱码原因后,后来解决乱码问题已经不是什么太困难的事情,直到前端时间我遇到了和浏览器本身的缓存所导致的显示乱码,才将我折腾了一翻,不过这也算是一种场景下的表现,在对应的服务器、对应的框架、对应的代码、对应的浏览器下偶然的发生了,让人难以捉摸,不过还好,最终将偶然变成必然进行了模拟,更加了解到交互的细节,解决了这个问题,这个问题也是两年多来遇到最难搞定的乱码问题了。

 

1、在请求一个资源时,URL上带上中文,没有编码,导致了乱码:这种不推荐,但是非要用,就用JS对URL进行Encode操作,同理,两个程序之间通过Http进行交互,如果需要携带中文也通过这种方法编码,否则浏览器为你编码,编出来是什么就不好说了,不同的浏览器会采用不同一些默认值和缓存手段来处理。

 

2、请求服务器端一个资源,数据中包含有中文,中文在客户端使用了utf-8编码规则,服务器端接受的是乱码,通过上面第二章的分析,我们可以看到一个请求到服务器上是请求一个进程,这个进程会交给对应的应用下的程序去处理,应用下有可能有框架去处理,框架最终交给业务代码去处理,也就是中间任何一个部分都可以进行解码操作,如果发生的转码行为不一样或者发生了两次不一样的转码就会出现乱码(发生两次不同的编码就有可能会产生不好解决的问题),如在很多tomcat中默认的请求是按照IOS8859-1来处理的,struts你可以通过设置struts.i18n.encoding来控制它的编码,但是不论在哪里设置,最终是在讲byte转换为char的过程中进行不同的处理,如果服务器和框架本身没有对它进行处理,那么你可以使用一个:request.setCharacterEncoding()来处理,但是这个一定要和提交请求的编码一致,这个代码如果过滤器后面没有其他的处理,就可以放在过滤器中保证这个应用下面都是用这种字符集来接受请求的参数;

如果转码的和客户端发送的编码一致,但是程序处理需要另一种编码,那么就你就需要先按照对应编码转换为byte[]数组,然后再然后自己需要的编码进行解析出来,这个过程就是new String(a.getByte("IOS-8859-1"),"UTF-8");这样就OK了。

在weblogic服务器上可能会需要设置(在web.xml中增加,这个为全局参数,这个参数会被weblogic服务器在启动对应的应用时读取并使用):

<context-param>
<param-name>weblogic.httpd.inputCharset./*</param-name>
<param-value>GBK</param-value>
</context-param>
补充一点:这类请求也同时可以解决你在通过了一个servlet后forward到下一个jsp页面使用request.getParameter的出现乱码的问题,因为request对象都是一个。

 

3、页面在输出时本身为乱码,如果是velocity的配置就要看看在配置velocity中的参数:output.encoding是否正确;如果是JSP那么就要看看页面头部指定的:pageEncoding="UTF-8",或者contentType="text/html;charset=UTF-8",默认会使用后者,前者主要是在输出时使用,但是没有的话后者可以起作用,而前者不会去设置输出的头部,也就是浏览器获取到头部信息,而contentType正好是浏览器的头部信息,如:

java小技巧-关于乱码的那些个破事

指定了contentType这里就会变成对应的头部,或者设置Header时,将Key设置为:Content-Type即可;不设置这个,并不代表浏览器解析出来就是乱码,要看情况,但是设置了这个基本不太可能是乱码,因为即使本地有缓存,头部信息浏览器都会去请求服务器端的,注意这个头部并不是客户端浏览器在<head></head>内部设置的,这个设置在客户端某些浏览器有一些站点缓存的时候,不会被生效,所以最终极的方法还是在头部去设置。

 

当然页面输出乱码有可能本身某个步骤读取出来的数据就是乱码,而并不一定是渲染的时候,那么这个部分就需要跟踪代码了,比如数据库中本身存的就是乱码、或者读取数据库的时候变成了乱码、获取读取某个文件的时候出现的问题、或者和其他进程中交互中出现的乱码,其原理一致 ,框架能控制的在框架控制,框架不方便控制的就用代码控制。

如在weblogic中,你可以在weblogic.xml中配置一段:

<jsp-descriptor> 
<jsp-param>
<param-name>compilerSupportsEncoding</param-name> 
<param-value>true</param-value> 
</jsp-param> 
<jsp-param> 
<param-name>encoding</param-name> 
<param-value>GBK</param-value> 
</jsp-param> 
</jsp-descriptor>

来说明你的就jsp要使用什么样的一个编码集来进行编译(jsp渲染的时候是首先要将jsp内部本身转换为servlet,然后在输出的)。

4、客户端浏览器本身的头部问题,上面已经说明,通过contentType的设置即可解决问题,这类问题一般是偶尔出现,而且是因为站点引用资源多种编码集导致的缓存在浏览器中的一些内容,而服务器端没有返回contentType的时候就会发生。

5、客户端JS或CSS的静态资源乱码,其实上面也是说过了,就是DefaultServlet如果提供了字符集的选择就将其设置对应的字符集,如果没有,那么它一般是采用二进制处理的话传输过程中不会出现问题,客户端接收的时候,在js上设置一个charset,即:<script type="text/javascript" src="abc.js" charset="GBK"></script>,如果这样设置显得过于麻烦,或者在某些部分很恶心的浏览器上不见效,那么就将DefaultServelt进行重写,在输出内容时候,设置response.setContentType("text/javascript; charset=GBK");至于静态资源如何去做缓存,或是否做缓存,是业务上的问题,默认静态资源会做一定程度的缓存,这个具体要看WEB服务器的默认指标和服务器设置。

6、读写文件时乱码,如果读写文件不需要识别内部的数据,进而作一些处理,那么就不需要转换为字符,直接使用字节流进行处理即可,这样最快速,减少两个步骤的转码过程;如果是需要处理内容,那么在Reader和Writer部分,设置字符集,如:new InputStreamReader(new FileInputStream(file) , "GBK");如果你不设置,而直接用FileReader和FileWriter将会采用某种父进程的默认字符集。

7、和远程的程序进行简单socket交互时,同上不需要任何处理数据转换,那么用二进制处理最稳定、最快速;

8、和远程的程序进行类似RPC协议,也就是传递的都是具有数据类型的信息,序列化的过程需要交给框架,此时需要在框架内部指定序列化和反序列化的字符集,或者在程序中指定,否则使用默认。

9、URL上本身有中文,那么就需要提前编码,可以使用java中的URLEncoding提前编码好渲染到前端,也可以使用JS提供Encoding方法来完成。

 

其实还有很多很多乱码的场景,上面说的应该不是8种乱码,而是8大类,其实也算是最初提出3大类的细化版本,还可以继续细化,总之如果你真想出现了乱码,可以让他千变万化,要让它不出现乱码关键是编码和解码的方法要一致,现今的乱码不能说用一种公共的方法来解决,因为标准和个性化是并存的,到目前为止不能谁将谁干掉;但是可以用一种较为科学的流程和方法来处理;每种应用场景都有可能遇到它所存在的乱码的可能性,关键是要了解其实质上在哪里发生的,哪里发生了一个错误的转换。

上一篇:Java数组


下一篇:MySQL 事务和 MVCC 机制