JavaWeb基础之编码问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lqzkcx3/article/details/82556455

说是编码问题,准确来说是中文问题,因为英文用户是没有类似困扰的。

1. 概述

平常我们在遇到编码问题时,都是一通百度,然后一顿复制粘贴之后,发现字符串似乎正常了,然后就认为问题得到了解决,弃而不管。既然大家都知道这种行事方式是不对的,那么本文将通过对收集到的资料和实践来尝试探究这其中的知识点。

2. 为何要编码

  1. 计算机中存储信息的最小单位为一个字节,即8个bit。所以能表示的字符范围是0~255。
  2. 但人类需要表示的符号太多,无法用一个字节来完全表示。

为了解决以上矛盾,于是出现了一个新的数据结构char(16bit长度),而从charbyte必须进行编码。

3. 常见编码

  1. ASCII
    单字节编码。共计128个,其中 0 ~ 31 为控制字符,32 ~ 126是打印字符。
  2. ISO-8859-1
    依然是单字节编码,共计能表示256个字符。是目前大部分框架的首选字符集。
  3. GB2312 / GBK
    GBK兼容GB2312,编码算法一致,只是GBK包含更多的汉字字符。
    经过GB2312编码的汉字都可以用GBK进行解码,反之则不然。
  4. UTF-8
    最理想的中文编码方式,不论从编码效率还是在安全性上。
  5. UTF-16
    表示字符方便,同时也导致空间占用大,不利于网络传输。

4. 一次请求涉及到的编码

终于到了本文的核心内容了,先让我们通览下在一次请求中涉及到的编解码位置:

  1. 浏览器端发起一个HTTP请求,涉及到编码的是URL,Cookie, Paramerter。
  2. 服务器端接收到该请求,其中URI,Cookie和POST表单参数需要解码
  3. 服务器端涉及到的读取数据库或者本地/网络上的其他文件,这些数据也牵扯到编码问题。
  4. 服务端处理完请求,还需要将返回值进行编码,通过Socket发送给客户端浏览器,再经过浏览器解码成为文本。

大致流程可以参见如下的图,相信读者一眼就看出来了,此图非原创。
《深入分析JavaWeb技术内幕》P75

4.1 URL

首先让我们看看URL的组成部分。
URL组成 - 《深入分析JavaWeb技术内幕》P76

以下我们将以IE作为测试对象,研究编解码对我们程序的影响

首先本次进行测试的相关链接为 : http://localhost:9999/strutsDemo/user_夫礼者.action?name=夫礼者

而其在IE浏览器下表现如下(PathInfo为UTF-8编码,而QueryString则是进行的GBK编码):
IE浏览器

相应的后台代码如下:

public void 夫礼者() throws Exception{
    HttpServletRequest request = ServletActionContext.getRequest();
    String parameter = request.getParameter("name");
    // 先进行GBK解码, 再进行UTF-8编码     
    String parameter2 = new String("夫礼者".getBytes("GBK"),CharsetUtil.CHARSET_UTF_8);    
    // ������
    System.out.println(parameter2);
    // ������
    System.out.println(parameter);
    // true
    System.out.println(parameter2.equals(parameter));
    // false
    System.out.println(parameter2.equals("夫礼者"));
}

