深度揭祕亂碼問題背後的原因及解決方式

weixin_33890499發表於2017-02-24
1111670-2d6c38de8bef1c10

做Web開發的IT人,如果工作中沒遇到過幾次亂碼的問題,估計都不好意思說自己是開發工程師。 :)

而亂碼問題也是各種各樣,有儲存到資料庫中是亂碼的,有在服務端接收到引數是亂碼的,有在後臺返回到客戶端時候出現亂碼的……

這形形色色的亂碼問題,如果處理起來不得要領,著實會讓開發人員費不少工夫。
本文,將從亂碼的產生原因,應用伺服器內部對引數的處理各方面詳解原理及解決方案。

1編碼

在開發中,只要有IO的地方,都會涉及到編碼。例如下面的程式碼:

System.out.println( "Hello 中國" );

你猜這個會輸出什麼呢?
這個其實是和你的檔案編碼有很大關係的,可能會輸出
Hello 中國

也有可能會輸出成下面這個樣子

Hello ���

你可以改動IDE中的檔案編碼重複試幾次。那為什麼會產生這個原因呢?
我們來看這行程式碼執行過程中的呼叫棧:

1111670-0d64e0f9f59c8500

我們看到,簡單的一句輸出,也是有編碼的
看看CharsetEncoder,實現類真多啊
1111670-bb55462b7d40a2e5

再比如我們都無比熟悉的equals方法,先宣告常量STR如下:
1111670-eb84db6ecb68945e.png
之後,程式碼中有如下的邏輯
1111670-eb84db6ecb68945e.png

你認為,這個時候會有輸出嗎?
答案是看情況

當我把Constant類以UTF-8為編碼儲存後,把包含if邏輯的程式碼以GBK儲存之後,equals執行時比較的兩個引數就變成了下面的樣子:


1111670-ae6a40f3342971c4

所以這一定是不會為true的。
這也是亂碼產生的原因,


原因即解碼時採用的encoding與編碼時用的encoding不一致所造成的。

2 應用伺服器內的亂碼

Web應用中,亂碼的成因和上述分析是一致的
我們嚮應用伺服器傳送一個這樣的請求:

http://localhost:8080/test/servlet?abc=你好

伺服器用request.getParameter("abc")來獲取,這個時候有亂碼問題嗎?
答案依然是It depends.

這次的看情況是要看哪些情況呢?有以下這些。

  • Tomcat的版本

  • 是否單獨設定通道的編碼

  • 是否有使用統一的編碼Filter

Tomcat預設對於不同的通道(Connector),都可以獨立設定相關的編碼屬性,例如預設是下面這樣的配置:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

而我們可以增加自定義的編碼配置:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" **URIEncoding**="UTF-8"/>

紅色的URIEncoding即為新增的屬性,這個引數是和Tomcat的版本有關係的。
在Tomcat8中,其對應的官方文件是這樣說明的:

This specifies the character encoding used to decode the URI bytes, after %xx decoding the URL. If not specified, UTF-8 will be used unless the org.apache.catalina.STRICT_SERVLET_COMPLIANCE
system property is set to
true
in which case ISO-8859-1 will be used.

也就是不設定
-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=true
那預設的編碼會採用UTF-8。
但是在Tomcat的8.0之前版本官方文件裡是這樣寫的說明:

This specifies the character encoding used to decode the URI bytes, after %xx decoding the URL. If not specified, ISO-8859-1 will be used.

也就是不特殊指定,預設將使用ISO-8859-1進行編碼。

看Connector的構造方法中,也是明確按此進行配置的

