在上一篇我们把编码的理论知识讲了一遍,在这篇我将会根据这列理论知识对一些乱码问题进行具体分析并给出解决方法
操作系统中的编码
在window中默认文件编码格式是ANSI,而ANSI并不特指哪一个具体的编码,它在不同的语言环境会对应不同的编码,比如在简体中文系统中就是GBK编码,而在繁体中文就是Big5编码。我们可以在Windows记事本中通过另存为来指定保存时所用的编码,如果不知道默认会以GBK编码保存(简体中文环境)
而在Liunx系统中文件默认编码为UTF-8,所以会经常出现一个文件在两个操作系统之间转移的时候发生乱码,其实解决方法很简单
- 更改文件的读取编码为原系统的默认编码
- 在原系统中按照目标系统的编码重新保存
也可以先1,之后直接把文件转换成当前系统默认编码,这样就不用每次打开文件时重新选择读取编码
java中字符串乱码
java类中字符串乱码
在java中,一个程序需要先编译成class文件,然后在由java虚拟机进行执行。而在class文件中,字符串都是以UTF-8编码表示的,这个是java虚拟机的规范。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。而表结构中存在着一个叫CONSTANT_Utf8_info的表,用来存放字符串,而且是按UTF-8编码。所以乱码实际上是编译的时候你的文件编码跟javac使用的文件编码不一致而导致的。
javac默认的编码为操作系统的编码,这个编码在windows下位GBK,Linux下为UTF-8。在这里我用UTF-8编码格式把下面代码保存在windows系统中。
public class EncodingTest
{
public static void main(String[] args)
{
String s1 = "啊哈哈哈";
System.out.println(s1);
}
}
编译,运行,结果为乱码
这是因为javac以GBK编码格式来读取我们的文件。
解决方法也很简单,只需要在编译时加上一个
-encoding utf-8
参数就行了而在IDE里,不管文件用什么编码,运行时我们基本都没有发现过乱码,其实IDE在编译时会帮助我们加上编码参数。
我们经常听说java中一个字符占两个字节,可是刚才又提到class文件中为UTF-8编码,这不是矛盾吗。其实我们所说的java中一个字符占两个字节指的是我们的java程序在运行时一个字符占用两个字节的空间,而着两个字节的内容正式其对应的UTF-16BE编码值。我们知道java虚拟机不能直接运行class文件,一个class能运行需要经过类的加载链接初始化三个阶段,而在这个过程过包括了把class文件的字符串字面量读取并放入java堆中的字符串常量池,而在常量池里都是按照UTF-16BE编码存在。
文件读取乱码
文件读取乱码的原因相信大家也很容易猜到了,就是读取编码和文件实际编码不一致。但是实际上会有各种复杂的情况。比如下面的代码,我们在Intellij idea里运行,其中1.txt
是在windows系统里以UTF-8编码的文件,我们用NIO的api直接把文件内容读取到一个byte数组里,然后用这个byte数组构造一个字符串。
@Test
public void testFieEncoding() throws IOException
{
Path path = Paths.get("1.txt");
byte[] bytes = Files.readAllBytes(path);
String s1 = new String(bytes);
System.out.println(s1);
}
按道理来说在这里由于没有指定编码,所以为默认的GBK,应该得到乱码输出,可是并没有
是不是很神奇,其实这都是IDE搞的鬼,Intellij idea在运行程序时会加上一个
-Dfile.encoding
虚拟机参数,其值为项目使用的编码。
jvm中的默认编码
在jvm进行需要编码的相应操作时,如果我们没有指定,其便会采用默认的编码进行处理,而这个默认的编码我们可以通过以下代码获得
System.out.println(System.getProperty("file.encoding"));
或者用下面jdk1.4新增的方法
System.out.println(Charset.defaultCharset());
其中JVM获取默认编码的方式如下
- 首先去查看是否在启动jvm时指定了
-Dfile.encoding
参数,如果指定,则为其指定的编码 - 如果1失败了,则会去环境变量里去找
JAVA_TOOLS_OPTIONS
这个环境变量。在这个环境变量里我们指定jvm默认的编码,比如设置其值为-Dfile.encoding="UTF-16"
- 如果1和2都失败了,则会去读取操作系统的默认编码,并将其设置为默认编码
不过,最好的方法应该时在构造字符串时指定编码。像下面这样,只要读取文件编码为UTF-8,不管怎么搞,都不会出现乱码。
public void testFieEncoding() throws IOException
{
Path path = Paths.get("1.txt");
byte[] bytes = Files.readAllBytes(path);
String s1 = new String(bytes, "UTF-8");
System.out.println(s1);
}
http请求中的编码
URLEncode
我们知道URL只能包含英文字母、阿拉伯数字和某些标点符号,当我们要在Url中传递非英文字符的话,就必须要进行编码,而这个编码并没有具体的标准,有的浏览器默认按照UTF-8,有的则默认跟随系统编码。 一个URL编码过程是这样的,如我我要向localhost:8080以GET传递name=张三这个参数,首先它的URL将会是这个样子http://localhost:8080?name=张三
,既然URL不允许有中文,那么需要对特殊字符“编码”做编码转换成字节,假设我们用UTF-8来进行Encoder,“张三”两个字通过UTF-8编码后的字节用16进制表示就是E5BCA0E4B889
。UrlEncode方法在每个字节前面添加一个百分号,形成最终的地址:http://localhost:8080?name=%E5%BC%A0%E4%B8%89
我们可以借助URLEncoder这个类来帮助我们完成
System.out.println(URLEncoder.encode("http://localhost:8080?name=张三", "UTF-8"));
浏览器端的处理
刚才我们说了浏览器会对非英文字符进行Encoder处理,那个Encoder使用的编码又是如何确定的呢
- 如果设置
<meta charset="">
标签,则会按照其设置的值进行Encoder - 如果没有设置,则会使用一种编码进行Encoder,前面我们说了,这个默认编码在不同浏览器上会有区别,有的会使用UTF-8,有的会使用操作系统默认的
- 对于ajax请求,不管是否指定
<meta charset="">
标签,都是按照UTF-8来进行Encoder的
服务端的处理
尽然浏览器传过来的URL是编码过的,那么服务器端必然要对请求进行解码。而在Tomcat中默认的解码编码为ISO-8859-1。刚才我们讲了浏览器编码的编码一般不是UTF-8,就是操作系统的GBK,服务端解码是按照ISO-8859-1,所以很容易发生乱码.
解决方案
- 转换编码
假设项目采用UTF-8编码
String name = request.getParameter("name");
name = new String(name.getBytes("ISO-8859-1"),"UTF-8")
- 修改tomcat的默认URIEncoding为UTF-8
我们修改server.xml中的Connector标签,加上URIEncoding="UTF-8"
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000" URIEncoding="UTF-8"
redirectPort="8443" />
- 编写拦截器,指定编码
比如在spring中使用的编码拦截器
<filter>
<filter-name>charsetFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>charsetFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
post请求中的编码
对于post请求,其参数在请求体里,而不在URL中,所以不存在Encoder的过程,因此处理相对容易一些。除了上面的解决方法外,我们还可以通过request.setCharacterEncoding()
方法来设置读取请求体时使用的编码,所以此方法对GET请求时无效的,而网上经常有人说拿它作为GET请求乱码的解决方案,是在误导别人。注意此方法要在获取参数之前调用。
常见的一些乱码
"??"乱码分析:
这种情况一般是用不支持中文的编码去编码中文字符,例如ISO-8859-1 仅能编码非英文字符,所以非英文字符被其编码时会被转换为 0x3F(即?的 ASCII 编码),这时编码已经真被转成不可逆的乱码了。之后无论用兼容 ASCII 的哪种编码方案解码还原出的字符串都是"?"。"²âÊÔ"乱码分析:
这种一般是用不支持中文的编码去解码含有中文的编码造成的,ISO-8859-1 仅能表示非英文字符,所以使用其解码时会严格按一个字节一个字节地进行解析(这种操作其实对编码没构成破坏,还可以重新用 ISO-8859-1 获取字节流后再用正确的编码方式解码得到正确的字符串)。
- "����"乱码分析:
这种一般是GBK编码后的内容用UTF-8进行解码时发现四个字节均为 UTF-8 非法字节流,所以直接转化为了�。
乱码问题(一)编码那些事