尚学堂.张志宇.乱码分析_03_读取servlet参数.doc

1 重要结论
J2SE 5.0 用的是Unicode 4.0
J2SE 6.0 用的也是Unicode 4.0
Java编程语言用16位的编码代表文本。使用UTF-16编码.
一个 char 表示一个 UTF-16 代码单元
并不是一个char代表一个字符,因为一个增补字符需要2个char来代表

2 web.xml
    <servlet>
      <servlet-name>TestInitServlet</servlet-name>
      <servlet-class>TestInitServlet</servlet-class>
     
      <init-param>
      <param-name>name</param-name>
      <param-value>我们</param-value>
      </init-param>
      <init-param>
      <param-name>age</param-name>
      <param-value>30</param-value>
      </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>TestInitServlet</servlet-name>
        <url-pattern>/TestInitServlet</url-pattern>
    </servlet-mapping>

3 原理

import java.io.UnsupportedEncodingException;

public class Test1 {
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "我们";
System.out.println(s);

// ced2 c3c7
System.out.println("--------编码成GBK得到如下字节----------");
byte[] bytes = s.getBytes("GBK");
for (int i = 0; i < bytes.length; i++) {
System.out.println(Integer.toHexString(bytes[i]));
}
// 内存里面是utf16编码,6211 4eec。参看U4E00.pdf
System.out.println("--------utf-16----------");
System.out.println(getUnicodeFromStr(s));
System.out.println("--------得到正确的中文----------");
// 再组装成正确的字符串
String ss = new String(s.getBytes("GBK"), "GBK");
System.out.println(ss);
}

public static String getUnicodeFromStr(String s) {
String retS = "";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
retS += String.format("%1$04x", (int) c) + " ";
}
return retS;
}
}

代码分析:
首先明确的是
“我们”的gbk编码是:ced2 c3c7
可以到GBK编码表去验证
http://www.microsoft.com/globaldev/reference/dbcs/936.mspx

“我们”的utf-16编码是:6211 4eec
可以参考从unicode官方网站上下载下来的《U4E00.pdf》确认此事。
其实U4E00.pdf里面查到的只是代码点。
而utf-16编码,对于普通字符来说(即不是增补字符),和代码点是一致的。

当执行这句话
String s = "我们";
内存里面,局部变量s指向一个字符串对象,这个对象是由utf-16编码序列组成的。即内存里面是这么表示的:s62 11 4e ec
在unicode字符集里面,6211这个代码点代表“我”这个字符,4eec这个代码点代表“们”这个字符。而普通字符的utf-16编码和代码点又是一致的。

接下来当我们执行这句话
System.out.println(s);
又发生了这些事情。
第一, jvm把这个字符串传给了dos窗口,或者说传给了eclipse里面的console。
那么,jvm传给dos窗口的是那些字节呢?是ced2 c3c7
也就是说,jvm不会把内存的表示方式传给dos窗口,也就是说不会把utf-16编码传递给dos窗口,而是把这个字符串的gbk编码传递给了dos窗口。
还有,为什么是把gbk的编码而不是其他的编码传给dos窗口呢?因为gbk是咱们的winxp操作系统的默认编码。
第二, dos窗口,把接受到的字节(这里已经是gbk编码了)按照gbk编码组装成字符串并且显示出来。

接下来执行这句话
byte[] bytes = s.getBytes("GBK");
这句话的意思是得到这个字符串的gbk编码。
jvm能确认局部变量s代表的是“我们”这两个字符吗?当然能。因为现在内存里表示为s62 11 4e ec,这在unicode里面代表的就是“我们”这两个字符。
要得到“我们”这两个字符的gbk编码,只需要到gbk编码表里面去查找就可以了。
所以这一步会得到gbk的编码ced2 c3c7

接着:
System.out.println(getUnicodeFromStr(s));
这句话是用来确认内存里面是不是s62 11 4e ec,
getUnicodeFromStr这个方法里面的代码是取得组成这个字符串的所有char,然后返回表示这些个char值的固定格式的字符串。
不要忘记,字符串是由char组成的。每一个char代表一个utf-16编码单元,但一个char未必表示一个字符,因为java用两个char来表示一个增补字符。

