• [转]httpclient编码


    这几天都在纠结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的代码。

  • 相关阅读:
    使用 Responsive Elements 快速构建响应式网站
    分享25个新鲜出炉的 Photoshop 高级教程
    【特别推荐】Node.js 入门教程和学习资源汇总
    高清精美壁纸:2013年11月桌面日历壁纸免费下载
    Allocation-Free Collections(在堆栈上使用内存)
    李愬雪夜袭蔡州:挽救唐朝国祚的关键之战(先示弱于敌军,对降将攻心为上、以诚待人,然后逐步收集情报,最后出奇兵奇袭。但出奇兵也要有预案,否则会被两面夹击。做一件事情真不容易)
    C 与 C++ 谁的效率高,为什么?(结论是,两个不同层次的语言不应该相互比较,非要比较的话,是不同情况下各有快慢)
    在Win10上,Android Studio检测不到设备的解决方案
    web系统整体优化
    服务器间通讯问题的排查
  • 原文地址:https://www.cnblogs.com/dirgo/p/7607318.html
Copyright © 2020-2023  润新知