java web中文编码问题

深入分析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技术内幕》一书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值