【转】HTTP协议及浏览器编码行为
HTTP协议和浏览器是Web国际化的基础,在进入Java服务器端之前,必须先对它们的编码行为有所了解。
HTTP协议是B/S体系结构应用程序的基础,只有了解了HTTP协议,才能理解如何在B/S体系结构下实现应用程序的国际化。
1.HTTP请求
当用户在浏览器的地址栏中输入一个URL并按回车键之后,浏览器会向HTTP服务器发送HTTP请求。HTTP请求主要分为“Get”和“Post”两种方法。
2.采取“Get”方法的HTTP请求
“Get”请求的典型用途是从HTTP服务器获取指定的资源,这样的请求不包含请求体。在浏览器中输入一个URL并按回车键后,浏览器就会生成这种类型的请求。HTTP服务器根据该请求所包含URL中的参数来动态产生响应内容,即“Get”请求的参数是URL的一部分。例如:
http://www.baidu.com/s?wd=Chinese
上述URL是一个使用百度搜索关键字“Chinese”的URL,参数“wd”包含在URL中,一起发送到HTTP服务器,参数的值是“Chinese”。当参数名和参数值都是ASCII字符时不会出现问题,但当参数名或参数值中包含非ASCII字符时就有可能出现问题。
由于URL通过网络传递,因此,为了保证信息的兼容性和通用性,当URL包含非?ASCII字符时,必须对其进行转义。如果将上例中的参数值改为“中文”,则URL变为:
http://www.baidu.com/s?wd=中文
当在浏览器(我们使用的是Firefox2.0)的地址栏中输入上述URL并按回车键后,可以看到浏览器会自动对URL进行转义,得到的是:
http://www.baidu.com/s?wd=%D6%D0%CE%C4
可以看到“中文”已经被浏览器自动转义成为了%D6%D0%CE%C4,它们是汉字“中文”的GBK编码对应的转义形式。另外,不同的浏览器对URL进行转义的行为是不同的,具体内容请参阅6.1.2节的介绍。
当HTTP服务器收到这样的请求时,必须先将转义的字符解释为有效的字符,再对URL进行处理。但是,HTTP协议中并没有指定使用何种编码和字符集来解释URL中的非ASCII字符(细节可参阅RFC2396,2.1节),因此,是否能成功解析就完全取决于URL中非ASCII内容的编码是否与?HTTP服务器的解析编码一致。例如,如果我们希望在Google中也搜索“中文”,构造如下URL:
http://www.google.com/search?q=%D6%D0%CE%C4
在浏览器地址栏中输入这个URL并按回车键后,会发现搜索结果页面查询的关键字并不是“中文”而是一个不能识别的乱码。这是因为Google的HTTP服务器使用UTF8编码来解释URL中的非ASCII字符。如果使用下面以UTF8编码的URL就能得到正确的结果:
http://www.google.com/search?q=%E4%B8%AD%E6%96%87
请注意:Google在不同区域的服务器可能会使用不用的编码方式来解析URL。例如www.google.cn可以正确解析:http://www.google.cn/search?q=%D6%D0%CE%C4;而www.google.com只能正确解析:http://www.google.com/search?q=%E4%B8%AD%E6%96%87。
而且,由于Google可以根据用户浏览器的区域设置自动将用户重定向到某个特定区域的服务器上,因此在Firefox中,如果浏览器的首选区域是?zh-cn,那么访问如下url:http://www.google.com/search?q=%D6%D0%CE%C4会被自动重定向到http: //www.google.cn/search?q=%D6%D0%CE%C4,因此,显示的结果是正确的。
3.采取“Post”方法的HTTP请求
“Post”请求通常用来向HTTP服务器提交量比较大的数据(比如请求中包含许多参数或者文件上传操作等),它与“Get”方法的主要区别在于请求的参数包含在消息体而非?URL中,服务器同样需要获得正确的编码信息才能够正确解析在消息体中的请求参数。在?“Post”方法的HTTP请求中,通常包含一个“Content-Type”get方法不产生这个消息头,因为没有content消息头指明该消息体的媒体类型和编码,如“Text/XML; charset=gb2312”,指明该请求的消息体中包含的是纯文本的XML类型的数据,字符编码采用“gb2312”。?(在request方式中就不行吗?)
使用一些Firefox插件可以辅助开发人员分析请求的消息头和消息体,较常用的有Firebug等。
4.HTTP响应
HTTP响应是HTTP服务器在接收请求之后向客户端返回的信息。一个HTTP响应通常由状态行、消息头和消息体组成。HTTP响应消息的第一行是状态行,表示服务器对请求的应答。常见的应答有:“200:OK”、“404:Not Found””、“500:Internal Server Error”等。
与HTTP请求类似,HTTP响应消息也包含一个“Content-Type”消息头(get方法不产生这个消息头,因为没有content),它指定了消息体中内容的类型和编码,例如“text/html; charset=UTF-8”。只有正确指定了“Content-Type”消息头,浏览器才能正确解析收到响应消息体中的数据并呈现页面。
浏览器是发送HTTP请求和接收HTTP响应的客户端,HTTP协议保证了大多数情况下浏览器行为的一致性,但不同浏览器之间仍有许多差异。这些差异经常导致B/S体系结构应用程序的开发变得困难。本节着重解释不同浏览器在涉及国际化方面的不同行为。
使用浏览器发送HTTP请求有多种方式:
在浏览器地址栏中直接输入URL;
在页面中通过点击“提交”按钮提交表单;
用户在页面中点击超链接产生的请求;
使用JavaScript脚本的XMLHTTPRequest对象发送请求。
(1)在浏览器地址栏中直接输入URL
当URL中包含非ASCII码字符时,Firefox会自动将这些字符进行转义,转义使用的编码由浏览器的语言版本决定。例如,“http://www.baidu.com/s?wd=中文”将会转义为http://www.baidu.com/s?wd=%D6%D0%CE %C4。
Firefox也提供了使用UTF-8进行URL编码的选项。在地址栏中输入“about:config”,并按回车键打开配置页面,在过滤器中输入“network.standard-url.encode-utf8”以定位到该选项,如图6-1所示。将该选项的值修改为true,以使Firefox始终用UTF-8对URL进行转义。这样,上述URL将转义为http://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87。
默认情况下,中文Windows平台上的IE浏览器将URL分为两个部分,“?”之前的部分URL使用UTF8进行转义,而“?”之后的参数部分,则不进行转义而直接使用GBK编码发送。例如URL“http://localhost/中文.jsp?test=中文”,前一个“中文”将按照UTF8?编码的转义形式“%E4%B8%AD%E6%96%87”发送,而参数部分的“中文”则直接以GBK编码发送,因此,最终发送的URL如图6-2所示。
在IE的“Internet选项”的“高级”选项卡页中有一个选项“总是以UTF-8发送URL”,在缺省情况下该选项是选中的。如果去掉这个选项,IE将会以系统当前的代码页来对URL进行编码。在中文Windows中整个URL都将以GBK编码发送,如图6-3所示。
(2)在页面中通过单击“提交”按钮来提交表单
在表单中属性“method”用来指定提交表单时所使用的HTTP请求方法,可以选择Post或者Get。用户不指定时,默认采用Get方法。而表单所提交内容采用的编码则由页面当前的编码决定。例如,在一个JSP中包含以下表单代码:
===formencoding.jsp====
<%@ page language="java"? contentType="text/html; charset=GBK"
??? pageEncoding="GBK"%>
<form action="formencoding.jsp" >
<input type="text" name="中文" ></input>
<input type="submit"/>
</form>
在IE或Firefox浏览器中打开该页面,在“中文”输入框中填入“中文”并单击“提交”按钮,会产生一个Get请求,所使用的URL为:
http://localhost:8080/jsbook/formencoding.jsp?%D6%D0%CE%C4=%D6%D0%CE%C4即使用GBK编码对URL进行转义。
如果将该页面的contentType重置为contentType="text/html;charset=UTF-8",则该表单所产生的URL为:
http://localhost:8080/jsbook/formencoding.jsp? %E4%B8%AD%E6%96%87=%E4%B8%AD%E6%96%87%即使用UTF-8编码对URL进行转义。
如果表单使用Post方法,则提交的参数将放在请求的消息体中,而使用的编码方式仍将由该页面的编码方式决定。
(3)在页面中单击超链接产生的请求
用户单击页面中的超链接时,浏览器将会产生一条“Get”请求。这个请求的URL使用的编码方式由当前页面使用的编码及使用的浏览器共同决定。我们仍然使用前文的例子“http://localhost/中文.jsp?test=中文”来说明。
在IE中,页面编码为UTF8时,这一请求中“?”前的部分将以UTF8编码转义,而“?”后的参数部分将直接使用UTF8编码发送;当页面编码为GBK时,请求中“?”前的部分仍以UTF8编码转义,而“?”后的参数部分将直接使用GBK编码发送。
在Firefox中,页面编码为UTF8时,整个URL将以UTF8编码转义。如果页面编码为GBK,则请求以GBK编码转义。
如果在IE中禁用了“总是以UTF-8发送URL”选项,那么当页面编码为UTF8时,这一请求中“?”前的部分将以UTF8编码转义,而“?”后的参数部分将直接使用UTF8编码发送;当页面编码为GBK时,整个请求都将直接使用GBK编码发送。
(4)使用XMLHTTPRequest对象发送请求
最后,我们来看一下使用JavaScript脚本来发送请求的情形。XMLHTTPRequest对象(下面简称XHR)是构成Ajax应用程序的基础,它允许JavaScript脚本直接向服务器发送HTTP请求,在页面不刷新的前提下与服务器通信,提交和获取数据。
使用XHR对象发送请求也分为Get和Post两种。
IE中使用XHR对象发送“Get”请求时,对URL所使用的编码规则和在地址栏中输入URL是一致的。
Firefox中使用XHR对象发送“Get”请求时始终使用UTF-8编码对URL进行转义,而发送“Post”请求时,参数和URL分离,参数部分在消息体中,使用UTF-8编码。要使Web服务器能够正确识别,最好在Content-type消息头中添加“Charset”信息,如以下代码段所示:
//创建 XHR 对象,并准备 URL 和请求参数
xmlHttp.open("Post",url,true);
xmlHttp.setRequestHeader("Content-type",
?????? "application/x-www-form-urlencoded;charset=UTF-8");
xmlHttp.setRequestHeader("Content-length",? params.length);
xmlHttp.setRequestHeader("Connection",? "close");
xmlHttp.send(params);
请注意,设置上述请求的消息头只能用来告知服务器该消息体所使用的编码,并不能通过修改此消息头的值来改变该请求所使用的编码。
前文描述了浏览器在发送HTTP请求时选取编码的行为。那么从服务器端返回HTTP响应时,浏览器又是如何判断该响应使用了何种编码的呢?
浏览器判断返回的HTTP响应消息所使用的编码遵循以下一系列规则。
首先,浏览器会检查HTTP响应中的“Content-type”消息头。如“text/html; charset=UTF-8”,表明该消息所包含的内容是纯文本的HTML文档,采用UTF-8编码。但在很多情况下,服务器返回的Content- type消息头并不包含“charset”信息。
当响应消息不包含“charset”信息时,浏览器会尝试自动探测编码。第一个步骤是检查响应消息体的开头是否包含UTF-8的BOM(字节顺序标记,ByteOrder Marker)。BOM是一种用来判断文件编码的特定字节标记,如果一个文件的开头几个字节包含了UTF-8的BOM,那么浏览器就可以断定这个HTML?文件是采用UTF-8编码的。
如果该HTML中不包含BOM,那么,浏览器就会尝试寻找HTML页面中的<meta>标记,如:
<meta http-equiv="Content-Type"? content="text/html; charset=UTF-8" />
如果页面中又不包含<meta>标记,那么,浏览器将采用默认的编码来解析。在中文的IE和Firefox里就是采用GBK或GB2312编码。
因此,要使服务器端返回的响应消息能够正确地被浏览器解析,最简单有效的方法就是在响应的“Content-type”消息头中设置charset属性。在Servlet编程中可以在doGet()或doPost()方法中调用:
response.setCharacterEncoding("UTF-8")
在JSP编程中可以在页面开头指定响应的编码:
<%@ page language="java"? contentType="text/html; charset=UTF-8" import="java.? util.*"
?? pageEncoding="UTF-8"%>
本节描述了HTTP协议中数据传输时需要考虑的编码问题,以及在浏览器中发送请求时所使用的不同编码设置。由于编码设置在整个B/S体系结构的“请求-处理-响应”各个环节中无处不在,其中任何一个环节的错误设置都会导致最终呈现给用户的数据出现乱码,因此,理解这些编码设置的原理将有助于我们在遇到问题时检测和判断究竟是哪个环节设置错误。
而避免这些错误和复杂的编码设置的最好办法,就是在所有的环节都统一使用UTF-8编码,这也可以说是在设计B/S体系结构的Web应用程序时需要贯穿始终的设计原则。
浏览器需要使用恰当的字符编码来解释HTML文档,以使网页可以用正确的语言、字符集和字体显示。为了保证这一点,HTML文档应当包含明确的信息来表明所使用的字符编码,这可以通过设置META标记实现。网站管理员还可以通过配置IBM HTTP Server?来为每一个响应加上合适的HTTP消息头,从而确保在META标记缺失的情况下网页也能正常显示。
1.在HTML文档中使用META标记来标识字符编码
META标记的作用是什么?META标记在W3CHTML规范中定义,嵌入在HEAD标记中,用于标识关于HTML文档的元信息,这些元信息可以被浏览器和Web服务器所解析和使用。在META中设置HTTP-EQUIV属性,可以告诉浏览器该META信息与HTTP信息头等价。如果HTTP-EQUIV属性缺失,那就需要设置NAME属性来标识该元信息,并且不应将这个元信息作为HTTP信息头来解析。如果NAME属性缺失,但HTTP-EQUIV存在,那么会假设NAME与HTTP-EQUIV相同。而CONTENT属性用来保存与HTTP-EQUIV或NAME相关的值,以构成一个“键/值对”(key-value pair)。
设置META标记最简单也是最安全的做法是将HTML以UTF-8编码保存,并且在HEAD中加入名称为Content-Type的META信息。
...
<head>
<meta http-equiv="Content-Type"? content="text/html; charset=UTF-8"/>
<title>这是网页的标题</title>
</head>
...
如果HTML整体显示正确,但只有标题为乱码,那是什么问题?这在多数情况下是因为TITLE标记出现在META标记之前,浏览器顺序解读HTML时首先遇到TITLE,而在那时还不曾确定应当用什么字符编码解析,比如下面就是一个错误的例子。
...
<head>
<title>这是网页的标题</title> <!--这一行不应该出现在 META 之前-->
<meta http-equiv="Content-Type"? content="text/html; charset=UTF-8"/>
</head>
...
2.配置IBM HTTP Server
这是一种自动防错机制,即便个别HTML文档因为这样或那样的原因,没有用META标记声明字符编码,IBM HTTP Server仍能够自动地在HTTP响应消息头中提供编码信息,帮助浏览器正确解读HTML文档。
具体的配置方法是修改IBM HTTP Server的配置文件httpd.conf(默认情况下位于conf目录下),如下面的代码片段所示。
...
AddType ‘text/html; charset=UTF-8’ html htm
...
这样,服务器就会为每一个后缀为html和htm的请求回复中自动加上消息头(如下所示),以达到通知浏览器文档的编码格式的目的。
Content-Type: text/html; charset=UTF-8
上面的配置适用于整个网站统一采用UTF-8编码的情况。那么,如果并没有统一为UTF-8编码该怎么办呢?IBM HTTP Server可以根据文档所处目录的不同,自动添加不同的消息头,当然这种配置会复杂一些。
假设网站包含不同编码的文档,分布在不同的目录中。
/root/-----/en/---index.html?? (encoded in ASCII)
??????? |???????? page1.html
??????? |
???????? |--/cn/---index.html?? (encoded? in GB2312)
??????? |???????? page1.html
??????? |
???????? |--/ja/---index.html?? (encoded? in Shift_JIS)
?????????????????? page1.html
那么相应的?httpd.conf?大致如下。
<Directory /root/en/ >
...
AddType 'text/html; charset=iso-8859-1' html htm
...
</Directory>
...
<Directory /root/cn/ >
...
AddType 'text/html; charset=GB2312' html htm
...
</Directory>
...
<Directory /root/ja/ >
...
AddType 'text/html; charset=Shift_JIS' html htm
...
</Directory>
有了上面的配置,在目录/root/下的html和htm就会以相应的HTTP消息头返回给浏览器。比如对于/root/cn/index.html,就会返回“Content-Type: text/html;charset=UTF-8”消息头。
如果不希望直接修改httpd.conf,那么,可以通过修改.htaccess来覆盖httpd.conf中的配置,可以达到相同的目的。.htaccess是一个作用于其所在目录和所有子目录的配置文件,对.htaccess的修改不需要重新启动IBM HTTP Server就能生效,因此被经常使用。要保证.htaccess中的配置覆盖httpd.conf,首先需要在httpd.conf中对相关目录加上?“AllowOverride”参数,如下所示。
...
<Directory /root/cn/ >
...
AllowOverride All
...
</Directory>
然后在.htaccess中关于/root/cn/的配置中加入下面一行以使之生效。
AddType 'text/html; charset=GB2312' html htm
更多关于IBMHTTP Server配置文件和参数的信息,请查阅相关文档。
3.小结
以UTF-8编码保存HTML文档。
在HTML文档中加入META标记以标识字符编码。
配置IBMHTTP Server以启用自动防错功能。
在当代的网络应用程序中,JSP常常扮演MVC模型中视图的角色,用来生成HTML以返回客户端,从而在浏览器中呈现用户界面。对国际化的应用程序来说,JSP文件可能以多种编码(为不同的语言)编写,因此,需要有特定的参数标识JSP的字符编码,以保证应用程序服务器能正确地解析和编译JSP文件。本节会介绍相关的参数并说明它们的用途,同时再一次强调,采用统一的UTF-8编码是最简单和最安全的策略。
1.设置JSP文件的编码
在JSP能够被执行并响应用户请求之前,首先要经过一个编译的过程,这里JSP文件的编码指的是应用程序服务器读取JSP,进行编译时所使用的字符编码。最通常的做法是将所有JSP都保存为UTF-8格式,并在文件中使用pageEncoding标明字符编码。
<%@ page language="java"? pageEncoding="UTF-8" %>
如上pageEncoding就是一个专用于标识JSP字符编码的参数,但同时它也是可选的。你也可以不提供pageEncoding,而通过设置contentType参数来指定页面编码。
<%@ page language="java"? contentType="text/html; charset=UTF-8" %>
像这样缺少pageEncoding的情况下,contentType中的charset部分将用于JSP页面的字符编码。如果同时使用了contentType和pageEncoding,哪一个参数优先级更高呢?答案是pageEncoding优先。在contentType和pageEncoding中使用不同的编码是一种非常少见的情况,后面的章节会具体讨论。但一般而言,字符编码在contentType和?pageEncoding中应当保持一致。
除了上述在每一个JSP文件中指定字符编码外,从JSP 2.0规范开始,也可以在web.xml中一次声明多个JSP文件的编码。
<jsp-config>
?? <jsp-property-group>
???? <description>For all JSPs</description>
???? <url-pattern>*.jsp</url-pattern>
???? <page-encoding>UTF-8</page-encoding>
?? </jsp-property-group>
</jsp-config>
如果在web.xml中加入上面的配置,那么,所有的JSP文件都会采用UTF-8编码执行编译,这比在每一个JSP文件中加入声明要简洁得多,当然,这首先要求应用程序服务器支持JSP 2.0规范。
在JSP2.0规范中,JSP文件的编码声明和查询次序的定义如下。
(1)首先,寻找web.xml中url-pattern匹配的jsp-config声明。
(2)其次,寻找JSP文件中的pageEncoding参数,如果pageEncoding参数与jsp-config中的配置不符,将产生一个JSP编译错误。
(3)再次,寻找contentType中的charset声明,这仅当jsp-config和pageEncoding参数都不存在时才有效。
(4)最后,如果上述所有设置都不存在,那么,采用ISO-8859-1作为默认编码。
2.设置JSP响应字符编码
JSP响应字符编码是指JSP生成的HTTP响应所使用的字符编码,也就是JSP运行后生成的HTML文档的字符编码,该编码由contentType参数指定。在每一个JSP文件中都加入合适的contentType声明是推荐的做法。比如:
<%@ page language="java"? contentType="text/html; charset=UTF-8" %>
这样会调用ServletResponse.setContentType()方法,从而设置HTTP响应的字符编码。
如果缺少contentType,那么会继续查询pageEncoding参数及web.xml中的jsp-config配置。如果其中任意一个存在,那么其字符编码将作用于HTTP响应,如果都不存在,那么最终会使用ISO-8859-1字符编码。
3.符合XML语法的JSP文件
JSP规范从V2.0开始支持完全符合XML语法的JSP文件,对于这类符合XML语法的JSP文件,上述的文件字符编码和响应字符编码的设置会略有不同。
对于JSP文件编码,它取决于XML文件第一行的编码声明。
<?xml encoding='UTF-8'?>
文件中pageEncoding和web.xml中的jsp-config设置仍然有效,但仅起到一个复查的作用。如果任意一处有不一致的编码声明,都将导致JSP编译时出错。例如,可以如下声明pageEncoding。
<jsp:directive.page? pageEncoding="UTF-8"/>
对于HTTP响应字符编码,仍然使用contentType参数,当然语法有所不同,以便符合XML规范。
<jsp:directive.page contentType="text/html;? charset=UTF-8"/>
如果contentType参数不存在,那么默认的字符编码将为UTF-8,而不再是ISO-8859-1。
4.一些不推荐的方法
对所有JSP统一使用UTF-8编码是最好的策略。如果非要在JSP中使用和语言相关的编码格式,那么,对于国际化的应用程序来说将是一场噩梦,因为这将导致同一个网页有多个对应的JSP文件,每个文件对应一种语言。如果要支持10种语言,那就有网页数乘以10的JSP文件,其中任意一个网页需要变更,都要改变10个JSP文件,这几乎是无法管理的。
另一种不推荐的情形是对一个JSP使用不同的文件编码和响应编码。比如,下面的声明表示JSP文件本身的编码是GB2312,而生成的HTML内容则使用UTF-8。
<%@ page language="java"? contentType="text/html; charset=UTF-8"? pageEncoding="GB2312"%>
这种做法虽然存在理论上的可能性,但实际操作中却很容易把两种编码混淆,从而产生错误。调试这类编码错误是非常复杂的,因为有太多的配置可能对其产生影响。
5.小结
最好以UTF-8编码保存JSP文档。
在JSP中使用pageEncoding参数声明文件字符编码,使用contentType参数声明响应字符编码。
如果应用程序服务器支持JSP 2.0规范,也可以在web.xml中使用jsp-config标记统一声明JSP文件编码。
对于一个国际化的应用程序而言,仅设置HTML和JSP编码是远远不够的,除了考虑服务器端输出内容的字符编码外,还要考虑如何正确解析来自全球范围内不同地域和语言的用户输入。本节将讨论如何设置编码以确保一个网页上的表单能够接收不同语言的输入,并且在服务器端这些不同语言的字符能够被正确地解析。
1.使用UTF-8编码提交数据
要使应用程序能够接收国际化的输入,首先要确保浏览器使用UTF-8编码向Web服务器提交数据。UTF-8作为通用编码,可以为全球不同的语言进行统一的编码和解码,这样可以大大简化服务器端为了适应多语言所需的编程。
当用户在浏览器单击提交表单时,浏览器会使用当前网页的字符编码提交表单上输入的内容,因此,只要对所有的网页都采用UTF-8编码,就可以保证浏览器总是使用UTF-8编码提交数据。
对网页编码的设置已经在“6.2.1 HTML的编码设置”和“6.2.2 JSP的编码设置”中详细讨论过了,可以根据网页输出的具体方式在相应的章节找到编码的设置方法,这里就不重复了。
2.设置字符编码以正确解析HTTP请求
当数据提交到服务器端后,一般在Servlet中进行字符编码的解析,然后获取用户输入,进行逻辑运算和业务处理。为了正确地解码用户输入,程序在读取HTTP请求之前,必须调用ServletRequest.setCharacterEncoding()方法来设置合适的字符编码,以便将HTTP?请求中包含的字节流解码为字符流。
protected final void doPost(HttpServletRequest req,? HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String input = req.getParameter("order");
}
如上例所示,在Servlet的doPost()方法中,首先调用setCharacterEncoding(),以便将UTF-8设置为字符编码,然后读取名为order的输入参数。这里setCharacterEncoding()设置的编码必须和浏览器提交数据所使用的字符编码一致,上例中即为UTF-8,否则,就会导致读入的字符串为乱码。
关于setCharacterEncoding()方法,一个需要强调的要点是:必须在读取HTTP请求之前设置编码,否则是无效的。这里说的“读取HTTP请求”不仅包括使用getParameter(),getParameterMap(),getParameterNames()和getParameterValues()之类的方法,还包括?getReader()方法,比如下面就是一个错误的例子。
protected final void doPost(HttpServletRequest req,? HttpServletResponse resp)
throws ServletException, IOException {
Reader reader = req.getReader();
req.setCharacterEncoding("UTF-8"); // 错误!必须在 getReader() 之前调用!
String input = req.getParameter("order"); // 有可能读入乱码
}
对于一般的程序,要确保在第一时间调用setCharacterEncoding()可能极为简单,但是,如果使用了比较复杂的Web层框架,情形就会不同。因为程序的控制权往往并不直接进入应用程序代码,而是通过框架转发调用再进入应用程序,此时如果在框架内部首先读取了HTTP请求,那么在应用程序中调用setCharacterEncoding()就为时已晚。
因此,根据所使用的Web框架的不同,调用setCharacterEncoding()的时机和位置都有可能不同,需要根据具体情况而定。上面的示例仅对最普通的Servlet而言,并不适用于所有的Web框架。后面的章节会对一些主流的Web框架进行与国际化相关的具体分析。
3.设置Servlet的响应字符编码
在大多数情况下,Java应用程序通过JSP生成HTML文档,作为响应发回浏览器,相关的设置已经在6.2.2中的“2.设置JSP响应字符编码”中介绍过了。然而,也存在个别特殊情况,程序会选择由Servlet直接生成响应,此时就需要在Servlet中设置响应的字符编码。在?ServletAPI中,有三个方法可以用于设置响应字符编码,它们都在ServletResponse接口中定义,分别是?setCharacterEncoding(),setContentType()和setLocale()。
一般推荐用setContentType()或者setCharacterEncoding()设置编码。它们的用法大致如下:
protected final void doPost(HttpServletRequest req,? HttpServletResponse resp)
throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;? charset=UTF-8");
PrintWriter writer = resp.getWriter();
writer.println("...");
}
两个方法都会在HTTP响应头中加入ContentType声明,通知浏览器响应内容的字符编码。
仅当旧版本的Servlet规范不支持setContentType()和setCharacterEncoding()时,才会使用setLocale()。调用setLocale()会导致采用和语言相关的默认编码,但仍通过ContentType响应头通知客户端。
这里同样需要注意,设置响应编码必须在输出响应之前执行,否则将起不到预期的效果。下面是一个错误例子。
protected final void doPost(HttpServletRequest req,? HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
writer.println("...");
resp.setCharacterEncoding("UTF-8"); // 错误,必须在 getWriter() 之前设置
}
4.小结
将UTF-8设置为网页编码,以UTF-8提交数据。
调用ServletRequest.setCharacterEncoding()设置编码解析HTTP请求。
如果不通过JSP,而是通过Servlet直接生成响应,那么调用ServletResponse接口中定义的setCharacterEncoding()或者setContentType()来设置响应字符编码。
在实际项目开发中,往往不会直接使用Servlet/JSP,更多的是在一些Web框架的基础上搭建应用程序,这些Web框架在带来完善的设计模式的同时,也对Serlet/JSP编程接口进行包装和扩展,这在一定程度上造成了不小的学习曲线。习惯在Servlet/JSP模式下工作的开发人员有时也会产生困扰:如何在这些框架下写出支持国际化的代码呢?面对更丰富的编程接口和类继承,应该从何处入手,通过什么方法来设置恰当的字符编码呢?本节将尝试回答这些问题。
1.Struts框架下的编码设置
Struts可能是时下最流行的Web框架,它推荐的MVC模式几乎是所有Web应用程序的首选。从原理上说,Struts是基于Servlet/JSP的一种扩展,就设置字符编码而言,仍然是通过Servlet编程接口来执行,其不同处主要在于设置编码的时机。
与Servlet不同,在Struts框架下,服务器端的程序入口并不在应用程序,而总是经由Struts框架,再辗转进入应用程序的操作(Action)逻辑,这对需要在第一时间设置的字符编码造成了不小的困扰。更重要的是,Struts还负责自动从HTTP请求读取表单输入,并导入?ActionForm中,以供应用程序使用。所以,必须寻找到一个时机,在Struts读取HTTP请求之前完成编码设置。
纵观Struts框架,最合适设置编码的位置是ActionForm的reset()方法。该方法在每次重置ActionForm时调用,紧接着Struts就会从HTTP请求读取表单输入。下面是一个范例。
public class SampleForm extends ActionForm {
public void reset(ActionMapping mapping,? HttpServletRequest request) {
// 在此设置字符编码
request.setCharacterEncoding("UTF-8");
...
}
}
具体设置的方法与Servlet中的编码设置一样,更详细的内容可以参阅6.2.3节中的“2.设置字符编码以正确解析HTTP请求”。
2.使用过滤器的通用编码设置方法
Java社区框架种类繁多是出名的。除了Struts以外,其他的Web框架也如雨后春笋般层出不穷。每一个框架都有自己的个性和特质,实现国际化、设置编码的方法都有可能不同。逐一分析每一个框架,寻找恰当时机和位置来设置字符编码固然可行,但有没有更通用的方法,能够普遍适用于大多数Web框架呢?
答案是:有。Servlet规范V2.3引入了过滤器(Filter)机制,这是一种非常有用的工具,能够通用地设置Web层的字符编码。其原理是,通过恰当的配置,过滤器可以在所有的HTTP请求进入Servlet之前进行拦截,从而保证在调用Web框架之前(不论什么框架,其入口总是一个特定的Servlet)第一时间完成字符编码的设置。
关于过滤器的详细说明和使用方法不在本书范围之内,读者可以自行参考Servlet规范以了解更多信息。下面仅以一个示例说明如何使用一个UTF8Filter来保证所有的HTTP请求都以UTF-8编码进行解析。
首先是一个名为UTF8Filter的Java类,它实现了Filter接口,用以拦截HTTP请求,并在请求进入Servlet之前对其进行编码设置。
package filters;
import java.io.IOException;
import javax.servlet.*;
public class UTF8Filter implements Filter {
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 进行拦截,然后设置字符编码
request.setCharacterEncoding("UTF-8");
// 继续,进入? Servlet 执行阶段
chain.doFilter(request, response);
}
public void init(FilterConfig filterConfig) throws? ServletException {
}
public void destroy() {
}
}
UTF8Filter中实现了拦截的逻辑,至于对哪一些HTTP请求进行拦截则是在web.xml中配置的。下面的web.xml片段将UTF8Filter配置为拦截所有的HTTP请求。
<filter>
<filter-name>UTF8Filter</filter-name>
<filter-class>filters.UTF8Filter</filter-class>
</filter>
<filter-mapping>
<filter-name>UTF8Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
请注意上面片段中的<url-pattern>/*</url-pattern>一行,模式“/*”和所有的URL都匹配,从而保证UTF8Filter拦截所有的HTTP请求。
3.小结
对于Struts框架,可以在ActionForm.reset()中设置字符编码。
用过滤器(Filter)拦截HTTP请求,在进入Web框架之前设置字符编码。
本章介绍了?Java?国际化开发在?Web?应用程序领域的相关技术。涉及的技术包括?HTTP?协议、HTML/JSP/Servlet?编码设置、“资源包”和“语言目录”的实现策略、标记库(Tag Lib),以及?JavaScript?的国际化开发等。
?