public Connector(String protocol) {setProtocol(protocol);...**//注意下面的程式碼**if (!Globals.STRICT_SERVLET_COMPLIANCE) {URIEncoding = "UTF-8";URIEncodingLower = URIEncoding.toLowerCase(Locale.ENGLISH);}}

這個配置又是如何作用於引數解析的呢?看下面

public void service(org.apache.coyote.Request req,org.apache.coyote.Response res)throws Exception {// Set query string encodingreq.getParameters().setQueryStringEncoding(connector.getURIEncoding()); 

//注意這裡,具體去設定的是Parameters類的queryStringEncoding,這個屬性會在後面解析URL中包含的引數時用到。}
public void setQueryStringEncoding( String s ) {queryStringEncoding=s;}

而具體引數處理時,傳進去的就是這個queryStringEncoding
processParameters( decodedQuery, queryStringEncoding );
這種情況,在Tomcat8中就不需要再顯示的配置URIEncoding了,而之前的版本則需要配置。有上面的程式碼參照,我們看到,對於URL中傳入的引數,除了設定URIEncoding這個配置之外,是沒有辦法保證的。因為其解析引數時使用的是queryStringEncoding這個引數,因此只有才保證傳到Tomcat的引數編碼和解碼正確了

而如果配置了統一的編碼過濾器,則過濾器內設定request的編碼一定要在解析引數前,即呼叫getParameter前設定,否則並不生效。這是因為parameter只會解析一次,之後就放到一個List中直接根據key返回了。

那是不是設定一個統一的編碼Filter,一切就萬事大吉了呢?

答案還是看情況吧?
恭喜,你會搶答啦!

3JSP亂碼問題

我們都知道在jsp中,可以設定這樣一個jsp頭
<%@ page contentType="text/html;charset=iso-8859-1" language="java" %>
那這個時候如果你的頁面中要輸出一些返回的中文資料,這個時候,頁面妥妥的出現了亂碼。原因自然是iso-859-1不支援中文有關。注意這裡charset不寫依然是按iso-8859-1為預設值。

這時,你想到了Filter。在Filter中你大膽的設定了
resp.setCharacterEncoding(encoding);
這個時候,頁面展示卻依然華麗的亂碼了。擦,這是啥原因?

那這個時候,在jsp中顯示資料的時候,依然還是會出現亂碼的,此時注意觀察下響應頭:


1111670-2aa1de540f1ace53

看下面的程式碼,由於response在輸出的時候。會獲取設定的encoding

/** * Return the writer associated with this Response. * * @exception IllegalStateException if <code>getOutputStream</code> has * already been called for this response * @exception IOException if an input/output error occurs */@Overridepublic PrintWriter getWriter()throws IOException {if (usingOutputStream) {throw new IllegalStateException(sm.getString("coyoteResponse.getWriter.ise"));}if (ENFORCE_ENCODING_IN_GET_WRITER) {/* * If the response's character encoding has not been specified as * described in <code>getCharacterEncoding</code> (i.e., the method * just returns the default value <code>**ISO-8859-1**</code>), * <code>getWriter</code> updates it to <code>ISO-8859-1</code> * (with the effect that a subsequent call to getContentType() will * include a charset=ISO-8859-1 component which will also be * reflected in the Content-Type response header, thereby satisfying * the Servlet spec requirement that containers must communicate the * character encoding used for the servlet response's writer to the * client). */ **setCharacterEncoding**(getCharacterEncoding());}usingWriter = true;outputBuffer.checkConverter();if (writer == null) {writer = new CoyoteWriter(outputBuffer);}return writer;}

而這個encoding是什麼設定的呢?

public void setContentType(String type) {
if (isCommitted()) {return;}if (SecurityUtil.isPackageProtectionEnabled()){AccessController.doPrivileged(new SetContentTypePrivilegedAction(type));} else {response.**setContentType**(type); //這裡在設定contentType,由於配置中同時包含charset}}

String[] m = MEDIA_TYPE_CACHE.parse(type); //這個是在解析contentType引數if (m == null) {// Invalid - Assume no charset and just pass through whatever // the user provided. coyoteResponse.setContentTypeNoCharset(type);return;}coyoteResponse.setContentTypeNoCharset(m[0]);if (m[1] != null) {// Ignore charset if getWriter() has already been called if (!usingWriter) {coyoteResponse.setCharacterEncoding(m[1]); //這裡就用解析出來的引數設定。isCharacterEncodingSet = true;}}

而我們一般為了處理這種亂碼問題統一寫的filter,在請求處理前就先把request和response的encoding都設定好。而這裡預設提供的charset為ISO-8859-1,就出了亂碼問題了。另外一個在處理JSP時容易出的問題,就是contentType中指定的charset和filter中已經設定的encoding,兩者不一致,比如你的jsp中忘記改了,使用的是預設的ISO-8859-1.此時,先通過filter設定的encoding會先於contentType的設定執行,因此,依然會出現亂碼問題。

4總結

在本文中,深入分析了亂碼背後產生的原因:編碼和解碼時採用的encoding不一致。而解決問題的最樸素的道理就是保持多種資料來源編碼的一致性,無論是資料庫的,檔案的,還是輸入輸出的,都採用一致的編碼,可以簡少很多問題。另外,許多檔案中有一些預設編碼,開發中可能不太注意,此處也是容易出現問題的地方。

相關文章