一、问题背景
我在浏览器端使用 GBK 编码,以 GET 方式向 Servlet 发送了一个 request(包含中文),结果无论我怎么解码,都无法还原回原来的中文。
二、解决方案
- 我们需要去
%CATALINA_HOME%/conf/server.xml
,增加一行配置:useBodyEncodingForURI="true"
- 在 Servlet 获取属性前,先调用
req.setCharacterEncoding("GBK");
,如上问题,则使用GBK
编码。
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
useBodyEncodingForURI="true"
redirectPort="8443" />
三、方案解析
- 以GET 方式,Tomcat 会对属性先进行一次解码,解码方式为 UTF-8,而我们提交的属性,是以 GBK 进行编码,二者编码不一致,故乱码;
- 只设置
req.setCharacterEncoding("GBK");
,对该问题是没作用的,因为该接口只会影响 Body 部分的编码,而以 GET 方式提交,属性是携带在请求头。Tomcat 对 header 跟 body 的编解码方式是不一样的,后文会提到。 - 在设置
req.setCharacterEncoding("GBK");
的基础上,我们可以再设置useBodyEncodingForURI="true"
,强制让 header 以解码 body 的编码方式进行解码,那么req.setCharacterEncoding("GBK");
这个设置就能生效了。
四、其他编解码的知识点
在解决这个问题的时候,我对编解码有了一个较为全面的认知,此处再简略提一些有关于编解码的知识点,以供读者解决相关问题时提供一些思路。
- Tomcat header 跟 body 的编解码方式不一样。 以下代码是我去Tomcat 源码上摘抄的,说明 Tomcat 原本就有这样的设计:如果不指定编码格式,那么对于 header,将默认采用 UTF-8 进行编解码,对于 body 将默认采用 ISO_8859_1进行编解码
public static final Charset DEFAULT_URI_CHARSET = StandardCharsets.UTF_8;
public static final Charset DEFAULT_BODY_CHARSET = StandardCharsets.ISO_8859_1;
- Tomcat 对 header 默认采用 UTF-8 进行编解码,是从 Tomcat 8 开始的,之前的版本 还是默认采用 ISO_8859_1 进行编解码(由于笔者是用 Tomcat 9,此点未进行验证)
- 浏览器显示时的编码,跟浏览器接收到 Response 的进行解析的编码,可能是不一样的(这个可以阅读下文章末尾的引用)
req.setCharacterEncoding()
跟resp.setCharacterEncoding()
均只对 body 部分的编解码有作用,指定了resp.setCharacterEncoding()
,那么在浏览器解码该响应时,必定不会乱码,但浏览器正文会不会乱码,就不一定了。
- 如果想确保浏览器不乱码,那么还需要设置
resp.setContentType("text/html;charset=UTF-8");
,告诉浏览器以什么方式解码。 - 如下图,针对解码后再编码,发现跟原始数据不一致的情况,是因为乱码后不可逆了,对于非乱码的字符,还是可逆的。如图代码
URLDecoder.decode("%C4%D0", "UTF-8");
(%C4%D0
是URLEncoder.encode("男", "GBK");
的结果)解码的字符为“�”(编码为0xFFFD
),这个字符是 UTF-8 转码失败后的默认字符,因此是多对一的关系,不可能逆向编码回原来的编码。所以遇到这种情况,除非能拿到原始的编码数据(或者对 Tomcat 的解码过程找到相对应的钩子方法进行干涉,例如本文的解决方案),否则这种乱码问题就解决不了了。
// java.nio.charse.CharsetDecoder.java
protected CharsetDecoder(Charset cs,
float averageCharsPerByte,
float maxCharsPerByte)
{
this(cs,
averageCharsPerByte, maxCharsPerByte,
"\uFFFD");
}