这几天都在纠结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. Snip for Mac(桌面截图工具)安装

    1.软件简介    Snip 一款用于桌面截图的工具. 2.资源列表 链接 提取密码 系统要求 软件语言 Snip for Mac v2.0 (5771) fgab macOS 10.6.8 及以上 ...

  2. 利用css如何让嵌套的div层不继承父div层的透明度?

    http://zhidao.baidu.com/link?url=cvQhh0Q7_ah0qg9tc-2zP0cjB_PoIiIq6t6RFpp4aZPPNoVJUqyy7TT41TU5pWzRtRY ...

  3. Activity的onPause()、onStop()和onDestroy()里要做的事情

    onPause(): 当系统调用你的activity中的onPause(),从技术上讲,那意味着你的activity仍然处于部分可见的状态,当时大多数时候,那意味着用户正在离开这个activity并马 ...

  4. MFC动态按钮的创建及其消息响应 和 自定义消息

    原文链接: http://www.cnblogs.com/gaohongchen01/p/4046525.html 动态按钮(多个)的创建: 1.在类中声明并定义按钮控件的ID #define IDC ...

  5. T-SQL查询进阶--理解SQL Server中索引的概念,原理以及其他

    简介 在SQL Server中,索引是一种增强式的存在,这意味着,即使没有索引,SQL Server仍然可以实现应有的功能.但索引可以在大多数情况下大大提升查询性能,在OLAP中尤其明显.要完全理解索 ...

  6. Another app is currently holding the yum lock; waiting for it to exit.. yum被锁定无法使用

    yum被锁定无法使用 Another app is currently holding the yum lock; waiting for it to exit.. 解决方法: rm -rf /var ...

  7. linux命令分享一:压缩命令

    Linux操作系统中,*.zip.*.tar.*.tar.gz.*.tar.bz2.*.tar.xz.*.jar.*.7z等格式的压缩与解压 zip格式 压缩: zip -r [目标文件名].zip ...

  8. 基于Docker的负载均衡和服务发现

    应用的容器化和微服务化带来的问题 在缺省网络模型中,容器每次重启后,IP会发生变动,在一个大的分布式系统保证IP地址不变是比较复杂的事情 IP频繁发生变动,动态应用部署无法预知容器的IP地址,clie ...

  9. 腾讯云服务器 - 定时备份MariaDB/MySQL

    数据库数据备份尤为重要,而我们不会人工手动去备份,这样会很麻烦,我们都是通过服务器每日自定运行来做的,设置一个定时时间即可 首先我们看一下mysqldump这个文件的位置: 可以看到目录在 /usr/ ...

  10. webpack配置实践

    首先我们的需求: 打包调试 提取公共代码 压缩 热替换 1.打包调试 第一步,我们在目标文件夹下安装webpack(假设已有package.json)npm i webpack@ -gcnpm i w ...