深入分析Java Web中文编码
文章开始之前我们先考虑一个问题,我们为什么要编码?能不能不编码呢,答案肯定是否定的(不然也不存在今天要讨论的问题了0.0)的。因为计算机是无法直接理解我们人类所用的语言符号的,反之我们也无法直接理解计算机的语言。注意:计算机中的基本存储单元为一个字节(byte),而人类的语言符号太多,因此必须经过一些拆分和翻译才能让计算机理解我们的语言。
举个例子,我们把计算机能够理解的语言假定为英语,其他语言要在计算机中使用,必须得经过一次翻译,将其翻译为英语。这个翻译的过程即编码。
是的,如果大家都说英语,而且计算机中存储信息的最小单元是英文单词,这样就不存在编码问题了。
总结来说,之所以存在编码问题原因在于:
- 计算机中存储信息的最小单元是1个字节,即8bit,所能表示的字符范围是0-255。
- 人类要表示的符号太多,无法用一个字节完全表示
因此要解决上述矛盾必须要有一个新的数据结构——char,而从char到byte必须要编码(反之亦然)
如何“翻译”?
计算机中的翻译方式有很多,常见的有ASCII、ISO-8859-1、GB2312、GBK、UTF-16、UTF-8等。我们可以把这些看作是一个字典,它们规定了转化的规则,我们只要按照这个规则就能让计算机正确表示我们的字符。关于如何选择编码方式,要考虑很多因素,例如,是存储空间重要还是编码效率重要等问题。这些编码方式的具体区别和优势在此不展开讨论,下面详细说说java中需要编码的情况。
简单比较一下几种编码格式:
GBK2312和GBK编码规则类似,GBK的范围更大,能处理所有汉字,因此二者之间处理汉字的话首选GBK。UTF-16和UTF-8都是处理Unicode编码,相对来说UTF-16的编码效率高,字符到字节的转换简单高效,字符串操作也更好,适合在磁盘和内存之间使用。但它不适合网络传输,因为网络传输容易损坏字节流,而UTF-16中一个字符吗值损坏后面的所有码值都会受到影响,另外对于单字节的字符处理UTF-16会在高位补0(变成16位),浪费了存储空间。UTF-8对ASCII字符采用单字节存储,单个字符损坏不影响传输,在编码效率上介于GBK和UTF-16之间,在编码效率和安全性上做了平衡,是理想的中文编码方式。
Java中需要编码的场景
设计编码的操作一般都在字节和字符的转化上,而需要这种转换的场景主要是I/O操作,包括磁盘I/O和网络I/O。
1. 在I/O操作中
在java中,Reader类是JAVA的I/O中读字符的父类,而InputStream类是读字节的父类,InputStreamReader类就是关联字节到字符的桥梁,它负责在I/O过程中处理读取字节到字符的转换,而对字节到字符的解码实现,则委托StreamDecoder去做,在解码过程中需要用户指定Charset编码格式,如果未指定编码格式,将按照本地环境的默认字符集。写的情况也类似,字符的父类是Writer,字节的父类是OutPutStream,通过OutPutStreamWriter转换字符到字节,StreamEncoder类负责将字符编码成字节,编码格式和默认编码规则与解码是一致的。
例如:实现文件读写功能代码
String file = "C:/test.txt";
String charset = "UTF-8";
//写字符转换成字节流
FileOutputStream outputStream = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(outputStream,charset);
try {
writer.write("这是要保存的中文字符");
}finally{
writer.close();
}
//读取字节转换成字符
FileInputStream inputStream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(inputStream,charset);
StringBuffer buffer = new StringBuffer();
char [] buf = new char[64];
int end = 0;
try {
while((end = reader.read(buf))!= -1){
buffer.append(buf,0,end);
}
} finally {
reader.close();
}
在设计I/O操作的程序中,我们要注意指定统一的编码charset字符集,如果不指定,会默认使用操作系统默认编码,这样程序的编码格式和运行环境绑定起来,在跨环境时就可能会出现乱码。
2. 在内存操作中
在Java开发中除了I/O设计编码外,最常用的应该是内存中进行字符和字节的相关转换.
String类就提供了转换方式。
String s = "中文字符串";
byte[] b = s.getBytes("UTF-8");
String n = new String(b,"UTF-8");
Charset类也提供了encode和decode对应编码和解码:
String s = "中文字符串";
Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = charset.encode(s);
CharBuffer charBuffer = charset.decode(byteBuffer);
Java Web中涉及的编解码
对于中文来说,有I/O的地方就会涉及编码,而如今大部分I/O乱码问题都涉及到网络I/O。
当用户从浏览器发起一个HTTP请求,对于请求的URL、Cookie、Paramiter来说都需要编码,服务端接收到HTTP后要解析HTTP,其中URL、Cookie和POST表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其他地方的文本文件,这些数据都可能存在编码问题,当Servlet处理完所有请求后,需要再编码通过Socket发送到用户请求的浏览器里,浏览器再解码成文本。一次HTTP请求需要编解码的地方很多,下面详细讨论:
1. URL的编解码
URL中可能存在中文,因此需要编码。URL中的路径信息(PathInfo)和请求参数(QueryString,即?后面的部分)很有可能会出现中文,一般情况下PathInfo是UTF-8编码,而QueryString是GBK编码,至于我们经常看到请求中的%号是因为浏览器编码URL是将非ASCII字符按照某种编码格式编码成16进制数字后再将每个16进制表示的字节加上%(为什么要这样并不知道0.0)。因此浏览器对PathInfo和QueryString的编码是不一样的,不同的浏览器对PathInfo的编码也可能不一样,这就导致服务器解码上的困难。
下面以Tomcat为例看看如和解码。
tomcat对URL的URI部分进行解码的字符集是在connector的<Connector URIEncoding=”UTF-8”/>中定义的,如果未定义使用默认编码ISO-8859-1解析,所以如果有中文URL时最好把URIEncoding设置成UTF-8。
对于QueryString的解析过程,以GET方式HTTP请求的QueryString与以POST方式请求的表单参数都是作为Parameters保存的,都通过request.getParameter获取参数值,对它们的解码是在request.getParameter方法第一次被调用时进行。请求参数QueryString的解码集在哪里定义的呢?它和URI的解码字符集不一样,QueryString的解码字符集要么是Header中ContentType定义的Charset,要么是默认的ISO-8859-1要使用ContentType中定义的编码,就要将connector的<Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”ture”/>中的useBodyEncodingForURI设置为ture。这个配置项并不是对整个URI都采用BodyEncoding进行编码,而仅仅是对QueryString使用BodyEncoding解码
2. HTTP Header的编解码
当客户端发起一个HTTP请求时,除了URL,还可能会在Header中传递其他参数,如Cookie、redirectPath等,对Header中的项进行解码也是调用request.getHeader时进行的。如果请求的Header项没有解码则调用MessageBytes的toString方法,这个方法对从byte到char的转化使用的默认编码也是ISO-8859-1,而我们也不能设置Header的其他解码格式,所以如果你设置的Header中有非ASCII字符,解码中肯定会乱码。因此我们尽量不要在Header中传递非ASCII字符,如果一定要传递,可以先将这些字符用org.apache.catalina.util.URLEncoder编码,再添加到Header中,这样从浏览器到服务器的传递过程中就不会丢失信息了,我们要访问这些项时再按照相应的字符集解码即可。
3. POST表单的编解码
POST表单参数传递方式和URL中的请求参数不同,它是通过HTTP的BODY传递到服务器端的。提交时编解码都是使用ContentType的Charset编码格式,我们可以通过request.setCharacterEncoding(charset)来设置。一定要在第一次调用request.getParameter方法之前就设置request.setCharacterEncoding(charset),否则也有肯出现乱码。
4. HTTP BODY的编解码
用户请求资源成功后,将通过Response返回给客户端浏览器。这个过程需要先经过编码再到浏览器进行解码,编解码字符集可以通过response.setCharacterEncoding来设置,它将覆盖request.getCharacterEncoding的值,并且通过Header的Content-Type返回客户端,浏览器接收到返回的Socket流时将通过Content-Type的charset来解码。如果返回的HTTP Header中Conten-Type没有设置charset,那么浏览器将根据HTML的<meta HTTP-equov=”Content-Type” content=”text/html;charset=GBK”/>中的charset来解码。如果也没有定义,那么浏览器将使用默认编码来解码。
访问数据库都是通过客户端JDBC驱动来完成的,用JDBC来存取数据时要和数据的内置编码保持一致,可以设置JDBC URL来指定,如MySQL:url=“jdbc:mysql://localhost:3306/DB?useUnicode=ture&characterEncoding=GBK”。
在JS中的编码问题
在web应用中,通过js发起异步请求时遇到编码问题的情况越来越多。
1. 外部引入js文件
在一个单独的js文件中包含字符串时:
document.write("中文字符串");
这是如果引入一个script.js脚本需要设置charset:
<html>
<head>
<script src="statics/javascript/script.js" charset="gbk"></script>
这时如果script没有设置charset,浏览器就会以当前这个页面的默认字符集解析这个JS文件,如果外部的JS文件与当前页面的编码格式不一致,上面代码中的中文输入就会变成乱码。
2. JS的URL编码
- encodeURL()
encodeURL()可以将整个URL中的字符(特殊字符除外,如“!”“#”“¥”“&”“’”“(”“)”“*”“+”“,”“-”“.”“/”“:”“;”“=”“?”“@”“_”“~”“0-9”“a-z”“A-Z”)进行UTF-8编码,在每个码前面加上“%”。解码则通过decodeURI()函数。 - encodeURLComponent()
encodeURLComponent()这个函数比encodeURI()编码还彻底,他除了对“!”“’”“(”“)”“*”“-”“.”“_”“~”“0-9”“a-z”“A-Z”这几个字符不编码,对其他字符都编码。解码通过decodeURIComponent()进行解码。 Java和JS编解码问题
前面说了JS编解码问题,如果js进行了编码,编码的字符传到服务器端后可以通过Java来解码,那么java又是怎么解码的呢?我们知道在Java端处理URL编解码有两个类,分别是java.net.URLEncoder和java.net.URLDecoder。这两个类可以将所有“%”加UTF-8码值用UTF-8解码,从而得到原始的字符。
查看URLEncoder的源码可以发现,URLEncoder受保护的特殊字符少于JS中受保护的特殊字符。java端的URLEncoder和URLDecoder与前端JS对应的是encodeURLComponent和decodeURLComponent。注意,前端用encodeURLComponent编码后,到服务器端用URLDecoder解码可能会出现乱码。因为JS编码默认的是UTF-8编码,而服务器端中文解码一般都是GBK或者GB2312,所以用encodeURLComponent编码后是UTF-8,而Java用GBK去解码显然不对。解决办法是用encodeURLComponent两次编码,如encodeURLComponent(encodeURLComponent(str))。这样在java端通过request.getParameter()用GBK解码后取得的就是UTF-8编码的字符串,如果java端需要使用这个字符串,则再用UTF-8解码一次;如果是将这个结果直接通过JS输出到前端,那么这个UTF-8字符串可以直接在前端正常显示。
常见问题分析
1. 中文变成了看不懂的字符
一般是因为GBK编码后用ISO-5899-1解码导致
2. 一个汉字变成一个问号
一般是因为用了不支持汉字的ISO-5899-1编码和编码导致
3. 一个汉字变成两个问号
一般是因为对中文用了GBK编码后再使用ISO-5899-1解码然后再使用了GBK进行了编解码导致。
借鉴《深入分析Java Web技术内幕》一书