这几天都在纠结Java Web开发中的中文编码问题。其实,很多Java Web开发者都被中文编码“折磨”过,网络上有大量的讨论。以前我也读过这方面的博文,读完后感觉似乎懂了,好像知道了编码问题的原因和解决方法。但是, 一旦投入到实际开发中,发现自己其实没懂,囧!
    连续纠结了几天,总算对前因后果有个清晰地认识,故“略谈”一下。之所以略谈,是因为我并非(也没有能力)完整地阐述Java Web开发的中文编码问题,而是就事论事地总结这几天遇到的问题和收获。

问题

使用HttpClient 3.x发送GET或POST请求,请求参数中包含中文。服务器是Tocmat 5.5,通过断点调试,发现Servlet拿到的中文参数是乱码。显然,HttpClient和Tomcat没有就中文参数的编码达成一致。
    于是,开始深入HttpClient和Tomcat的代码,结合断点调试,发现中文编码问题并不是想象中的那么简单。

术语约定

为了使得描述更加请求,我对本文中出现的“术语”进行约定,避免一词多义引起的歧义。

  1. Encoding: 编码(动词)
  2. Decoding: 解码(动词)
  3. Charset: 编码或解码使用的字符集

另外,编码了的数据必然需要解码,因此encoding和decoding往往是同现的。不过为了叙述简练,下文需要两者同现的地方,仅使用encoding。

哪些数据需要encoding?

在研究中文编码问题前,我们首先要弄清一个问题:哪些数据需要encoding?

一个Http请求的数据大致包括URI、Header、和Body三个部分。这三个部分貌似都需要encoding,不过我这次只涉及到URI和Body,因此 就不讨论Header了。

我们一般关心请求参数的中文编码问题。虽然URI Path中也可以包括中文,但是。。。这不是给自己找麻烦吗?
    GET的请求参数在QueryString中,是URI的一部分。因此,对于GET请求,我们需要关注,URI是如何encoding的?
    POST的请求参数在Body中,因此,对于POST请求,我们则需要关注,Body是如何encoding的?

对于HttpClient和Tomcat来说,encoding和decoding本身是很容易的事情,关键是要知道charset是什么?要不通过API进行设置,要不通过配置文件进行配置。麻烦的是,URI和Body的charset还可以不一样,使用不同的方法进行设置和配置。

HttpClient是一个类库,通过自身提供的API对URI和Body的charset进行设置;Tomcat通过配置项和Servlet API,对URI和Body的charset进行设置。

HttpClient如何设置charset?

我们先看看如何设置GET请求QueryString的charset,然后看看POST请求Body的charset,最后看看如何获取响应数据的charset。

设置GET请求QueryString的charset

我们通过GETMethod的setQueryString方法设置QueryString。setQueryString方法有两种原型,我们分别看看。

  1. public void setQueryString(NameValuePair[] params){
  2. LOG.trace("enter HttpMethodBase.setQueryString(NameValuePair[])");
  3. queryString = EncodingUtil.formUrlEncode(params, "UTF-8");
  4. }

原型一以参数键值对的形式设置QueryString,使用固定的UTF-8作为charset,而且做URLEncode。因此,调用原型一之后,HttpClient就不会对QueryString再做任何encoding了。

如果不想使用UTF-8,那么可以使用原型二。

  1. public void setQueryString(String queryString){
  2. this.queryString = queryString;
  3. }

原型二直接设置QueryString的内容。需要注意的是,queryString参数一定是按照某种charset进行URLEncode之后的字符串 。

另外,也可以通过GETMethod的构造函数,直接设置URLEncode之后的uri (包括了QueryString):

  1. public GetMethod(String uri) {
  2. super(uri);
  3. LOG.trace("enter GetMethod(String)");
  4. setFollowRedirects(true);
  5. }


    设置POST请求Body的charset

首先,我们可以在POST请求中的Header中设置Content-Type:

  1. PostMethod method = new PostMethod();
  2. method.addRequestHeader("Content-Type","text/html;charset=UTF-8");