接下来。
String ss = new String(s.getBytes("GBK"), "GBK");
这句话的意思是把这些字节组装成字符串。
这些字节是ced2 c3c7
组装工作很简单。到gbk编码表里面去匹配就可以了。
是不是组装成功取决于这些字节是不是正确的有效的gbk编码。因为gbk编码表里面很多代码点是没有定义字符的。
ced2 c3c7是有效的gbk编码,当然可以正确组装成“我们”这个字符串。
这个字符串在jvm里依然得用utf-16编码来表示。所以,内存里面是这么表示的:ss62 11 4e ec

最后一句话的情形前面分析过了。
System.out.println(ss);
JVM先得到这个字符串的gbk编码然后传给dos窗口,让后dos窗口按照gbk组装成字符串然后显示出来。

4 情况1
下面这个servlet,没有调用resp.setContentType方法,也没有调用new String(s.getBytes("iso8859-1"), "gbk");这句话。
结果网页上显示的是正确的中文。为什么呢?
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestInitServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter out = resp.getWriter();
String s = this.getInitParameter("name");
out.println(s);
out.close();
}
}

下面来分析:
web.xml确实以gbk编码存储到硬盘上的。用ultraedit软件打开它,切换到16进制表示方式,可以看到ced2 c3c7这几个字节
但tomcat默认把它当作iso8859-1的文本来读入内存的。
iso8859-1的特点是一个字节表示一个字符。
而且iso8859-1的编码和unicode的代码点是兼容的。每个iso8859-1的编码(一个字节)的前面再加一个字节0x00,就成了unicode代码点。
可以到unicode官方网站验证iso8859-1的编码和unicode的代码点之间的对应关系。
http://www.unicode.org/Public/MAPPINGS/
或者查看已经下载下来的文件:《ISOIEC 8859-1与Unicode的关系.mht》
而普通字符的代码点就是utf-16编码。
所以,当tomcat默认把它当作iso8859-1的文本来读入内存时候,会把每个字节当作一个iso8859-1字符,然后在内存里表示为这个字符的utf-16编码。所以这个时候,内存里面局部变量s指向了一个字符串对象,这个对象是utf-16编码,即s00ce 00d2 00c3 00c7

内存里表示成什么内容很关键。这时内存里面的字符串对象 和执行这句话String s = "我们";得到的字符串对象已经完全不同了。

接下来的这句话
out.println(s);
把这个字符串传到客户端浏览器。JVM这里可不是把内存里面的内容传到浏览器。JVM必须以某种编码形式来传递这个字符串。因为没有指明到底以什么编码来传递。所以默认是iso8859-1。
所以tomcat帮忙调用s.getBytes("iso8859-1"),得到4个字节,ce d2 c3 c7,让后把这4个字节传给客户端。
客户端浏览器以为接受到的是gbk的编码,所以默认按照gbk编码组装成字符串

这个servlet可以用下面的程序来模拟:
import java.io.UnsupportedEncodingException;

public class Test2 {
public static void main(String[] args) throws UnsupportedEncodingException {
// 读入web.xml,内存里面的unicode编码是这些字节内容:
byte[] bytes = new byte[4];
bytes[0] = (byte) 0xce;
bytes[1] = (byte) 0xd2;
bytes[2] = (byte) 0xc3;
bytes[3] = (byte) 0xc7;
String s = new String(bytes, "iso8859-1");
System.out.println(getUnicodeFromStr(s));
//默认先编码成iso8859-1,再传到客户端
byte[] bytes1 = s.getBytes("iso8859-1");
for (int i = 0; i < bytes1.length; i++) {
System.out.print(String.format("%1$02x", bytes1[i]) + " ");
}
System.out.println();
//客户端浏览器按照GBK编码组装成字符
System.out.println(new String(bytes, "GBK"));
}
public static String getUnicodeFromStr(String s) {
String retS = "";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
retS += String.format("%1$04x", (int) c) + " ";
}
return retS;
}
}


5 情况2
下面这个servlet,调用了resp.setContentType方法,但没有调用new String(s.getBytes("iso8859-1"), "gbk");这句话。
结果网页上显示的是乱码。为什么呢?

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestInitServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=gbk");
PrintWriter out = resp.getWriter();
String s = this.getInitParameter("name");
out.println(s);
out.close();
}
}
这种情形和上种情形只有一个地方不同,就是加了resp.setContentType("text/html;charset=gbk");
这句话。这句话的意思是说传到客户端之前,先按照gbk编码。