由以上的测试结果我们可以得出如下结论:

  1. 每种浏览器对PathInfo和QueryString的编码格式有自己的选择。

    1. Chrome, 都是UTF-8
    2. IE ,前者是UTF-8 而后者是GBK,
    3. FireFox,待测试。
  2. 虽然在IE下有相应的配置,但似乎并未生效。(IE与Tomcat与HTTP1.1
  3. 请求的URL传递到后端,解析工作是在org.apache.coyote.http11.InternalInputBuffer.parseRequestLine()中进行的,这个方法将把前端传递过来的URL的byte[]设置到org.apache.coyote.Request的相应属性中。注意此时的URL依然是byte格式,转成char的操作是在CoyoteAdapter.convertURI()中完成的。
  4. CoyoteAdapter.convertURI()中,Tomcat会获取用户在${CATALINA_HOME}/conf/server.xml中配置的<Connector URIEncoding="utf-8" /> 属性来完成 byte到char的转换。
  5. 而对于<Connector/>可配置的另外一个属性useBodyEncodingForURI则仅仅是对URL中的QueryString部分起效——即对QueryString使用BodyEncoding解码,这一点千万注意。
  6. 最佳实践<Connector URIEncoding="utf-8" useBodyEncodingForURI="true" />
4.2 Http Header

Cookie,redirectPath就是Header中的一员。

我们使用Fiddler进行如下测试:
修改Header

相应的后端代码如下:

final String contentType = request.getHeader("Content-Type");
// 先进行ISO-8859-1编码, 再进行UTF-8解码; 就正常了
String contentType_successDecode = new String(contentType.getBytes("ISO-8859-1"),CharsetUtil.CHARSET_UTF_8);
// application/json;charset=GBK;lq=夫礼è
System.out.println(contentType);
// application/json;charset=GBK;lq=夫礼者
System.out.println(contentType_successDecode);

通过以上,我们可以得出以下结论:

  1. Header默认是使用ISO-8859-1来进行编码的,服务端(Tomcat)以相应的格式进行解码。所以默认情况下,中文将会乱码。
  2. 对Header中各项的解码工作是在调用request.getHeader时进行的(惰性求值)。
  3. 以上解码的过程中,将调用MessageBytes.toString()方法,而通过跟踪其细节, MessageBytes内部是通过调用 ByteChunk.toString 来完成解码的, 而ByteChunk 内部有个字段 public static final Charset DEFAULT_CHARSET = B2CConverter.ISO_8859_1——正是通过它来进行解码。注意ByteChunk是提供了setCharset方法的。
  4. Cookie的解析过程中伴随着校验(源码位置:Cookies.processCookieHeader()方法的第340行,更准确点说是在CookieSupport.isV0Separator()中),所以如果包含中文,则将直接抛出异常。
  5. 最佳实践:不要在Header中传递非ASCII字符;如果一定要传递,可以先将这些字符用org.apache.catalina.util.URLEncoder进行编码来确保信息不丢失。
4.3 POST表单
  1. 以GET方式HTTP请求的QueryString和以POST方式HTTP请求的表单参数都是作为Paramters保存的,都通过request.getParameter来获取相应的参数值。对他们的解码则是在request.getParameter第一次被调用时进行的(源码位置:org.apache.catalina.connector.Request.getParameter(),里面很清晰地展示了惰性求值的逻辑,更具体的参见下方的源码补充)。
  2. POST表单参数是通过HTTP的BODY传递到服务端的,这一点与QueryString是不同的。
  3. 当我们在页面上单击提交按钮时,浏览器首先根据ContentType的Charset编码格式(默认情况下,)对在表单中填入的参数进行编码,然后提交到服务器端,在服务器端同样也是用ContentType中的字符集进行解码的。所以通过POST表单提交的参数一般不会出现问题,而且这个字符集是我们可以设置的——request.setCharacterEncoding
  4. 结合第一条和第三条,你一定要在第一次调用request.getParameter方法之前就设置好request.setCharacterEncoding
  5. 最佳实践:除非特殊情形,禁止开发人员调用request.setCharacterEncodingresponse.setCharacterEncoding方法,并以javax.servlet.Filter的形式(例如Spring提供的 org.springframework.web.filter.CharacterEncodingFilter)来完成编码格式的设置工作。
4.4 Http Body

用户请求的资源获取完毕之后,所获取到的内容将通过Response返回给客户端浏览器。这个过程涉及到先编码,再到浏览器解码的流程逻辑。

  1. Response编码字符集可以通过response.setCharacterEncoding来设置,它将覆盖request.setCharacterEncoding的值,并且通过Header的Content-Type返回客户端。
  2. 浏览器接收到返回的Socket流时将通过Content-Type的charset来解码。
  3. 如果返回的HTTP Header中Content-Type没有设置charset,那么浏览器将根据HTML中的<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />中的charset来解码。
  4. 如果以上都不满足,浏览器将使用默认的编码格式来解码。

5. 补充

  • Java中一个char是16个bit,相当于两个字节。所以如果两个汉字用char表示,将占据4个字节的空间。
  • 一段文本的大小,只看字符本身的长度是没有意义的;一样的字符采用不同的编码最终存储的大小也会不同。这一点在作文本压缩时需要特别留意。
  • 以下链接中关于字符集的文章是本人在三年多前收集的资料,虽然年代有些久远,但并不影响其权威性,毕竟这个领域更新换代波及面广,所以迭代速度慢。
  • 针对multipart/form-data类型的参数,也就是上传的文件编码,同样也使用ContentType定义的字符集编码。值得注意的是,上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及字符编码,而真正编码是在将文件内容添加到parameters中时,如果用这个不能编码,则将会使用默认编码ISO-8859-1来编码。
  • 开发人员必须清楚的servlet规范:

    1. HttpServletRequest.setCharacterEncoding()方法 仅仅只适用于设置post提交的request body的编码而不是设置get方法提交的queryString的编码。该方法告诉应用服务器应该采用什么编码解析post传过来的内容。很多文章并没有说明这一点。
    2. HttpServletRequest.getPathInfo()返回的结果是由Servlet服务器解码(decode)过的。【本人测试返回值为null】
    3. HttpServletRequest.getRequestURI()返回的字符串没有被Servlet服务器decoded过。
    4. POST提交的数据是作为request body的一部分。
    5. 网页的Http头中ContentType("text/html; charset=GBK")的作用:

      • 告诉浏览器网页中数据是什么编码;
      • 表单提交时,通常浏览器会根据ContentType指定的charset对表单中的数据编码,然后发送给服务器的。
      • 这里需要注意的是:这里所说的ContentType是指http头的ContentType,而不是在网页中meta中的ContentType。
  • 通过跟踪源码,我们发现struts2的 org.apache.struts2.dispatcher.Dispatcher.prepare() 中,如果发现当前请求是Ajax,则强制使用UTF-8。
  • 通过跟踪源码,我们发现Tomcat在获取对request进行解码的字符格式时,会参考HTTP的Content-Type Header。细节参见下方的源码Request.getCharacterEncoding()

6. 相关源码

正所谓“talk is cheap, show me the code”

// -------- org.apache.catalina.connector.Request.getParameter
/**
 * Return the value of the specified request parameter, if any; otherwise,
 * return <code>null</code>.  If there is more than one value defined,
 * return only the first one.
 *
 * @param name Name of the desired request parameter
 */
@Override
public String getParameter(String name) {
    // 惰性解析
    if (!parametersParsed) {
        // 如果对于query paramters 的字符编码解析有疑问, 可以在如下方法中断点
        parseParameters();
    }

    return coyoteRequest.getParameters().getParameter(name);

}

// -------- org.apache.catalina.connector.Request.parseParameters
/**
 * Parse request parameters.
 */
protected void parseParameters() {

    parametersParsed = true;

    Parameters parameters = coyoteRequest.getParameters();
    boolean success = false;
    try {
        // Set this every time in case limit has been changed via JMX
        parameters.setLimit(getConnector().getMaxParameterCount());

        // getCharacterEncoding() may have been overridden to search for
        // hidden form field containing request encoding
        // 获取设置的编码
        String enc = getCharacterEncoding();
        // 通过配置文件设置的; useBodyEncodingForURI
        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
        if (enc != null) {
            parameters.setEncoding(enc);
            if (useBodyEncodingForURI) {
                parameters.setQueryStringEncoding(enc);
            }
        } else {
            // 使用默认的"ISO-8859-1"
            parameters.setEncoding
                (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
            if (useBodyEncodingForURI) {
                parameters.setQueryStringEncoding
                    (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
            }
        }

        parameters.handleQueryParameters();

        ...
        }

        ...
}

// -------- org.apache.catalina.connector.Request.getCharacterEncoding
/**
 * Get the character encoding used for this request.
 */
public String getCharacterEncoding() {

    if (charEncoding != null)
        return charEncoding;

    // HTTP头部, 从Content-Type里取
    charEncoding = ContentType.getCharsetFromContentType(getContentType());
    return charEncoding;
}
  1. 《深入分析Java Web技术内幕》 P61
  2. Java中文编码小结
  3. 浏览器URL编码
  4. 深入浅出URL编码
  5. Windows 记事本的 ANSI、Unicode、UTF-8 这三种编码模式有什么区别?
  6. 字符,字节和编码
  7. Wiki - UTF-8
  8. UTF-8编码规则(转)

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/82556455