在这里,Body的charset就UTF-8。

其次,如果没有设置Content-Type,我们还可以设置HttpClientParam的ContentCharset:

  1. HttpClient httpClient = new HttpClient();
  2. HttpClientParam params = httpClient.getParams();
  3. params.setContentCharset("UTF-8");

然后,如果没有设置HttpMethodParams的ContentCharset,我们还可以设置HttpMethodParams的ContentCharset:

  1. PostMethod method = new PostMethod();
  2. HttpMethodParams params = method.getParams();
  3. params.setContentCharset("UTF-8");

这三种设置方法的优先级依次递增,也就是说如果同时设置,则以后面的为准。如果都没有设置,默认charset是ISO-8859-1。

响应数据的charset

我们一般使用HttpMethodBase(GETMethod和PostMethod的父类)的getResponseBody系列方法获取响应数据。getResponseBody系列方法包括:

  1. public byte[] getResponseBody() throws IOException{...}
  2. public byte[] getResponseBody(int maxlen) throws IOException{...}
  3. Public InputStream getResponseBodyAsStream() throws IOException {...}
  4. public String getResponseBodyAsString() throws IOException {...}
  5. public String getResponseBodyAsString(int maxlen) throws IOException {...}

我比较喜欢getResponseBodyAsString方法,因为返回值类型是String,直接可以使用。不过,提到String就必须想到charset 。响应数据的charset肯定由Web Server(Tomcat)设置的,HttpMethodBase是怎么知道的呢?

我们看看getResponseBodyAsString()方法的代码:

  1. public String getResponseBodyAsString() throws IOException {
  2. byte[] rawdata = null;
  3. if (responseAvailable()) {
  4. rawdata = getResponseBody();
  5. }
  6. if (rawdata != null) {
  7. return EncodingUtil.getString(rawdata, getResponseCharSet());
  8. } else {
  9. return null;
  10. }
  11. }

顾名思义,getResponseCharSet方法的功能就是获取响应数据的charset。那就看看她的代码吧:

  1. public String getResponseCharSet() {
  2. return getContentCharSet(getResponseHeader("Content-Type"));
  3. }

可见,getResponseCharSet方法Content-Type Header获取响应数据的charset。这要求Servlet必须正确设置response的Content-Type Header 。

Tomcat如何设置charset?

即使HttpClient正确设置了charset,Tomcat还要知道charset是什么,才能正确decoding。我们先看看如何设置GET请求QueryString的charset,然后看看POST请求Body的charset,最后看看Servlet响应数据的charset。

设置GET请求QueryString的charset

Tomcat通过URI的charset来设置QueryString的charset。我们可以在Tomcat根目录下conf/server.xml 中进行配置。

  1. <Connector
  2. URIEncoding="UTF-8"
  3. useBodyEncodingForURI="true"
  4. acceptCount="100"
  5. connectionTimeout="20000"
  6. disableUploadTimeout="true"
  7. enableLookups="false"
  8. maxHttpHeaderSize="8192"
  9. maxSpareThreads="75"
  10. maxThreads="150"
  11. minSpareThreads="25"
  12. port="8080"
  13. redirectPort="8443"/>

URIEncoding属性就是URI的charset,上述配置表示 Tomcat认为URI的charset就是UTF-8。如果HttpClient也使用UTF-8作为QueryString的charset,那么 Tomcat就可以正确decoding。详情可以参考org.apache.tomcat.util.http.Parameters类的handleQueryParameters的方法:

  1. // -------------------- Processing --------------------
  2. /** Process the query string into parameters
  3. */
  4. public void handleQueryParameters() {
  5. // 省略部分代码
  6. processParameters( decodedQuery, queryStringEncoding );
  7. }

Tomcat在启动的过程中,如果从conf/server.xml中读取到URIEncoding属性,就会设置queryStringEncoding的值。当Tomcat处理HTTP请求时,上述方法就会被调用。

