1. “联通”怪事
新建一个名为 mytxt.txt 的记事本文件,输入“联通”后,保存并关闭。
再次用记事本打开 mytxt.txt,你会发现:
“联通”乱码了。
2. 字符如何存储与显示?
例如:“联” 的 GBK 编码的二进制表示为 0xC1AA。
3.1. ASCII
ASCII(美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,是现今最通用的单字节编码系统。
3.2. ISO-8859系列
ASCII 是单字节编码系统,但它只用了 7 位,即只能表示 128 个字符。为了表示更多的欧洲常用字符,ISO 组织对 ASCII 进行了扩展,制定了一系列标准来扩展 ASCII 码,他们是ISO-8859-1 ~ ISO-8859-16,其中 ISO-8859-1 涵盖了大多数西欧语言字符,应用得最广泛。
3.3. BIG5/GB2312/GBK/GB18030
BIG5
世界上 ,沒有一拳解決不了的事,如果有,那就兩拳。 琦玉
GB2312
GBK
GB18030
3.4. UNICODE与UTF-8
Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。每个数字代表唯一的至少在某种语言中使用的符号。
注:Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。UTF-8 则是目前使用最广的一种 Unicode 编码方式。
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码。UTF-8 用 1 到 4 个字节编码 Unicode 字符。UTF-8的编码规则很简单,只有二条:
例:“联” 的 Unicode 编码是 0x8054。由于 0x8054 位于 0x0800-0xFFFF 之间,所以使用 3 字节 UTF-8 转换模板:1110xxxx 10xxxxxx 10xxxxxx。0x8054 的二进制表示是:1000 0000 0101 0100,用这个比特流依次代替模板中的x,得到: 11101000 10000001 10010100,即E8 81 94。
4. “联通”怪事揭秘
“记事本”默认用 GBK 编码保存数据,“联通”两字的GBK编码如下:
巧合的是,“联”的两个字节、“通”的两个字节的起始部分的都是"110"和"10",正好符合 2 字节 UTF8 编码的规律,于是再次打开记事本时,记事本就误认为这是一个用 UTF-8 编码的文件,用 UTF-8 去解析 GBK 的数据,造成了乱码。
5. 编码衍生问题汇总
5.1. BOM 与 UTF-8
1. 什么是 BOM 头?
BOM 头是放在 UTF-8 编码格式文件的头部,占三个字节(0xEF 0xBB 0xBF),用来标识该文件属于UTF-8编码。
注:window记事本在用UTF-8格式保存文件时,会自动加上BOM头。
2. 有什么问题?
有些软件不能正确识别BOM头。
比如,Linux/Unix 环境下,不能编译 UTF-8-BOM 格式 Java 文件。
3. 如何去掉 BOM 头?
用软件咯,以Notepad++为例,依次选择【编码】->【转为UTF-8】,保存即可。
5.2. ZipOutputStream 中的编码问题
下面分类说明
1. JDK6 的 java.util.zip.ZipOutputStream
package encoding;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class ZipDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("d:/webj2ee.zip");
ZipOutputStream zos = new ZipOutputStream(fos);
zos.putNextEntry(new ZipEntry("中文路径/中文名称.txt"));
zos.write("没有什么是一拳解决不了的,如果有,那就两拳。——琦玉".getBytes("UTF-8"));
zos.closeEntry();
zos.close();
fos.close();
}
}
2. Apache Ant 的 org.apache.tools.zip.ZipOutputStream
package encoding;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
public class ZipDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("d:/webj2ee.zip");
ZipOutputStream zos = new ZipOutputStream(fos);
zos.setEncoding("GBK");
zos.putNextEntry(new ZipEntry("中文路径/中文名称.txt"));
zos.write("没有什么是一拳解决不了的,如果有,那就两拳。——琦玉".getBytes("UTF-8"));
zos.closeEntry();
zos.close();
fos.close();
}
}
3. ≥JDK7 的 java.util.zip.ZipOutputStream
package encoding;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class ZipDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("d:/webj2ee.zip");
ZipOutputStream zos = new ZipOutputStream(fos, Charset.forName("UTF-8"));
zos.putNextEntry(new ZipEntry("中文路径/中文名称.txt"));
zos.write("没有什么是一拳解决不了的,如果有,那就两拳。——琦玉".getBytes("UTF-8"));
zos.closeEntry();
zos.close();
fos.close();
}
}
5.3. String.getBytes(charset) 与 new String(bytes, charset)
1. 不要想当然认为它们可逆:
public static void main(String[] args) throws UnsupportedEncodingException {
String r1 = new String("联通".getBytes("ISO8859-1"), "ISO8859-1");
System.err.println(r1);
}
注:ISO8859-1根本无法表示中文,当然也就无法得到“联通”两字在ISO8859-1中的编码值了,所以再通过new String()还原就更无从谈起了。
2. 下面这种形式常用于网络数据传输:
public static void main(String[] args) throws UnsupportedEncodingException {
String sent = new String("联通".getBytes("UTF-8"), "ISO8859-1");
System.out.println(sent);
String recv = new String(sent.getBytes("ISO8859-1"), "UTF-8");
System.out.println(recv);
}
5.4. I/O 流与字符集
示例:
package encoding;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class IOs {
public static void main(String[] args) throws IOException {
// 1. “字符流” -> “字节流”
try (FileOutputStream fos = new FileOutputStream("d:/one-punch-man.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");) {
osw.write("没有什么是一拳解决不了的,如果有,那就两拳。——琦玉");
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
// 2. “字节流” -> “字符流”
try (FileInputStream fis = new FileInputStream("d:/one-punch-man.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
BufferedReader br = new BufferedReader(isr);) {
String l = br.readLine();
System.out.println(l);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
5.5. JVM 默认字符集
1. 有一些接口与 JVM 默认字符集有关:
2. JVM 默认字符集如何确定?
先上一段JDK源码
java -Dfile.encoding=utf-8 ...
5.6. Eclipse - Console 中的编码
不慌,
轮到“前端”的编码问题了
5.7. 浏览器如何解析 HTML 文件?
解析策略:
万能用例代码:
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
StringBuffer sb = new StringBuffer();
sb.append("\r\n");
sb.append("<!DOCTYPE html><html><head>\r\n");
sb.append(" <meta charset=\"gbk\">\r\n");
sb.append(" <title></title>\r\n");
sb.append("</head><body>\r\n");
sb.append("\t没有什么是一拳解决不了的,如果有,那就两拳。——琦玉\r\n");
sb.append("</body></html>");
String html = sb.toString();
// HTML类型
response.setContentType("text/html");
// BOM头
response.getOutputStream().write(0xEF);
response.getOutputStream().write(0xBB);
response.getOutputStream().write(0xBF);
response.getOutputStream().write(html.getBytes("UTF-8"));
}
例1: 没有BOM头、没有content-type,按<meta chaset>解析
例2: 有BOM头,按UTF-8解析
例3: 没BOM头,有content-type,按content-type解析
5.8. 浏览器如何解析外部 JS 文件?
通过<script>标记引入外部 JS:
<script charset="UTF-8" src="webj2ee.js">
解析逻辑如下:
例1:没有BOM头、没有content-type,有charset声明;
例2:有 BOM 头;
例3:没有BOM头、有content-type;
5.9. URL编码、解码函数?
5.9.1. escape()、unescape()
escape() 是将字符转换为Unicode编码值。解码是通过unescape()函数。
注意:ECMAScript v3 标准不建议使用escape()处理URL编码。应该使用encodeURI和encodeURIComponent()来代替;
5.9.2. encodeURI()
encodeURI()是将字符串进行UTF-8编码。解码通过decodeURI()。
5.9.3. encodeURIComponent()
encodeURIComponent()也是将字符串进行UTF-8编码,它比encodeURI的编码还要彻底,在encodeURI的基础上,将那些在URI中有特殊含义的标点符号也一起编码了。
注意:这个函数通常用于将一个URL当做一个参数放在另一个URL中。
5.9.4. URLEncoder.encode(charset)
Java 端的URL编码类,解码类为 java.net.URLDecoder。
注意:建议使用 UTF-8 字符集进行编码。
注意:基本等同于 JS 的 encodeURIComponent 函数(主要是标点、空格不同)。
5.10. Http Header 中的编码
Http 的 Header 中传递的内容(比如:Cookie),编解码统一用的是ISO8859-1字符集,而且不能更改,所以在Header中不能使用非ASCII字符。
示例:在Cookie中传输中文会报错
<%@ page language="java" pageEncoding="UTF-8"%>
<%
Cookie c = new Cookie("name", "联通");
response.addCookie(c);
%>
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title></title>
</head><body></body></html>
注:如果要在Cookie中存储中文信息,可以用base64等技术编码一下。
5.11. GET 请求中的编码、解码
5.11.1. URL结构
以Tomcat作为Servlet Engine 为例,它们分别对应到下面这些配置文件中:
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
<Context docBase="code" path="/code" reloadable="true" source="org.eclipse.jst.jee.server:code"/>
<servlet>
<servlet-name>Code</servlet-name>
<servlet-class>com.Code</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Code</servlet-name>
<url-pattern>/servlet/servlet/*</url-pattern>
</servlet-mapping>
5.11.2. 浏览器如何编码 PathInfo 和 QueryString
备注:
5.11.3. 服务器如何解码 PathInfo 和 QueryString
5.11.3.1. PathInfo 解码(实际是 URI,包含PathInfo):
Tomcat 对 URI 解码的字符集由 Connector 中的 URIEncoding 属性指定,默认 ISO-8859-1。
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="UTF-8"/>
5.11.3.2. 对QueryString:
总结:不要在GET请求中使用中文字符。如果必须要传输中文字符,那可以先用encodeURIComponent()方法对中文字符编码,再发送GET请求。
5.12. Request 中的编码问题(POST请求)
5.12.1. 通过 Form 发的 POST 请求:
5.12.2. 通过 $.ajax 发的 POST 请求:
最佳实践:下面的4部分字符集要统一、建议使用UTF-8
5.13. Response 中的编码问题
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("没有什么是一拳解决不了的,如果有,那就两拳。——琦玉");
5.14. 认识 Spring 的编码过滤器
<filter>
<filter-name>characterEncodingFilter</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>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Spring 源码:
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
request.setCharacterEncoding(this.encoding);
if (this.forceEncoding) {
response.setCharacterEncoding(this.encoding);
}
}
filterChain.doFilter(request, response);
}
所以encoding=true、forceEncoding=true合起来,意味着:
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
5.15. JSP 文件头中的编码问题
<%@ page language="java" pageEncoding="UTF-8"
contentType="text/html; charset=UTF-8"%>
注:如果不写contentType,只有 pageEncoding,则自动生成与 pageEncoding 一样编码的 contentType。
5.16. 䶮——“飞龙在天”
字音:yǎn; 起源:五代时南汉刘岩为自己名字造的字; 含义:“飞龙在天”的意思;
5.16.1. “䶮”的特殊性
GBK编码字符“䶮”,在Unicode中存在两种表示:PUA区的0xE863、非PUA区的0x4DAE。
注:PUA, Private Use Area
5.16.2. “䶮”为什么特殊?
GBK字符集中有80个增补字符最初并未在Unicode中定义,于是使用了Unicode的PUA区域的代码点表示。后来Unicode使用非PUA区域代码点正式定义了这80个字符。这样就出现有80个汉字在Unicode定义的代码点区域中有两种不同的表示方法。
GBK字符集80个增补字符:
5.16.3. “䶮”——可能会遇到什么问题?
例如:
Oracle使用ZHS16GBK字符集存储字符“䶮”,但AIX系统从数据库中读出后,展示为问号 (?)。
问题原因:
该问题是由Oracle ZHS16GBK字符集和IBM® GBK 转换器之间GBK未定义的代码范围Unicode映射的不兼容性导致的。
测试代码:
public class Code {
public static void main(String[] args) {
System.err.println(System.getProperty("file.encoding"));
printBytes(""); // GBK
}
private static void printBytes(String chars) {
byte[] bytes = chars.getBytes();
StringBuffer sb = new StringBuffer();
for(int i=0; i<bytes.length; i++){
int mask = 0x80;
do{
if((mask & bytes[i]) != 0){
sb.append("1");
}else{
sb.append("0");
}
}while((mask >>= 1) !=0);
sb.append(" ");
}
System.err.println(sb.toString());
}
}
AIX上的运行结果:
REDHAT上的运行结果:
5.17. 文件下载中的编码问题
...直接看代码吧...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String fileContent = "没有什么是一拳解决不了的,如果有,那就两拳。——琦玉";
String fileName = "琦玉名言.txt";
String encodedFileName = null;
if (request.getHeader("User-Agent").toLowerCase().indexOf("firefox") > -1) {
encodedFileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1");
} else {
encodedFileName = URLEncoder.encode(fileName, "UTF-8");
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName);
response.getOutputStream().write(fileContent.getBytes("UTF-8"));
}