乱码的原因很多,我以前帖过一个帖子,写乱码问题的,重帖一下,希望能有所帮助。
“与C/C++不同,Java中的字符数据是16位无符号型数据,它表示Unicode集,而不仅仅是ASCII集”①。这是一个很好的做法,它解决了www上更多的程序设计问题,比如说低成本的国际化(International),然而用16位的字符,却带来了浪费,毕竟Java所处理的信息,绝大多数都是英文,对它们来说7位的ACSII码已经足够了,而Unicode却需要双倍的空间,所以Java的这种兼顾各种语言的做法却是与存储资源及效率的妥协。而对中国的Java程序员(特别是初级的)来说,Java采用Unicode字符,却给我们带来了尴尬甚至噩梦——Web页面上显示的不是中文,而是乱码。
一、 常见字符集简介
字符集就是字符内码到字符的表现形式之间的映射的集合。ASCII字符A是就内码0x41的表现形式,所以在很多程序语言中,字符变量和整型变量仅在一念之差。
2. 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 + " = " + (int)chs);
}
System.out.println(args[0]);
}
}
编译并执行它,结果如图:
图 2-2 JVM输出
不过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 + " = " + (int)chs + ";");
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 == null)System.out.println(new String(bs));
else
System.out.println(new String(bs, encodings));
}
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 + " = " + (int)chs + ";");
}
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。
三、 向服务器发送中文
虽然大家一般都不用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了,其实说是继承也不全对,后面你就会发现的。
3. 靠什么决定了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,这个术语的翻译有点搞笑,我喜欢它)字符串就是
&#25105;&#26159;&#20013;&#22269;&#20154;
那么在服务器端我们通过
request.getParameter(“text”);
所得到的字符串也将是”&#25105;&#26159;&#20013;&#22269;&#20154;”,这明显是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 voiddo
Filter(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 voiddo
Get(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的全局函数
&#61548;
encodeURI(uri)
&#61548;
encodeURIComponent(uri)
&#61548;
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规范的服务器之间移植,而这些服务器的平台也可能不同。我们的客户的操作系统或所使用的浏览器,也有可能不同,所以我们应努力遵循一些建议:
&#61548; 从请求头中判断出用户的操作系统、浏览器和所使用的语言,而用适当的方法进行相应的响应头中的charset的设置。
&#61548; 尽量不在Servlet中使用含非英文字符的常量字符串。
&#61548; 对于JSP文件,只要页面中存在不被服务器忽略的含非英文字符的常量字符串,就应该对page指令中的pageEncoding属性进行相应的设置。
&#61548; 使用过滤器来设置通过
ServletRequest.setCharaterEncoding(String encoding);
来设置请求实体的编码,而不是在每个JSP或Servlet中设置。抛弃在Servlet设置content-Type的习惯,让过滤器根据不同的资源和用户对象通过
ServletResponse.setContentyType(String type);
来设置。
&#61548; 尽量使用UTF-8作编码,而不是GBK或GB2312。
&#61548; 充分考滤到底层数据库所使用的编码,它可能在应用程序的移植中带来麻烦。
&#61548; 不能简单有效地判断出用户浏览器编码提交的数据所使用的编码,是程序员的一大遗憾,如果W3C对现有的标准进行扩展,修改form元素的enctype,让这个属性也完全兼容MIME的charset,Web应用程序的开发,又少去了一些麻烦。