内存里面现在是这样的。s00ce 00d2 00c3 00c7,把这个字符串对象再编码成gbk是错误的。因为,在unicode中,00ce所代表的是iso8859-1那个字符,在gbk编码表里面并没有收录进来,所以没法编码成gbk,而且如果找不到匹配的字符,jvm不会报错,而是返回错误的结果。3f 3f 3f 3f。咱们希望它是ce d2 c3 c7。

如果内存里面是s62 11 4e ec, 再编码成gbk,才会得到我们想要的正确的结果。ced2 c3c7

好了,tomcat把错误的结果,3f 3f 3f 3f传给客户端浏览器。浏览器再把3f 3f 3f 3f这四个字节按照gbk组装成字符串显示出来,当然是乱码了。因为gbk编码表里面没有任何一个字符的编码是3f3f。

你就是尝试设置浏览器的字符编码设置也没用。比如设置为iso8859-1。首先iso8859-1没有定义3f,因为要和ascii兼容。而ascii字符集里规定,3f是“?”这个字符。
这个servlet可以用下面的程序来模拟:
import java.io.UnsupportedEncodingException;

public class Test3 {
public static void main(String[] args) throws UnsupportedEncodingException {
// 读入web.xml,内存里面的unicode编码是这些字节内容:
byte[] bytes = new byte[4];
bytes[0] = (byte) 0xce;
bytes[1] = (byte) 0xd2;
bytes[2] = (byte) 0xc3;
bytes[3] = (byte) 0xc7;

String s = new String(bytes, "iso8859-1");
// 打出来当然是乱码
System.out.println(s);
System.out.println(getUnicodeFromStr(s));

System.out.println("--------错误的做法----------");

// 先编码成GBK,再传到客户端
byte[] bytes_error = s.getBytes("GBK");
for (int i = 0; i < bytes_error.length; i++) {
//System.out.println(Integer.toHexString(bytes_error[i]));
System.out.print(String.format("%1$02x", bytes_error[i]) + " ");
}
// 浏览器根据这些字节组装成gbk:
String s_error = new String(bytes_error, "GBK");
System.out.println(s_error);

}
public static String getUnicodeFromStr(String s) {
String retS = "";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
retS += String.format("%1$04x", (int) c) + " ";
}
return retS;
}
}


6 情况3
下面这个servlet,调用了resp.setContentType方法,也调用了new String(s.getBytes("iso8859-1"), "gbk");这句话。
结果网页上显示的是正确的中文。为什么呢?

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestInitServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=gbk");
PrintWriter out = resp.getWriter();
String s = this.getInitParameter("name");
String ss = new String(s.getBytes("iso8859-1"), "gbk");
out.println(ss);
out.close();
}
}
这种情况,多了两条关键的语句
resp.setContentType("text/html;charset=gbk");
String ss = new String(s.getBytes("iso8859-1"), "gbk");

刚开始,内存里面是s00ce 00d2 00c3 00c7
s.getBytes("iso8859-1")会得到ce d2 c3 c7
这4个字节正是我们想要的gbk编码。咱们的目的是把这四个字节ce d2 c3 c7传到客户端。
如果你直接调用out.println(s);那就达不到目的。前面一种情况已经分析过了。

String ss = new String(s.getBytes("iso8859-1"), "gbk");
这句话是把ce d2 c3 c7这四个字按照gbk编码节组装成字符串。在gbk编码表里面,ced2代表我们的我字,c3c7代表我们的们字。字符串能够正确组装。但正确组装的这个字符串,内存里面要表示为这两个字符的utf-16编码。即内存里面的局部变量s指向的字符串对象为:ss62 11 4e ec

看看这两个字符串的不同:
s00ce 00d2 00c3 00c7
ss62 11 4e ec
s代表的是4个西欧字符
ss代表的是2个汉字字符。

当执行这句话的时候,
out.println(ss);
由于resp.setContentType("text/html;charset=gbk");这句话的作用,
tomcat会把ss代表的两个汉字字符编码成gbk编码。ce d2 c3 c7
如果没有resp.setContentType方法调用,则会被编码成iso8859-1.

最后,ce d2 c3 c7这四个字节传到浏览器,浏览器把这按照gbk编码组装。所以我们看到了正确的结果

你可能感兴趣的:(java,tomcat,J2SE)