J2EE Web组件中中文及相关的问题
作者:whodsow(原作)
“与C/C++不同,Java中的字符数据是16位无符号型数据,它表示Unicode集,而不仅仅是ASCII集”①。这是一个很好的做法,它解决了www上更多的程序设计问题,比如说低成本的国际化(International),然而用16位的字符,却带来了浪费,毕竟Java所处理的信息,绝大多数都是英文,对它们来说7位的ACSII码已经足够了,而Unicode却需要双倍的空间,所以Java的这种兼顾各种语言的做法却是与存储资源及效率的妥协。而对中国的Java程序员(特别是初级的)来说,Java采用Unicode字符,却给我们带来了尴尬甚至噩梦——Web页面上显示的不是中文,而是乱码。
一、 常见字符集简介
字符集就是字符内码到字符的表现形式之间的映射的集合。ASCII字符A是就内码0x41的表现形式,所以在很多程序语言中,字符变量和整型变量仅在一念之差。
1. ISO8859系列
ISO8859包括诸如ISO8859-1,ISO8859-2之类的一系列字符集,它们都是8位的字符集,0~0x7F仍与ASCII字符集保持兼容,大于0x7F的是各种拉丁字符或欧洲字符的扩展。
2. GB2312字符集
如果像ISO8859系列一样,大于0x7F的字符用来表示汉字,则最多表示128个,这显然不够,于是就有了GB2312标准所产生的字符集,如果当前字节(8 bit)小于0X80,则仍当它为英文字符;如果它大于等于0x80,则它和紧接着它的下一个字节构成一个汉字字符,这样,GB2312字符集可包含大约4000多个常用简体汉字和其他汉字中的特殊符号(如①㈠之类)。其他类似的汉字字符集还有GBK(GB2312的扩展),GB18030,Big5(繁,台湾省用),详细规范介绍可参考:http://www.unihan.com.cn/cjk/ana17.htm
3. Unicode字符集
Unicode字符最初是16位的(出于需要,后来增加了代用对),它和7位的US-ASCII保持兼容,MS的Windows NT/2000/XP和Sun的Java都用它作为默认的字符集,它最初是美国商务联盟的事实上的标准,它遵循国际通用字符(UCS)集标准:ISO/IEC 10646。Unicode的主要目标是提供一个“通用字符集”,这个通用字符集包括世界上所有的语言,字母和文字,所以在Unicode字符集中,不光“I”是字母,“我”也是字母,在写Java时也可以“int 我是中国人 = 0xff;”。毕竟16位的Unicode字符集最多只有216= 65536个字符,还不足以在实际应用中表示所有的字符,而且在以英文为主要信息的互联网时代,它的使用、存储与传输,都极其浪费空间,所以在此基础上出现了UTF-8(Unicode Transformation Form 8-bit form)和UTF-16这两种对Unicode字符编码的规范,在UTF-8中,属于US-ASCII中的字符,仍用一个字节表示,且和US-ASCII兼容,编码其他的字符,则用1(大于0x7F部分)到3个字节。UTF-8的变长性和复杂性,对非ASCII的字符,就不大友好了,也开始违背了Unicode的初衷。而UTF-16则是很简单的编码方式,它完全遵循Unicode标准,用16位的定长空间来表示部分Unicode字符集。关于Unicode的更多规范,请访问Unicode联盟站点:http://www.unicode.org,UTF-8和UTF-16分别定义在IETF的RFC 2279和RFC 2781中,可以通过http://www.ietf.org/rfc2279.txt或http://www.ietf.org/rfc2781.txt访问它们。
一般情况下,字符集名称是大小写不敏感的,所以GB2312也可以写作gb2312或Gb2312。
二、 乱码带来的尴尬
1. 先看一个JSP
JSP(Java Server Page)的实质还是一个Servlet,所以用JSP,也可以说明Servlet中的一些问题,就一般而言,JSP代码比Servlet代码还要简单。
我们先用JSP来做一个实验,下面的这个JSP文件中含有常量字符串“我是中国人”,看看它在浏览器的输出是否会是乱码?
<%-- discomfiture.jsp --%>
<%
String str = "我是中国人";
System.out.println(str);
out.println(str);
%>
从浏览器打开它,并没有乱码,显示的就是“我是中国人”这个字符串。先别乐,再看看服务器的输出窗口吧,如图2-1,服务器监视窗口输出了乱码(红色下划线标出)。
图 2-1 服务器窗口中输出的乱码
虽说只是在服务器端出现了乱码,而客户端浏览器是完全正确显示的,但这里很明显出了什么问题,否则都两边应该是正确的输出。
服务器端输出了乱码,说明服务器Java虚拟机(Java Virtual Machine, JVM)没有“得到”正确的字符串。为了保证JVM能够正确“得到”我们指出的含中文的常量字符串,我们可以直接用字符的Unicode内码代替字符串中的字符,就像
String str=”I am Chinese”;
用
String str = “\u0049\u0020\u0061\u006D\u0020\u0043\u0068\u006E\u0065\u0073\u0065”;
代替一样。明确给JVM指出这些字符串,是否还会出现乱码呢?要得到一个字符的Unicode内码是件很容易的事,Java和JavaScrtipt的字符都是用Unicode字符集的。先看看输出Unicode字符内码的Java程序:
public class getCode
{
public static void main(String args[])
{
char chs[] = args[0].toCharArray();
for(int i = 0; i < chs.length; i++){
System.out.println(chs[i] + " = " + (int)chs[i]);
}
System.out.println(args[0]);
}
}
编译并执行它,结果如图
不过JavaScript用起来,怎么也比Java来得快,这里也介绍一段JavaScript代码:
<script>
var str = "我是中国人";
for(var i = 0; i < str.length; i++)
{
document.wirte(str.charAt(i) + " = " + str.charCodeAt(i) + "<br>");
}
document.write(str);
</script>
保存为HTML文件,输出如下图:
图 2-2 JavaScript在IE6.0中输出
现在替换discomfiture.jsp中的中文字符串:
<%-- discomfiture1.jsp --%>
<%
String str = "\u6211\u662F\u4E2D\u56FD\u4EBA";
System.out.println(str);
out.println(str);
%>
实验得到了希望的结果,服务器端输出窗口正确输出了字符串,可客户端浏览器却又输出了乱码,如下图
图 2-3 JSP在IE6.0中出现了乱码
图 2-4 客户端刷新两次的服务器窗口的输出
因为直接使用的Unicode码生成的字符串,保证了在JSP生成的Servlet discomfiture1$jsp中,字符串str的值一定是“我是中国人”,而服务器窗口的输出也证实了这一点,那么也就是说在Servlet discomfiture$jsp中str的值并不是“我是中国人”,因为它在服务器窗口中输出了乱码,如图 2-1可以发现,当时输出了10个字符,即str的长度是10,并不是5。可为什么在浏览器却好好地得到了这个字符的输出呢? 先简要明白两个概念:编码与解码。
2. 编码与解码
编码(Encode)和解码(Decode)是两个相反的动作。编码是把字符按照某种映射标准(字符集),转换成字节,这时我们把执行编码动作时所采用的标准叫编码(encoding)。如我们对Unicode字符串
”我是中国人”
按照GB2312标准编码(byte bsg[] = ”我是中国人”.getBytes(“GB2312”);),就可以得到一个字节序列(bytes sequence),用十六进制的码值表示:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
按照UTF-8标准编码(byte bsu[] = ”我是中国人”.getBytes(“UTF-8”);),就可以得到字节序列:
0xE60x880x910xE60x980xAF0xE40xB80xAD0xE50x9B0xBD0xE40xBA0xBA
而解码则是将字节序列按照某种字符标准(解码,decoding),转换成字符串。如我们对字节序列:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
按照GB2312解码(new String(bsg,”GB2312”),或对字节序列:
0xE60x880x910xE60x980xAF0xE40xB80xAD0xE50x9B0xBD0xE40xBA0xBA
按照UTF-8解码(new String(bsu,”UTF-8”),均可得到字符串”我是中国人”,但是如果我们对GB2312编码的字节序列用UTF-8解码,这就乱了套,所得的字符串明显是错误的乱码。
让我们来看一个试验。
import java.io.UnsupportedEncodingException;
public class u2g
{
public static void main(String args[])throws UnsupportedEncodingException
{
String str = args[0];
char chs[] = str.toCharArray();
System.out.println("Unicode characters:");
for(int i = 0; i < chs.length; i++)
System.out.print(chs[i] + " = " + (int)chs[i] + ";");
System.out.println();
String messages[] = {
"Encodes this String into a sequence of bytes using the" +
"\nplatform's default charset.",
"Encodes this String into a sequence of bytes using gb2312.",
"Encodes this String into a sequence of bytes using utf-8."};
String encodings[] = {null,"gb2312","utf-8"};
byte bs[][] = new byte[3][];
for(int h = 0; h < messages.length; h++){
System.out.print(messages[h]);
if(encodings[h] == null)bs[h] = str.getBytes();
else bs[h] = str.getBytes(encodings[h]);
for(int l = 0; l < bs[h].length; l++){
if(l % 4 == 0)System.out.println();
System.out.print("byte[" + l + "] = " +
Integer.toHexString(bs[h][l] & 0xff) + ";");
}
System.out.println();
}
System.out.println("Decodes the sequence of bytes using corresponding encoding.");
for(int i = 0; i < bs.length; i++){
if(encodings[i] == null)System.out.println(new String(bs[i]));
else System.out.println(new String(bs[i], encodings[i]));
}
String messages1[] = {
"Decodes the sequence of bytes encoded by gb2312 into a string\nusing utf-8.",
"Decodes the sequence of bytes encoded by utf-8 into a string\nusing gb2312."};
for(int h = 0; h < 2; h ++){
System.out.println(messages1[h]);
str = new String(bs[h+1], encodings[h == 0 ? 2 : 1]);
chs = str.toCharArray();
System.out.print("Unicode characters:");
for(int i = 0; i < chs.length; i++)
{
if(i % 4 == 0)System.out.println();
System.out.print(chs[i] + " = " + (int)chs[i] + ";");
}
System.out.println();
}
System.out.println("The default encoding of system is " +
System.getProperty("file.encoding"));
}
}
JVM输出如图 2-5所示,很明显,对用UTF-8编码的字节流,用GB2312编码是彻底失败了,我们什么字符也没得到。我所使用的系统是MS Windows 2000 Server,默认字符集是GBK,这个实验也可以看出GBK兼容GB2312。
3. Java文件与编码
Java运用得如此广泛,以致于Java文件可能是采用任意一种字符编码的,如果不知道Java文件的编码标准是什么,就可能给我们的javac MyClass.java带来尴尬。所有文件的储存是都是字节的储存,在磁盘上保留的并不是文件的字
图 2-5 JVM输出
符而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成字节序列,所以读取文件又可能涉及到字节解码到字符,如果javac在读取java文件时,没有采取正确的decoding,就如同我们用UTF-8对GB2312字节流解码样,如果Java源程序中存在字符串常量(String str = “我是中国人”;),而这些常量又是非英文字符的话,那么javac将不能正确解码而形成乱码,甚至javac报Java文件语法错误。由于java.exe的options并没有关于encoding或decoding的选项,所以可以肯定在javac MyClass.java生成的class文件(byte code)中,字符串常量在编译Java源文件时,就被按照某种固定的编码标准编码在class文件中,按JVM(Java Virtual Machine, Java虚拟机)的规范,这个固定的编码就是UTF-8,在class文件中使用CONSTANT_Utf8_info结构表示常量字符串值。图 2-6就是用Dos命令debug.exe查看一个class文件(debug.exe不直接支持扩展名大于三个ACSII字符的文件,将Show.class拷贝到Show.txt),用红线标记出的正是字符串”我是中国人”的UTF-8编码结果。
图 2-6 用debug查看class文件
让我们再来做一些实验吧。
public class Show
{
static
{
String str = "我是中国人";
System.out.println(str);
}
public static void main(String args[]){}
}
我使用的操作系统的default charset是GBK,所以我的Show.java存储时的编码也是GBK,而javac的default decoding与系统所使用的decoding相同,也就是说我不必给javac的options添加”-encoding GBK”,javac也能对正确Show.java解码。可是如果我添加了”-encoding UTF-8”或” -encoding ISO8859-1”将会怎样呢,你可能已经知道了一些结果,如图 7-2,也许你在那里面发现了眼熟的东西,但我要说的并不是和图 2-5里面相同的乱码,为什么不看看图 2-1呢,但请不要把两个乱码字符串进行比较,因为它们本来就是不正确的字符串,对本来就不正确的东西进行比较,很多时候都是没有意义的,你根本就不能确认它们是什么,即使它们看起来是显示相同的字符串。
图 2-7对javac Show.java采取不同的解码
先不要着急,你可以先思考一下我说的东西,再进行下面的一些实验。
用记事本打开Show.java,然后用同名另存它,不过在另存时编码选择“UTF-8”(Win 2000及更高版本的Windows中的记事本,都可以这么做)。你已经知道了,我们现在该用javac –encoding UTF-8 Show.java命令行了,可是我们又遇到尴尬了,如图 2-8所示
图 2-8编译报错——我们又一次尴尬了
javac报出了我的语法错误,这可有些让人着实想不通了,找来找去都找不到错误出处,是的,你找不到,不管你是用记事本打开这个Show.java还是用MS的InterDev中的VJ++都找不出错误所在,一切都好好的,报错所指的第一行中,根本就不存在那两个字符。还好一切都逃不过debug.exe的眼睛,原来Show.java文件的前三个字节是0xEF、0xBB和0xBF,虽然不懂它的意思,但还是可以理解的,因为在未打开文件前,Windows不知道它所要处理的文件是用什么编码的,这不像在Internet上,我们可以从信息的附加信息中知道信息所使用的编码,而Windows却不能(也许它应该这么做了),大概是为了标识该文件是用UTF-8编码的吧,系统只好在文件前加上了那三个字节(我们实在无法就凭着几个字节就能判断或确定出它是用什么编码编码的,XML中不也是要指定encoding的么)。可这下javac就不同意了(也许这种做法是MS自己想出来的吧),它仍将文件的前三个字节当作有效数据而不是特殊的标识,这下就错了。用debug把前面那三个字节删除(见图 2 –11)就行了,但当我们用GBK或GB2312解码时,javac报语法错误了,这是应该的;用ISO8859-1虽通过了编译,却又出现了乱码,这也是应该的。如图2-9
图 2-9 Show. java是用UTF-8编码的
我们还可以得出一个结论的:如果Java文件中不存在非英文字符的字符串常量,我们是有理由不去关心javac的options中的-encoding了,不管这个Java类在将来是否会去处理非英文字符,因为我们已经完全生成了正确的class,剩下的有关编码或decoding的事,为什么不交给它去做呢,说到这里,我们有必要再做一个实验。
4. 文件的编码
这个实验是用段Java小程序读取中文文件的,不过在这之前,你最好先看看这几个类的Java Document吧,java.io.FileInputStream, java.io.InputStreamReader, java.io.BufferedReader,我从Sun的《JavaTM 2 SDK, Standard Edition Documentation Version 1.4.0》中摘抄了一点很令我们振奋的Document:
An InputStreamReader is a bridge from byte streams to character streams: It reads bytes and decodes them into characters using a specified charset. The charset that it uses may be specified by name or may be given explicitly, or the platform's default charset may be accepted.
Each invocation of one of an InputStreamReader's read() methods may cause one or more bytes to be read from the underlying byte-input stream. To enable the efficient conversion of bytes to characters, more bytes may be read ahead from the underlying stream than are necessary to satisfy the current read operation.
…
public int read()
throws IOException
Read a single character.
通过InputStreamReader来读取文件或其他输入流,我们已经不直接读取到byte了,而我们所读取到的char也是InputStreamReader使用“自以为正确的解码”来解码字节流(type stream)所得的结果,因为如果我们在构造InputStreamReader对象时,没有使用参数charset来指定字节流的编码(编码字符流时所采用的encoding),它将使用缺省的编码(encoding)来作为解码(decoding);OutputStreamWriter有着相同的机制却执行着相反的动作。Utf_8File.java的源代码:
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
public class Utf_8File
{
public static void main(String args[])
throws FileNotFoundException,IOException,UnsupportedEncodingException
{
if(args.length < 3){
System.out.println("cmd encoding decoding file");
return;
}
FileInputStream fis = new FileInputStream(args[2]);
InputStreamReader isr = new InputStreamReader(fis, args[0]);
BufferedReader br = new BufferedReader(isr);
String str;
System.out.println("File content:");
while((str = br.readLine()) != null)
System.out.println(str);
br.close();
File f = new File(args[2]);
fis = new FileInputStream(args[2]);
byte bs[] = new byte[(int)f.length()];
int b, index = 0;
while((b = fis.read()) != -1)
bs[index++] = (byte)b;
fis.close();
System.out.println("File content:");
System.out.print(new String(bs, args[1]));
}
}
用记事本编辑一个Show.txt文件,里面只有五个汉字:我是中国人,保存的时候用UTF-8编码,如图 2-10,请注意我的每一个命令行。
不错,第一次读取Show.txt时出现了乱码,都是文件前面那三个讨厌的流氓字节惹的祸,我们用debug把前面那三个字节删除,如图 2-11,第二次就好好的了。除此之外我们好像并没有发现乱码,但你也会发现“java Utf_8File utf-8 gb2312 Show.txt”的第二个“File content:”后什么也没有,我们用GB2312解码UTF-8编码的字节流是失败了(我并没有说对其他的UTF-8编码字节流用GB2312解码也不会得到什么字符)。
图 2-10 Java程序读取Show.txt
你很细心,发现了图 2-10中的那个MalformedInputException,这是可以解释的,正是因为“java Utf_8File utf-8 gb2312 Show.txt”的第二个“File content:”后什么也没有,说过的,我们在这里没有得到任何字符,也许在其他地方可能会得到字符,但那是乱码(Malformed Character)。
图 2-12 我们的确收到了这个Exception
现在,你也可以明白为什么System.out和System.in可以乖乖地干活了吧,也可以知道我们以往不考虑这些东西好像也没出错的原因了吧,JVM用缺省的encoding/decoding帮我们做好了这些。那好,为什么不用GB2312编码另存Show.txt试试。
现在我们可以直面图 2-1所遇的尴尬了。
5. JSP文件与编码
是的,我的JSP文件的编码是GB2312,如果我使用的Tomcat编译这个JSP文件没有使用GB2312作为decoding,又或是discomfiture$jsp被编译时,没有使用GB2312作为decoding,那么含非英文字符的常量字符串将不会被正确编译,也就会出现了乱码,因为我们可以肯定System.out是不会把正确有字符输出错误的。其实问题没有这么多,JSP引擎(JSP engine)在将JSP文件编译成Java文件后,javac编译这个Java文件时所用的encoding参数是UTF-8,也就是说JSP引擎所生成discomfiture$jsp.javap,而这个文件的存储时的编码(encoding)是UTF-8就行了,而我们所在乎的就只是JSP引擎编译JSP文件所使用的编码。
先让JSP引擎正确编译discomfiture.jsp,我们得看看discomfiture$jsp.java到底是不是这么回事,图 2-13.a是用记事本打开的discomfiture$jsp.java的拷贝,图 2-13.b和2-13.c都是用debug查看discomfiture$jsp.java的拷贝,记事本和debug都是很可爱的小东西,我们可以一直信任她们。
图 2-13.a这儿没有乱码
图 2-13.b这儿没有流氓
让JSP$jsp.java采用固定的UTF-8编码是很明智的,因为我们可以很容易控制JSP文件的存储编码,而又可以轻松地把各种存储的编码告诉给JSP引擎,同时也完全照顾到了英文字符和非英文字符的利益,也许UTF-8使用起来较复杂,但JSP$jsp.java仅仅是一个temp,我们应该可以容忍的。
是的,我们可以很容易把JSP文件的编码告诉给JSP引擎,这样在编译JSP文件时,含非英文字符的常量字符串就不会再被编译错了。
图 2-14 海纳百川
JSP文件中的page指令中的pageEncoding属性,就把JSP文件的编码告诉给了JSP引擎,比如:
<%@ page pageEncoding=”gb2312” %>
page指令中的属性都是可选属性,pageEnoding的默认值是contentType中的charset的值,所以如果我们使用了:
<%@ page contentType=”text/html;charset=gb2312” %>
而没有明确指出pageEncoding,则pageEncoding的值也是GB2312了,所以我们常常没有在意这个属性,JSP文件中的含非英文字符的常量字符串也没有编译错。如果连这个属性也没有设置,那可有点糟糕,contentType的默认值是text/html;charset=ISO8859-1,我们的JSP文件的编码也成了ISO8859-1,所以就出来了图 2-1。可那时候浏览器为什么乖乖的呢?让我们看看”我是中国人”的流程吧,图 2-15
JSP引擎对一个已知编码的JSP文件如何处理对我们来说可以当作透明的,我们也完全信任它会正确处理的。我们的discomfiture.jsp文件被以GBK字节流保存着,常量字符串”我是中国人”,被编码成:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
当JSP引擎读取这个文件的字节流时,因为我们并没有指定pageEncoding或contentType的charset,JSP文件的编码被认为是ISO8859-1,则JSP引擎也用ISO8859-1对这些字节解码。按照ISO8859-1的标准,每个字节被解码成一个数值相等的字符,这时JSP引擎认为这个常量字符串(注意Java采用的是Unicode字符集)是:
\u00CE\u00D2\u00CA\u00C7\u00D6\u00D0\u00B9\u00FA\u00C8\u00CB
一直到Servlet discomfiture$jsp的执行时,都是如此。当Servlet把这个字符串用JVM在服务器本地输出时,被按照缺省GBK解码,然后给底层操作系统输出GBK字节流,这就乱码了。我们可以用下面这段JSP代码证明这个推测:
<%-- discomfiture2.jsp --%>
<%
String str = "我是中国人";
System.out.println(str);
char chs[] = str.toCharArray();
for(int i = 0; i < chs.length; i++)
{
System.out.print(Integer.toHexString((int)chs[i]));
System.out.print(" ");
}
out.println(str);
%>
服务器端输出如图 2-16
图 2-16 字符串中每个字符的Unicode码
所以我们在discomfiture1.jsp中直接给字符串中的每个字符用Unicode码赋值,Servlet中也就是正确的字符串”我是中国人”了,也能正确输出到System.out。当我们没有设JSP的page指令中的contentType时,Servlet输出到网络的字节流按缺省的编码ISO8859-1编码,这种编码方式直接丢弃Unicode字符的高位为0的字节,将低位字节写入输出流中(当高位不为0时,不能正确编码则将字符’\u3f’写入字节流中,所以我们得到了字符’?’),则在客户端,我们又得到了字节流:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
在浏览器输出这些字节流所代表的字符串(浏览器输出的当然是字符不会是字节了)时,被按照系统缺省编码方式编码,我们又得到了字符串”我是中国人”,其实这是一种错误与错误的耦合。现在我们也就不难理解图 2-3的乱码——五个’?’了。
三、 向服务器发送中文
虽然大家一般都不用JSP甚至Servlet来处理处理客户提交的数据或访问请求参数,但JSP的使用或更新总是比Servlet或 JavaBean来得方便(至少在Tomcat 4.0.4中是如此,因为我们常常不得不为修改了Servlet或JavaBean而重启服务器),所以在这里我们还是要用JSP来访问请求参数了。
不管是在JSP还是在Servlet中,我们都是用ServletRequest(或其子类)的方法getParameter(String name)来访问请求参数的,这个方法返回的是String,也就是说我们能得到的是已经对Internet传来的字节流解码所得的字符串。如果服务器能对这些字节流进行正确的解码,那将是件完美的事。其实说来也很简单,要做到这一点只需要服务器知道这些字节流在客户端是用什么编码进行编码也就行了。如图 2-17我们希望decoding==encoding。
图 3-1 数据从浏览器到服务器
可理想与现实往往是没有交集的,我们怎么也不能让服务器知道这些字节到底是用什么编码被编码的,即使在万维网相关技术的主要设计组织W3C(World Wide Web Consortium)的《HTML 4.01 Specification》和Internet工程任务组(Internet Engineering Task Force, IETF)定义HTTP1.1的rfc2616(《Hypertext Transfer Protocol -- HTTP/1.1》)中也没有发现有什么相关的推荐办法,能在现有的浏览器和HTML Web页下让服务器知道这个编码是什么(如果你知道该怎么做,一定请记得告诉我),所以在缺省情况下,Tomcat 4.0.4又一厢情愿的用起了ISO8859-1来对客户端提交的数据进行解码。如图 2-17,decoding=ISO8859-1,可如果encoding!=ISO8859-1呢,很明显这就是一个数据错误传输了。注意,我们这里所讲的数据,仅仅是客户端给服务器发送的数据中的实体(Entity Body)中的数据。
1. 谁决定了Encoding
谁决定了浏览器的当前Web页通过Form向Internet(最终的对象当然是服务器)发送数据字节流的编码呢?当然是浏览器了。那浏览器又是靠什么决定这个编码的呢?那是继承的浏览器解码当前页(它当然也要对当前Web页解码了,别忘了任何文件或Internet上的元数据都是字节)所使用的decoding了,其实说是继承也不全对,后面你就会发现的。
2. 靠什么决定了Encoding
大约是这六个方面来的信息使浏览器决定用什么encoding:
1) XSL所决定的
2) 实体(Entity Body)中的特殊标记
3) 用户手动对该Web页设置的decoding
4) 响应头(Response Header Field)中的Content-Type
5) HTML元素META中的charset
6) 浏览器以前所用的decoding
它们的优先级可能会因为浏览器的不同而不同,但在IE6.0中是递减的,微软这种做法确实暧昧、耐人寻味,难怪它会在浏览器大战中取得胜利。下面仅对这六点一一解释。
XSL(eXtensible Stylesheet Language, 可扩展样式单语言)可以方便地将XML(eXtensible Markup Language, 可扩展标记语言)转换为其他很多种内容,我们这里只关心它把XML转换为HTML,而对浏览器编码设置的影响。如果该XSL遵循 W3C在1998年发布的有关XSL的第一个工作草案标准(设置XSL文件中的xmlns:xsl="http://www.w3.org/TR/WD-xsl"),则可以通过在XSL中添加元素meta,并作相类似的设置达到设置浏览器编码的目的:
<meta http-equiv="Content-Type" content="text/html; charset=GB2312"/>
如果该XSL是事实上的XSLT(eXtensible Stylesheet Language Transformation, 可扩展样式单转换语言),即xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”,则浏览器编码无条件使用UTF=16,即使在XSL文件中添加meta元素来设置编码都将被忽略。
实体是指服务器给浏览器返回的数据中的实体,也可以简单理解为返回数据中除附加头和空白行之外的所有数据。在前面说过,Windows 2000 Server(我实验所用的操作系统)会在以UTF-8编码的文件前加三个流氓字节(0xEF0xBB0xBF),如果当前Web页是静态资源,则服务器会不加任何处理直接把这个Web 页返回到客户端,如果这个静态Web页也是在Windows 2000 Server中生成的话,那么实体的最初三个字节将是0xEF0xBB0xBF,浏览器很快检测到这三个字节,于是就用UTF-8对正确解码,后面那四点都会被忽略。
手动设置decoding就是在浏览器窗口中对编码进行设置,这是明确的告诉浏览器该Web页应该使用的编码,用户永远是对的。
Tomcat 4.0.4是不会在返回静态Web页设置响应头中的Content-Type,我们在JSP中所使用的:
<%@ page contentType="text/html;charset=gb2312"%>
或在Servlet中使用的:
response.setContentType(“text/html;charset=gb21312”);
就是对响应头(Response Header)中的Content-Type的设置,它的值遵循MIME(Multipurpose Internet Mail Extension protocol, 多用途的网际邮件扩充协议)规范,如图 3-2,请求JSP页面http://localhost/scqdac/t.jsp在客户端收到的所有数据,0x7d是实体中有效数据的长度。
图3-2服务器通过HTTP协议返回给浏览器的所有数据
由图中可以看出,响应头中的Content-Type与HTML中的meta的Content-Type完全无关,这是可以理解的。t.jsp的所有源代码如下:
<%@ page contentType="text/html;charset=gb2312"%><html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
</head>
<body>
</body>
</html>
JSP文件中的
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">。
已经没有什么意义了,没有谁会去理会它。关于响应头中的Content-Type,RFC2616中有详细的定义和说明,请参阅:http://www.ietf.org/rfc/rfc2616.txt。
我们也可以通过设置Web页中的HTML元素meta来达到设置Web页编码的目的:
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
关于HTML文档的字符设置(Document Character Set)请参阅《HTML 4.01 Specification》,http://www.w3c.org/TR/html401/html401.html(你同时还可以了解到HTML元素form的属性enctype的设置以及它的作用)。
如果实在找不到charset及相关的信息,浏览器就使用最近一次使用过的编码。
在前面提到了浏览器的编码并不完全继承于decoding,当解码Web页面所使用的decoding是UTF-16时,向服务器发送数据的实体还是使用UTF-8,至少在IE6.0的默认设置下是如此的。还有一个有趣的就是当encoding=ISO8859-1时,请求头(Request Header Field)
Content-Type:application/x-www-form-urlencoded
时,通过Form向服务器发送数据“我是中国人”(name=”text”)时,被编码成:
%26%2325105%3B%26%2326159%3B%26%2320013%3B%26%2322269%3B%
26%2320154%3B
这的确是一种有趣的编码方式,我们略加分析就可以发现这个逃逸(escape,这个术语的翻译有点搞笑,我喜欢它)字符串就是
我是中国人
那么在服务器端我们通过
request.getParameter(“text”);
所得到的字符串也将是”我是中国人”,这明显是SGML(Standard Generalized Markup Language, 标准通用标记语言)所采用的实体字符嘛,HTML当然也就能很好地处理它了,所以如果form属性action所对应的JSP中有:
<%=request.getParameter(“text”)%>
那么浏览器将重现“我是中国人”。所以我们可以由得它,当然也可以很容易地处理它,毕竟它是标准的东西。
3. 把字节串还给我们
服务器在没有得到我们的通知的情况下,自我主张地执行了类似
String str = new String(bytes,”ISO8859-1”);
的操作,而且还不让我们能够直接获得客户传输的字节串(Servlet API中没有这个方法)。但我们还是可以让ServletRequest把字节串还给我们,那就是执行它的逆运算,用ISO8859-1编码:
String str = request.getParameter(“text”);
byte bs[] = str.getBytes(“ISO8859-1”);
这时所得的bs,我们有足够的理由相信它完全就是客户端发给服务器的,因为用ISO8859-1对字节流解码,是不会失真的,它得到的字符串,所有的字符高位字节都等于0,也就是说用ISO8859-1对它编码,也不会丢失数据,我们将得到本来的字节串。
4. 重新解码
只要我们对这些字节串用正确的编码重新解码,我们将得到客户提交的真实字符,也许它们和客户端的字符的内码不同,但绝对是相同的字符。
String str = request.getParameter(“text”);
byte bs[] = str.getBytes(“ISO8859-1”);
String text = new String(bs, ”GBK”);
当然最简单和有效的莫过于:
request.setCharacterEncoding(“GBK”);
String text = request.getParameter(“text”);
有时我们在学习中没有使用String的第二个参数,直接使用
String text = new String(bs);
其实是我们系统的缺省编码是GBK,而String正是引用了这个缺省编码。
也许在这个时候,我们才真正地感觉到,如果能够知道客户端浏览器使用了什么编码,那将是多么愉快的事,可是我们不能。不要指望ServletRequest.getCharacterEncoding()能给你带来什么,如果没有在服务器端明确使用ServletRequest对象的方法:setCharacterEncoding(String encoding)设置该对象所描述的请求中的数据的编码,那么该对象的getCharacterEncoding()将返回null,而我们认为的完美组合:
request.setCharacterEncoding(request. getCharacterEncoding());
不管getCharacterEncoding()有没有帮助,都是没有意义的——你本就是从我这里知道的,我还用得着你告诉我么。
既然决定浏览器编码的六种方法中,有三种都可以被服务器所使用(但我们的确不屑于用第一种方法,尽管在IE面前它是最有效的)。如果向服务器提交数据的表单是包含在一个静态Web页面中的,那么我们就设置HTML元素meta的属性,如果该表单是包含在JSP的,我们就设置page指令中的contentType。那么在处理该表单所提交的数据时,我们可以用相应的编码对字节串重解码。但不要大意,这种方法并不是可完全信任的,因为我们的用户可能使用了六种方法中的第二种方法重置了浏览器的编码,幸运的是如果Web页中的不是所有的信息都是英文字符的话,用户还是不会无聊地执行这种非法操作,除非他真的想得到乱码。
5. 使用过滤器
实际中我们处理客户端数据时,大多数时候都是在JavaBean中实现的,我们当然可以在会话Bean中把decoding掺合进去,但没有谁愿意这么做,而事实上我们可能会有很多的Bean,这种做法是维护和更新所不能容许的。我们还可以在JSPs/Servlets中通过ServletRequest.setCharaterEncoding(String encoding)来设置,但在众多JSPs/Servlets中这么做也是件令人讨厌的事。最理想的办法是一劳永逸——只在一个地方进行编码和解码的处理,那就是在过滤器(Filter)中。我们只要在过滤器中对客户提交的数据正确解码了,就不用JSPs/Servlets/JavaBeans来操心了。我们的客户可能是大陆的,也可能是台湾省的,也就是说我们至少还得为区分GB2312和Big5来操心了,最理想的就是让它们提交上来的数据都是UTF-8编码的,那就不用决定decoding了,这里我们可以通过第四种方法来让服务器影响浏览器选择encoding,即设置响应头中的Content-Type,让所有的JSP和Servlet的响应实体的编码都是UTF-8,那么浏览器也就会选择我们所使用的UTF-8来编码提交的数据。
package filters;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class EncodingFilter implements Filter
{
private FilterConfig config;
private String defalutEncodeing;
public void init(FilterConfig config)
{
this.config = config;
defalutEncodeing = config.getInitParameter("encoding");
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException,ServletException
{
request.setCharacterEncoding(defalutEncodeing);
String uri = ((HttpServletRequest)request).getRequestURI();
if(uri.indexOf(".jsp") != -1 || uri.indexOf("servlet") != -1)
response.setContentType("text/html;charset=" + defalutEncodeing);
System.out.println("Filter set the encoding of the response to " +
response.getCharacterEncoding());
chain.doFilter(request, response);
}
public void destroy()
{
//...
}
}
在Context的web.xml中进行如下配置:
<filter>
<filter-name>Encoding Filter</filter-name>
<filter-class>filters.EncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Encoding Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
可用一个test.jsp测试却发现,浏览器的输出并没有像我们预想的那样好,test.jsp源代码如下:
<%@ page pageEncoding="GBK"%>
<%String str = "在JSP中,编码已经被过滤器设置成:" + response.getCharacterEncoding();
System.out.println(str);%>
<%=str%>
它的输出如图3-3和图3-4,在客户端出现了乱码是没办法的事,因为通过服务器的输出我们可以发现在过滤器中,编码的确已经被设置成UTF-8,但在JSP中,编码又被设置成了ISO-8859-1。用过滤器对JSP编码设置失败,所以JSP的输出也失败了,我们看到了乱码。
图 3-3 对过滤器对JSP文件预设置编码失败
图 3-4 服务器输出说明在JSP中编码又被设置为ISO8859-1
但一段Servlet代码却证明了过滤器本来已经成功进行了设置。
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintWriter;
public class encoding extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter out = response.getWriter();
String str = "在Servlet中,编码是:" + response.getCharacterEncoding();
System.out.println(str);
out.println(str);
out.flush();
}
}
你可以在有过滤器和没有过滤器的情况下做实验,结果应该发现在有过滤器状态下,服务器端和客户端均正确得到了UTF-8的输出,在没有过滤器的状态下,服务器端正确输出编码为ISO-8859-1,客户端却输出了乱码,不管是JSP时的乱码还是无过滤器Servlet的乱码,这些都是可以理解的,因为当没有对JSP明确设置page的contentType指令,JSP引擎自动把它设置为ISO-8859-1,而用ISO-8859-1来编码中文,当然只有得到乱码了。
现在可以说利用过滤器来设置响应实体编码,以达到控制浏览器提交数据的编码的希望是没有意义了,没有谁会用Servlet来输出含Form的HTML页。但在过滤器中设置用户提交的数据的编码码还是有意义的。对于JSP我们还是可以妥协到在每个JSP中来定义contentType,也许在一JSP文件中定义contentType,而其他JSP文件对这个文件使用静态包含是可行的。
<%--encoding.jsp--%>
<%@ page contentType=”text/html;charset=UTF-8”%>
静态包含
<%--form.jsp--%>
<%@ page pageEncoding="GBK"%>
<%@ include file="encoding.jsp"%>
<%String name = request.getParameter("name");%>
<html>
<head>
<title>Form</title>
</head>
<body>
<form method="POST" action="test.jsp">
<label>请输入您的姓名</label>
<input type="text" name="name" size="20">
<input type="submit" value="提交">
</form>
<%if(name != null && !name.equals("")){
//%><p>您的姓名是:<%=name%></p><%
}%>
</body>
</html>
在这里,我们对客户提交的数据的解码是在过滤器Encoding Filter中完成的。form.jsp中的<%@ page pageEncoding="GBK"%>是不能省略到encoding.jsp中去,很明显它只对使用了它的JSP文件有效,而且不会在include指令中传递到包含文件中去的。
6. URI的中文字符串
通常情况下,我们都会尽力避免在URI(Uniform Resource Indentifier,统一资源标识符,定义在RFC2396中)中出现非英文字符,但不是所有的时候都能避免,而且这种避免可能加大我们的开发成本或运行效率。如果想直接用含中文的URL(Uniform Resource Locator,统一资源定位符,定义在RFC1738中,是URI的子集)对服务器上的Web资源进行访问,那是不行的。比如:
http://localhost/我是中国人.html
是不能访问到服务器上的“我是中国人.html”,因为浏览器(我使用的是MSIE6.0b)会无条件对该URL用UTF-8来编码:
http://localhost/%E6%88%91%E6%98%AF%E4%B8%AD%E5%9B%BD%E4%BA%BA.
html
而服务器却用系统缺省编码来解码这个URL,即相当于执行了
java.net.URLDecoder.decoder(url);
这个动作。不过这个方法已经不被赞成使用了(Deprecated),而应该使用新的方法
public String decode(String url, String charset)
服务器使用缺省的GBK来解码URI,而浏览器却很难做到使用GBK来编码URI,JavaScript中有三个用来编码URI的全局函数
l encodeURI(uri)
l encodeURIComponent(uri)
l escape(uri)
前面两个函数出现在IE5.5+中,它们用UTF-8来对参数uri编码,并返回编码的字符串;最后那个函数已经不被赞成使用了(Deprecated),它直接使用“%”加字符的Unicode内码来表示字符,如
escape(“我是中国人”)=%u6211%u662F%u4E2D%u56FD%u4EBA
这样,我们只好借助java.net.URLEncoder来编码URI了,如
<%-- encodeURL.jsp--%>
<%String file = “我是中国人.html”;
url = java.net.URLEncoder.encode(file, "GBK");%>
<a href=”<%=url%>”><%=file%></a>
这样,我们就实现了访问文件名中含中文字符的Web文件。其实这种即增加开发成本,又牺牲服务器效率的做法是没有多大意义的,没有谁会故意非用个中文文件名不可。
在HttpServletRequest中,请求查询字符串(通过方法getQueryString()获得)即不是URI(通过方法getRequestURI()获得)的一部份,也不是URL(通过方法getRequestURIL()获得)的一部份,服务器使用URLDecoder解码URL时,丝毫不对它产生影响,可以说它是被独处理的,而我们前面所使用的过滤器Encoding Filter里面的
request.setCharacterEncoding(defalutEncodeing);
却会对它产生影响。让我们来看一个实验,该实验加载了过滤器Encoding Filter:
<script>
//浏览不会自动编码查询字符串中的非英文字符
function encodingHref(obj)
{
obj.href = encodeURI(obj.href);
}
</script>
<a href="test.jsp?name=胡洲" onclick="encodingHref(this)">go</a>
test.jsp的源代码如下:
<%@ page contentType="text/html;charset=GBK"%>
pathInfo = <%=request.getPathInfo()%><br>
pathTranslated = <%=request.getPathTranslated()%><br>
contextPath = <%=request.getContextPath()%><br>
queryString = <%=request.getQueryString()%><br>
requestURI = <%=request.getRequestURI()%><br>
requestURL = <%=request.getRequestURL()%><br>
servletPath = <%=request.getServletPath()%><br>
name = <%=request.getParameter("name")%>
输出如图3-5:
图3-5
输出的结果是我们希望的。我们还可以在服务器端使用JSP代码:
<a href=”test.jsp?name=<%=URLEncoder.encode(“胡洲”, “UTF-8”)%>”>go</a>
事先编码好请求的URI,使用JavaScript代码编码查询字符串和使用JSP代码比起来,两者各有所长。
四、 国际化
在前面第五小节中的form.jsp里,也许台湾省的客户更喜欢<label>中的提示信息是“請輸入您的姓名”,而美国客户喜欢“Please input your name”,我们可以通过国际化来满足他们的需求。
我们首先在webapp\WEB-INF\classes中创建三个区域相关的资源文件。中文简体版:
#LocalStrings_zh_CN.properties
page.title=国际化
form.label=请输入你的姓名
form.button=提交
中文繁体版:
#LocalStrings_zh_TW.properties
page.title=國際化
form.label=請輸入您的姓名
form.button=提交
英文版:
#LocalStrings_tw.properties
page.title=internationalization
form.label=Please enter your name
form.button=submit
LocalStrings_zh_CN.properties、LocalStrings_zh_TW.properties和LocalStrings_tw.properties这三个文件均以系统缺省的编码方式保存。form.jsp改成:
<%@ page contentType="text/html;charset=utf-8"%>
<%@ page import="beans.LocaleStrings"%>
<%LocalStrings lss = new LocalStrings("LocalStrings", request.getLocale());%>
<html>
<head>
<title><%=lss.getString("page.title")%></title>
</head>
<body>
<form method="POST" action="test.jsp">
<label><%=lss.getString("form.label")%></label>
<input type="text" name="name" size="20">
<input type="submit" value="<%=lss.getString("form.button")%>">
</form>
</body>
</html>
beans.LocalStrings的源文件如下:
package beans;
import java.util.ResourceBundle;
import java.io.UnsupportedEncodingException;
import java.util.Locale;
public class LocalStrings
{
protected ResourceBundle rb;
protected boolean encoded;
public LocalStrings(String baseName, Locale locale)
{
this.rb = ResourceBundle.getBundle(baseName, locale);
if(locale.getCountry().equals("CN") || locale.getCountry().equals("TW"))
encoded = true;
}
public String getString(String key)
{
String value = rb.getString(key);
if(encoded)
{
try
{
byte bs[] = value.getBytes("ISO-8859-1");
return new String(bs, "GBK");
}
catch(UnsupportedEncodingException uee)
{
return value;
}
}
return value;
}
}
从浏览器访问form.jsp,我们应该看到正确的输出,然后通过浏览的Internet选项,将浏览器的语言设置成“中文(台湾)[zh-tw]”,再访问form.jsp,我们就能看到繁体字的提示信息了;再将浏览器语言设置成“英语(美国)[en-us]”,再访问form.jsp。为什么要用LocalStrings到重新编码这些字符串呢,查看ResourceBundle的源程序可以发现,它读取文件中时,并没有对字符串进行编码,所以我们不得不在LocalStrings中对使用正确的编码对这些区域相关的字符串编码。
五、 一些建议
我们用J2EE(Java 2 Enterprise Edition/ Java 2 Platform, Enterprise Edition,Java 2企业版)开发的企业应用程序,可能在一些支持J2EE规范的服务器之间移植,而这些服务器的平台也可能不同。我们的客户的操作系统或所使用的浏览器,也有可能不同,所以我们应努力遵循一些建议:
l 从请求头中判断出用户的操作系统、浏览器和所使用的语言,而用适当的方法进行相应的响应头中的charset的设置。
l 尽量不在Servlet中使用含非英文字符的常量字符串。
l 对于JSP文件,只要页面中存在不被服务器忽略的含非英文字符的常量字符串,就应该对page指令中的pageEncoding属性进行相应的设置。
l 使用过滤器来设置通过
ServletRequest.setCharaterEncoding(String encoding);
来设置请求实体的编码,而不是在每个JSP或Servlet中设置。抛弃在Servlet设置content-Type的习惯,让过滤器根据不同的资源和用户对象通过
ServletResponse.setContentyType(String type);
来设置。
l 尽量使用UTF-8作编码,而不是GBK或GB2312。
l 充分考滤到底层数据库所使用的编码,它可能在应用程序的移植中带来麻烦。
l 不能简单有效地判断出用户浏览器编码提交的数据所使用的编码,是程序员的一大遗憾,如果W3C对现有的标准进行扩展,修改form元素的enctype,让这个属性也完全兼容MIME的charset,Web应用程序的开发,又少去了一些麻烦。
附录A. 本文所有的实验,环境配置如下:
l 操作系统 MS Windows 2000 Server
l 区域设置 中文(中国)
l 系统语言 中文(简体) 默认
l 浏览器 Microsoft Internet Explorer 6.0b
l Java虚拟机 J2SDK 1.4.0-b92
l Java服务器 Apache Tomcat 4.0.4
本文地址:http://com.8s8s.com/it/it15931.htm