{
inputStreamReader.close();
System.out.println(stringBuffer.toString());
在我们的应用涉及IO操作时,只要注意指定统一的编码解码Charset字符集,一般不会出现乱码问题。对有些应用程序不指定字符编码,则在中文环境中会使用操作系统默认编码。不建议使用操作系统默认的编码,因为这样会使应用程序和运行环境绑定起来,在跨环境时很可能出现乱码问题。
2. 内存中的编码
在内存中进行从字符到字节的数据类型转换也涉及编码,在Java中用String表示字符串,所以String类就提供了转换到字节的方法,也支持将字节转换为字符串的构造函数。
String string = "测试字符串";
byte[] bytes = string.getBytes("UTF-8");
String string2 = new String(bytes, "UTF-8");
3.在Java中如何编解码
以" I am 君山" 为例子介绍在java中如何以ISO-8859-1、GB2312、GBK、UTF-8、UTF-16进行编解码。
java中编码用到的类图如下:
首先根据指定的charsetName通过Charset.forName(charsetName)找到Charset类,然后根据Charset创建CharsetEncoder,再调用charsetEncoder.encode()方法对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。
string.getBytes(charsetName)编码过程的时序图如下:
1.按照ISO-8859-1编码
String str = "I am 君山";
String charset = "ISO-8859-1";
byte[] bytes = str.getBytes(charset);
System.out.println(binary(bytes, 16));
System.out.println(new String(bytes, charset));
4920616d203f3f
I am ??
可以看出,7个char字符应该ISO-8859-1变为7个byte数组,ISO-8859-1是单字节编码,中文"君山"被转换成是 3f 的byte,也就是"?",所以经常出现中文变成?。中文字符经过该编码会丢失信息,通常称之为"黑洞",它会把不认识的字符吸收掉。
2.GB2312编码
String str = "I am 君山";
String charset = "GB2312";
byte[] bytes = str.getBytes(charset);
System.out.println(binary(bytes, 16));
System.out.println(new String(bytes, charset));
4920616d20befdc9bd
I am 君山
可以看出,前五个字符经过编码后仍然是五个字节,汉字被编码成双字节,GB2312只支持6763个汉字,所以并不是所有汉字都能够用GB2312编码。
3.GBK编码
4920616d20befdc9bd
I am 君山
编码的结果与GB2312一样。GBK兼容GB2312,它们的编码算法一样,不同的是码表长度不同,GBK支持的汉字更多。所以只要是GB2312编码的汉字都可以用GBK进行解码,反之则不然。
4.UTF-8编码
4920616d20e5909be5b1b1
I am 君山
UTF-8对单字节范围内的字符仍然采用单字节,汉字采用3个字节。这也是U8采用0-6字节变长编码的优点。
5.UTF-16
结果如下:(feff表示大头存储)
feff004900200061006d0020541b5c71
I am 君山
UTF-16编码将char数组放大了一倍,单字节范围内的字符在高位补0变长两个字节,中文字符也变为两个字节。特点是编码效率高,规则很简单,缺点就是对单字节扩大为双字节浪费存储。
补充:几种编码格式的比较
GB2312与GBK编码规则类似,但是GBK范围更大,它能处理所有汉字字符。GB2312编码的GBK都可以解码,反之不然。
UTF-16与UTF-8都是处理Unicode编码,它们的编码规则不太相同。相对来说,UTF-16的编码效率更高,从字符到字节的相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间的快速转换,如Java的内存管理就使用UTF-16编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难修复,所以相比较而言UTF-8更适合网络传输。UTF-8对ASCII码采用单字节存储,另外单个字符损坏也不会影响后面的字符,在编码效率上介于GBK和UTF-16之间,所以U8在编码效率和编码安全性上做了平衡,是理想的编码方式。
4.JavaWeb设计编码的地方
从使用中文的角度来说, 有IO的地方就会设计编码,大部分的IO是网络IO,数据经过网络传输时是以字节为单位,所以所有的数据都被序列化为字节。在Java中,要被序列化必须实现Serializable接口。
这里考虑两个问题:
1.一段文本的实际大小如何计算?
一个简单的例子就是如何压缩Cookie的大小,减少网络传输量。所谓的压缩只是将多个单字节字符变成一个多字节字符,较少的是String.length(),而并没有减少最终的字节数。例如将 ab 两个字符通过某种编码变成一个奇怪的字符,虽然字符数从两个变成一个,但是如果采用UTF-8编码,这个奇怪的字符最后经过编码可能会变成3个或者更多的字节。同样的,如果整型数字 1234567 作为字符串处理,则采用UTF-8编码占用7个字节;采用UTF-16将占用14个字节,但是作为int类型的数字来存储时则只需要4个字节。所以看一段文本的大小,只看字符本身的长度是没有意义的,即使一样的字符,采用不同的编码最终的存储结果也不同,所以从字符到字节一定要看编码类型。
2.当我们在文本编辑器中输入汉字时它到底是怎么表示的?
我们知道,在计算机里所有的信息都是0和1表示的,那么一个汉字到底是多少个0和1呢。例如在"淘宝"的unicode编码是 "feff6dd85b9d", feff表示大头存储方式,所以16进制是 "6dd85b9d"。在java中,每个char占2byte,所以两个汉字在java中用char表示,会占用4个字节。
把这两个问题搞清楚以后,看一下在javaweb中哪些地方可能会存在编码转换。
用户从发起一个HTTP请求,需要存在编码的地方是URL、Cookie、Parameter。服务端收到请求后要解析HTTP,其中URI、Cookie、Post参数需要解码,服务端还可以读取数据库中的数据----本地或者网络中其他地方的文本文件,这些数据都存在编码问题。当Servlet处理完所有的请求后,需要再将这些数据编码,通过Socket发送到用户请求的浏览器里,再通过浏览器解码成为文本。过程如下:
2.1URL的编码解码
用户提交一个URL,可能存在中文,因此需要编码。URL的组成部分如下:
以tomcat作为Servlet Engine为例,把它们分别对应到下面这些配置文件中。
Port对应在<Connector port="8080">中配置,ContextPath在<Context docBase="MyWeb" path="/examples"/>配置,ServletPath在Web应用的web.xml的<url-pattern>配置,PathInfo是我们请求的具体的Servlet,QueryString是要传递的参数。注意这里是在浏览器直接输入URL,所以是Get方式提交,如果是POST方法请求,QueryString将通过表单方式提交到服务器端。
我们验证浏览器有中文的时候如何编码和解码,如下地址:
可以看到"君"的编码后为"%e5%90%9b","山"为"%e5%b1%b1"。可以看到PathInfo的编码和QueryString的编码都是UTF-8。至于为什么会有%,URL的编码规范RFC3986可知,浏览器编码URL是将非ASCII码按照某种格式编码成16进制数字后将每个16进制表示的字节前加"%"。
1.解释tomcat如何解码URI和QueryString
对URL的URI部分、QueryString 进行解码的字符集是在connector的<Connector URIEncoding="UTF-8"/>中定义的,如果没有定义,将以默认的编码集,所以有中文URI时最好指定为UTF-8编码。
这里解释一下解决get乱码以及解决办法:
(1)如果connector的URIEncoding是UTF-8编码,对URI和QueryString会采用UTF-8进行一次解码,如果前端的URI和QueryString是U8编码,会解码正确;
(2)一般URI没人写中文,所以一般乱码是QueryString乱码;如果server.xml不是U8编码会默认以ISO-8859-1解码,而前台编码用的是U8,所以会导致解码失败。解决办法就是前台用JS进行encodeURI(encodeURI(param))进行编码。关于两次encodeURI的原因如下:
前台一次encodeURI有可能导致后台解码乱码,比如前台用U8进行encodeURI,后台server.xml用ISO-8859-1解码就会乱码。
前台两次编码,后台代码中主动进行一次解码不会出错。例如:
汉字"中",一次encodeURI之后变为 "%e4%b8%ad", 两次encodeURI之后变为"%25e4%25b8%25ad" ,加%25是因为"%"编码后变为"%25"。后台接受到之后用server.xml指定的编码集解码为: "%e4%b8%ad",然后代码中主动解码一次即可:
URLDecoder.decode("%E4%B8%AD", "utf-8")
2. HTTPHeader的编解码
当客户端发起一个HTTP请求时,除上面的URL外,还可能在Header中传递其他参数,比如cookie、redirectPath。对header的解码是在调用request.hetHeader时进行的,只能使用ISO-8859-1,不能指定其他编码,所以如果Header中有非ASCII码字符,解码肯定会乱码。
所以Header中传递中文只能编码再添加到Header中。
3.POST表单的编解码
POST表单提交的参数的解码是在第一次调用request.getParameter时发生的,POST表单的参数传递 方式与QueryString不同,它是通过HTTP的BODY传递到服务端的。当我们提交时,浏览器首先将根据ContentType的Charset编码格式对在表单中填入的参数进行编码,然后提交到服务端,在服务端同样也是用ContentType的Charset编码格式进行解码。所以通过POST提交一般不会乱码,可以通过request.setCharacterEncoding进行设置。
需要注意:一定要在第一次调用request.getParameter方法之前就设置request.setCharacterEncoding(charset),否则提交上来的数据也可能出现乱码。
另外,针对multipart/form-data类型的参数,也就是上传文件的编码,同样也使用ContentType定义的字符集编码。需要注意的是,上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及字符编码,而真正编码是在将文件内容添加到parameters中时,如果用这个不能编码,会使用ISO-8859-1。
4.HTTP Body的编解码
当用户请求的资源成功后,这些内容将通过Response返回客户端浏览器。z合格过程要先经过编码,再到浏览器进行解码。编码字符集可以通过response.setCharacterEncoding来设置,它将会覆盖request.getCharacterEncoding的值,并且通过Header的Content-Type返回客户端,浏览器收到返回的Socket流时将通过Content-Type的charset进行解码。如果返回的Content-Type没有设置charset,那么浏览器将根据<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />中的charset进行解码,如果也没指定会使用浏览器默认的编码来解码。
5.JS需要编码的地方
JS操作页面元素的情况越来越多,尤其是JS发起异步请求的情况遇到的编码问题更会经常出现。
1.外部引入JS文件
在一个单独的JS文件中包含字符串输入的情况,如:
<!DOCTYPE html>
<title></title>
<script src="js/test.js"></script>
</head>
</body>
</html>
test.js代码如下:
document.write("测试汉字");
效果如下:
这时如果script标签没有设置charset,浏览器会以当前页面默认的编码解析这个JS,由于上面JS没有指定页面的编码,也没有指定script的编码集,所以编码错误,可以指定页面编码,也可以指定script编码,如下:
<script src="js/test.js" charset="UTF-8"></script>
或者指定页面编码集:
<meta charset="utf-8" />
2.JS的URL编码
JS处理编码有三个函数,escape、encodeURI和encodeURIComponent。
关于这三个函数的区别以及用法参考之前的文章:https://www.cnblogs.com/qlqwjy/p/9934706.html
3.其他需要编码的地方
除了URL和 参数编码问题,在服务端还有很多地方需要编码,如:XML、Velocity、JSP或者从数据库读取数据等。