默认的server.xml是没有配置URIEncoding属性的,需要我们手动设置 。如果没有设置,Tomcat就会采用一种称为“fast conversion”的方式解析QueryString。详情可以参考org.apache.tomcat.util.http.Parameter类的urlDecode方法。

useBodyEncodingForURI是与URI charset相关的另一个属性。如果该属性的值为true,则Tomcat将使用Body的charset作为URI的charset。下一节将介绍Tomcat如何设置Body的charset。如果Tomcat没有设置Body的charset,那么将使用HTTP请求Content-Type Header中的charset。如果HTTP请求中没有设置Content-Type Header,则使用ISO-8859-1作为默认charset。详情参见org.apache.catalina.connector.Request的parseParmeters方法:

  1. /**
  2. * Parse request parameters.
  3. */
  4. protected void parseParameters() {
  5. // 省略部分代码
  6. String enc = getCharacterEncoding();
  7. boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
  8. if (enc != null) {
  9. parameters.setEncoding(enc);
  10. if (useBodyEncodingForURI) {
  11. parameters.setQueryStringEncoding(enc);
  12. }
  13. } else {
  14. parameters.setEncoding (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
  15. if (useBodyEncodingForURI) {
  16. parameters.setQueryStringEncoding (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
  17. }
  18. }
  19. parameters.handleQueryParameters();
  20. // 省略部分代码
  21. }

默认的server.xml是没有配置useBodyEncodingForURI属性的,需要我们手动设置 。如果没有设置,Tomcat则认为其值为false。需要注意的是,如果URIEncoding和useBodyEncodingForURI同时设置,而且Body的charset已经设置,那么将以Body的charset为准 。

设置POST请求Body的charset

设置Body charset的方法很简单,只要调用javax.servlet.ServletRequest接口的setCharacterEncoding方法即可,比如request.setCharacterEncoding("UTF-8")。需要注意的是,该方法必须在读取任何请求参数之前调用,才有效果。详情可以参见该方法的注释:

  1. /**
  2. * Overrides the name of the character encoding used in the body of this
  3. * request. This method must be called prior to reading request parameters
  4. * or reading input using getReader().
  5. *
  6. *
  7. * @param env a <code>String</code> containing the name of
  8. * the character encoding.
  9. * @throws java.io.UnsupportedEncodingException if this is not a valid encoding
  10. */
  11. public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException;

也就是说,我们只有在调用getParameter或getReader方法之前,调用setsetCharacterEncoding方法,设置的charset才能奏效。

响应数据的charset

设置响应数据charset的方法很简单,只要调用javax.servlet.ServletResponse接口的setContentType或setCharacterEncoding方法即可,比如response.setContentType("text/html;charset=UTF-8")或response.setCharacterEncoding("UTF-8")。需要注意的是,这两个方法的调用时机也是有讲究的,详情可以参见他们的注释。

如果Servlet正确设置了响应数据的charset,那么HTTP响应数据中就会包含Content-Type Header。HttpClient的getResponseBodyAsString方法就可以正确decoding响应数据。

总结

在开发Java Web应用的过程中,遇到中文乱码问题,应该是一件正常的事情。我们不必首先怀疑HtpClient或Tomcat有莫名奇妙的bug,往往都是我们使用不当或配置不当。凡事总有原因,总要相信科学嘛!如果想彻底了解中文编码的前因后果,我觉得可以从HTTP规范、Servlet规范、HttpClient的API文档和Tomcat的配置文档入手,必要时可以追踪HttpClient和Tomcat的代码。

[转]httpclient编码的更多相关文章

  1. [Python网络编程]gevent httpclient以及网页编码

    之前看到geventhttpclient这个项目,https://github.com/gwik/geventhttpclient,官方文档说非常快,因为响应使用了C的解析,所以我一直想把这玩意用到项 ...

  2. HttpClient设置编码类型

    笔者引用的是commons-httpclient这个jar包httpclient 可是通过get/post方式获取带有中文页面的html文件时.返回的是乱码,在网上找了非常久.最终找到一个合适的: H ...

  3. HttpClient POST 的 UTF-8 编码问题

    http://www.360doc.com/content/09/0915/15/61497_6003890.shtml不 过在实际使用中, 还是发现按照最基本的方式调用 HttpClient 时, ...

  4. .Net基础——程序集与CIL HttpClient封装方法 .Net Core 编码规范 C#中invoke和beginInvoke的使用 WebServeice 动态代理类

    .Net基础——程序集与CIL   1. 程序集和CIL: 程序集是由.NET语言的编译器接受源代码文件产生的输出文件,通常分为 exe和dll两类,其中exe包含Main入口方法可以双击执行,dll ...

  5. C++使用libcurl做HttpClient(业务观摩,用C++封装过程式代码,post和get的数据,最好url编码,否则+会变成空格)good

    当使用C++做HTTP客户端时,目前通用的做法就是使用libcurl.其官方网站的地址是http://curl.haxx.se/,该网站主要提供了Curl和libcurl.Curl是命令行工具,用于完 ...

  6. 爬虫任务一:使用httpclient去爬取百度新闻首页的新闻标题和url,编码是utf-8

    第一个入手的爬虫小任务: maven工程 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=" ...

  7. HttpClient相关

    HTTPClient的主页是http://jakarta.apache.org/commons/httpclient/,你可以在这里得到关于HttpClient更加详细的信息 HttpClient入门 ...

  8. Atitit.http httpclient实践java c# .net php attilax总结

    Atitit.http httpclient实践java c# .net php attilax总结 1. Navtree>> net .http1 2. Httpclient理论1 2. ...

  9. 使用httpclient发送get或post请求

    HttpClient 是 Apache Jakarta Common 下的子项目,可以用来提供高效的.最新的.功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建 ...

随机推荐

  1. easyui datagrid checkbox选中事件

    $('#grid_Order').datagrid({       onCheck: function(index, data) { //alert(data[0]);               / ...

  2. 'Agent XPs' component is turned off as part of the security configuration for this server

    To turn on Agent XP's by running this script: sp_configure 'show advanced options', 1; Go RECONFIGUR ...

  3. 面向对象的Shell脚本

    还记得以前那个用算素数的正则表达式吗?编程这个世界太有趣了,总是能看到一些即别出心裁的东西.你有没有想过在写Shell脚本的时候可以把你的变量和函数放到一个类中?不要以为这不可能,这不,我在网上又看到 ...

  4. vfork 挂掉的一个问题

    在知乎上,有个人问了这样的一个问题——为什么vfork的子进程里用return,整个程序会挂掉,而且exit()不会?并给出了如下的代码,下面的代码一运行就挂掉了,但如果把子进程的return改成ex ...

  5. 《从零開始学Swift》学习笔记(Day 65)——Cocoa Touch设计模式及应用之选择器

    原创文章,欢迎转载.转载请注明:关东升的博客 实现目标与动作关联使用UIControl类addTarget(_:action:forControlEvents:)方法,演示样例代码例如以下: butt ...

  6. MySQL数据库知识点整理 (持续更新中)

    一.修改用户密码 格式(在命令行下输入):mysqladmin -u 用户名 -p旧密码 password 新密码 1. 给root添加密码ab12:  mysqladmin -uroot -pass ...

  7. Java – How to add days to current date

    1. Calendar.add Example to add 1 year, 1 month, 1 day, 1 hour, 1 minute and 1 second to the current ...

  8. Java – Stream has already been operated upon or closed

    Java – Stream has already been operated upon or closed package com.mkyong.java8; import java.util.Ar ...

  9. Java 8 forEach examples遍历例子

    1. forEach and Map 1.1 Normal way to loop a Map. Map<String, Integer> items = new HashMap<& ...

  10. 使用Delaunay三角剖分解决求多边形面积的问题

    朋友那边最近有个需求,需要框选一个选区,然后根据选区中的点求出面积.并且让我尝试用Delaunay来解决 似乎音译过来应该是德诺类 大致如下: 我在github上找了一个可以用的Delaunay库 h ...