Java Web中的中文編碼問題分析

一、爲什麼需要編碼

    在計算機中存儲信息的最小單位是1個字節,即8bit,所以能標識的最大字符範圍是0~255,而人類自然語言中例如漢語、日語要表示的符號太多,無法單純用一個字節來完全表示,爲了解決這個矛盾必須要有一個新的人類可識別的數據存儲結構字符,而從char到byte必須編碼。

 
二、常見的編碼格式

1 - ASCII碼

    總共128個,使用一個字節的低7位表示,0~31是控制字符如換行、回車、刪除等,32~126是打印字符,可以通過鍵盤輸入並且能夠顯示出來

 

2 - ISO-8859-1

    ISO組織在ASCII碼的基礎上利用了單個字節的所有位定製了一系列標準擴展了ASCII編碼,ISO-8859-1仍然是單字節編碼,總共可以表示256個字符

 

3 - GB2312

    GB2312全稱是《信息技術中文編碼字符集》,它採用了雙字節編碼,總的編碼範圍是A1~F7,其中A1~A9是符號區,總共包含682個符號;B0~F7是漢字區,包含6763個漢字。

 

4 - GBK

    GBK全稱是《漢字內碼擴展規範》是國家技術監督局位windows 95制定的新的內碼規範,它的出現是爲了擴展GB2312,並加入更多的漢字,它的編碼範圍是8140~FEFE,總共有23940個碼位可以表示21003個漢字,它的編碼與GB2312兼容,也就是說採用GB2312編碼的漢字均可以通過GBK去解碼,並且不會有亂碼。

5 - UTF-16

    UTF-16定義了Unicode字符在計算機中的存取方法,UTF-16使用兩個字節表示Unicode的轉化格式,這是一個定長的表示方法,無論什麼字符UTF-16都用兩個字節表示,2個字節就是16位,所以稱之爲UTF-16,UTF-16表示字符非常方便,每兩個字節表示一個字符,在字符串操作的時候大大簡化了操作,這個也是Java以UTF-16作爲內存的字符存儲格式的一個很重要的原因。

Unitcode使用UTF-16編碼規則如下:

    UTF-16編碼以16位無符號整數爲單位,我們

    

 

6 - UTF-8

    UTF-16統一採用兩個字節表示一個字符,雖然在表示上非常簡單方便,但是也有其缺點,有很大一部分字符使用一個字節就可以表示的現在需要兩個字節表示,存儲空間增大了一倍,在現在網絡帶寬還非常有限的今天,這樣做無疑會增大網絡傳輸的流量,而且也沒什麼必要,而UTF-8採用的是一種變長的技術,每種編碼區域都會有不同的字節長度,不同類型的字符可以有1~6個字節組成。

UTF-8有以下編碼規則

    1.如果是1個字節,最高爲(第8位)爲0,則表示這是1個ASCII字符(00~7F)。可見,所有ASCII編碼已經是UTF-8了。

    2.如果是1個字節,以11開頭,則連續的1的個數暗示這個字符的字節數,例如:110xxxxx代表它是雙字節UTF-8字符的首字節。

    3.如果是1個字節,以10開始,表示它不是首字節,則需要向前查找才能得到當前字符的首字節。

 
三、在Java中需要編碼的場景

1 - 在IO操作中存在的編碼

    我們知道在涉及編碼的地方一般都在從字符到字節或者是從字節到字符的轉化過程中,而需要這種轉化的場景主要是I/O,而這個IO主要包括磁盤IO和網絡IO。

    在Java中負責在IO過程中處理字節到字符轉換的是InputStreamReader,它繼承自Reader,在類創建時關聯了一個字節輸入流InputStream對象,對具體字節到字符的轉換它主要委託給內部的StreamDecode去做,StreamDecoder在解碼過程中需要基於指定的Charset獲取對應的CharsetDecoder將字節流爲字符,如果沒有指定將使用操作系統默認的編碼方式,在中文環境中通常時GBK。相反在java中關聯字符到字節轉換的橋樑是OutputStreamWriter,他繼承自Writer,在類創建時關聯了字節輸出流OutputStream對象,對具體字符到字節的轉換它主要委託給內部的StreamEncoder,StreamEncoder獲取對應Charset的CharsetEncoder編碼器將字符編碼位字節,如果沒有指定字符集Charset通常採用本地操作系統默認的字符集進行編碼。

    在我們的應用程序中涉及I/O操作時,只要注意指定統一的編解碼Charset字符集,一般不會出現亂碼問題。

    對有些應用程序如果不注意指定字符編碼,則在中文環境中會使用操作系統默認編碼。如果編解碼都在中文環境中,通常也沒有問題,但還是不推薦使用操作系統的默認編碼,因爲這樣會使你的應用程序的編碼格式和運行環境綁定起來,在跨環境時很可能出現亂碼問題。

 

2 - 在內存操作中的編碼

    在Java開發中除I/O涉及編碼外,最常用的應該就是在內存中進行從字符到字節的數據類型轉換,在Java中用String表示字符串,所以String類就提供了轉換到字節的方法,也支持將字節轉換爲字符串的構造函數。

    String s = "這是一段中文字符串";
    byte[] b = s.getBytes("UTF-8");  
    String str = new String(b,"UTF-8");

    在程序中這三行代碼一共經歷了以下過程:

1)UTF-16輸入流到Unicode的解碼(在JVM中發生)

2)Unicode到UTF-8編碼的輸出流

3)UTF-8輸入流到Unicode的解碼

4)Unicode到UTF-16的編碼(在JVM中發生)

思考下代碼最終執行結果字節數組a與b是否相同,結合Java默認的編碼方式以及JVM內部默認採用存儲漢字的編碼方式,分析這一過程

    byte[] a = new byte[]{(byte) 0xc6, (byte) 0xd0};
    String s = new String(a);
    byte[] b = s.getBytes();

Charset提供encode與decode,分別對應char[]到byte[]的編碼和byte[]到char[]的解碼。

    String s = "這是一段中文字符串";
    Charset charset = Charset.forName("UTF-8");
    ByteBuffer byteBuffer = charset.encode(s);
    CharBuffer charBuffer = charset.decode(byteBuffer);

 
四、在Java中如何進行編碼和解碼

    Java中涉及編碼的類圖如下:

 

下圖是String.getBytes(String charsetName)對應的時序圖

 

 

    由圖可知,String.getBytes(String charsetName)編碼基本流程如下:

1)根據charsetName找到Charset類,然後根據這個字符集編碼生成CharsetEncoder,這個類是所有字符編碼的父類,針對不同的字符編碼集在charset中定義了獲取對應CharsetEncoder的方法;

2)基於獲取到的編碼器CharsetEncoder對當前字符串進行編碼

 

1 - 按照ISO-8859-1編碼方式編碼

字符串“I am 君山”用ISO-8859-1編碼時,編碼結果如圖:

 

    可以看出,7個 char 字符經過 ISO-8859-1 編碼轉變成7個 byte 數組,ISO-8859-1 是單字節編碼,中文“君山”被轉化成值是 3f 的 byte。3f 也就是“?”字符,所以經常會出現中文變成“?”,很可能就是錯誤的使用了 ISO-8859-1 這個編碼導致的。中文字符經過 ISO-8859-1 編碼會丟失信息,通常我們稱之爲“黑洞”,它會把不認識的字符吸收掉。由於現在大部分基礎的 Java 框架或系統默認的字符集編碼都是 ISO-8859-1,所以很容易出現亂碼問題。

 

2 - 按照GB2312編碼

字符串“I am 君山”用GB2312編碼時,編碼結果如圖:

GB2312 對應的 Charset 是 sun.nio.cs.ext.EUC_CN,而對應的 CharsetEncoder是 sun.nio.cs.ext.DoubleByte.Encoder,我們進入該類encodeLoop方法的源碼:

            protected CoderResult encodeLoop(CharBuffer var1, ByteBuffer var2) {
                return var1.hasArray() && var2.hasArray() ? this.encodeArrayLoop(var1, var2) : this.encodeBufferLoop(var1, var2);
            }

    這裏只是簡單的做了基本要素的判空繼續進入encodeArrayLoop方法

            protected CoderResult encodeArrayLoop(CharBuffer var1, ByteBuffer var2) {
                char[] var3 = var1.array();
                int var4 = var1.arrayOffset() + var1.position();
                int var5 = var1.arrayOffset() + var1.limit();
                byte[] var6 = var2.array();
                int var7 = var2.arrayOffset() + var2.position();
                int var8 = var2.arrayOffset() + var2.limit();
     
                try {
                    while(true) {
                        if (var4 < var5) {
                            char var15 = var3[var4];
                            int var10 = this.encodeChar(var15);
                            CoderResult var11;
                            if (var10 != 65533) {
                                //若大於255則證明是雙字節字符,雙字節字符高8位作爲第1個字節存儲,低8位作爲第2個字節存
                                //儲,
                                if (var10 > 255) {
                                    if (var8 - var7 < 2) {
                                        var11 = CoderResult.OVERFLOW;
                                        return var11;
                                    }
     
                                    var6[var7++] = (byte)(var10 >> 8);
                                    var6[var7++] = (byte)var10;
                                } else {//否則是單個字節字符,直接編碼作爲單個字節存儲
                                    if (var8 - var7 < 1) {
                                        var11 = CoderResult.OVERFLOW;
                                        return var11;
                                    }
     
                                    var6[var7++] = (byte)var10;
                                }
     
                                ++var4;
                                continue;
                            }
            ......
       }

    這個方法代碼挺長,我們略過非核心內容可以看到真正進行字符編碼的地方是在圖中註釋處的代碼,我們看到他調用了本類的encoderChar方法繼續進入該方法進行後續分析:

            public int encodeChar(char var1) {
                return this.c2b[this.c2bIndex[var1 >> 8] + (var1 & 255)];
            }

    到這裏,Java中GB2312的大致編碼流程就很清晰了,GB2312 字符集有一個 char 到 byte 的碼錶,通過這個碼錶獲取每個字符對應的碼位值var10,再通過對這個碼位值進行判斷,如果大於255,則基於高位編址法取它的高8位作爲第一個字節存放,低8位作爲第2個字節,由此可見GB2312的編解碼其實是基於碼錶進行的。

 

3 - 按照GBK進行編碼

字符串“I am 君山”用GBK編碼時,編碼結果如圖:

    你可能已經發現,上圖與 GB2312 編碼的結果是一樣的,沒錯,GBK 與 GB2312 編碼結果是一樣的,由此可以得出 GBK 編碼是兼容 GB2312 編碼的,它們的編碼算法也是一樣的。不同的是它們的碼錶長度不一樣,GBK 包含的漢字字符更多。所以只要是經過 GB2312 編碼的漢字都可以用 GBK 進行解碼,反過來則不然。

 

4 - 按照UTF-16編碼

字符串“I am 君山”用UTF-16編碼時,編碼結果如圖

    用 UTF-16 編碼將 char 數組放大了一倍,單字節範圍內的字符,在高位補 0 變成兩個字節,中文字符也變成兩個字節。從 UTF-16 編碼規則來看,僅僅將字符的高位和地位進行拆分變成兩個字節。特點是編碼效率非常高,規則很簡單,由於不同處理器對 2 字節處理方式不同,Big-endian(高位字節在前,低位字節在後)或 Little-endian(低位字節在前,高位字節在後)編碼,所以在對一串字符串進行編碼是需要指明到底是 Big-endian 還是 Little-endian,所以前面有兩個字節用來保存 BYTE_ORDER_MARK 值,UTF-16 是用定長 16 位(2 字節)來表示的 UCS-2 或 Unicode 轉換格式,通過代理對來訪問 BMP 之外的字符編碼。

 

5 - 按照UTF-8進行編碼

    字符串“I am 君山”用UTF-8編碼時,編碼結果如圖:

    UTF-16 雖然編碼效率很高,但是對單字節範圍內字符也放大了一倍,這無形也浪費了存儲空間,另外 UTF-16 採用順序編碼,不能對單個字符的編碼值進行校驗,如果中間的一個字符碼值損壞,後面的所有碼值都將受影響。而 UTF-8 這些問題都不存在,UTF-8 對單字節範圍內字符仍然用一個字節表示,對漢字採用三個字節表示。UTF-8 編碼與 GBK 和 GB2312 不同,不用查碼錶,所以在編碼效率上 UTF-8 的效率會更好,所以在存儲中文字符時 UTF-8 編碼比較理想。

6 - 對幾種編碼格式的比較

    1)對於中文字符,GB2312與GBK編碼規則類似,但是GBK範圍更大,它能處理所有漢字字符,所以將GB2312與GBK進行比較,應該選擇GBK。
    2)UTF-16與UTF-8都是處理Unicode編碼,它們的編碼規則不太相同,相對來說,UTF-16的編碼效率較高,從字符到到字節的相互轉換更簡單,進行字符串操作也更好。它適合在本地磁盤和內存之間使用,可以進行字符和字節之間的快速切換,如Java的內存編碼就採用UTF-16編碼。但是它不合適在網絡之間傳輸,因爲網絡傳輸容易損壞字節流,一旦字節流損壞將很難恢復,所以相比較而言UTF-8更適合網絡傳輸。
    3)UTF-8對ASCII字符采用單字節存儲,另外單個字符損壞也不會影響後面的其他字符,在編碼效率上介於GBK和UTF-16之間,所以UTF-8在編碼效率上和編碼安全上做了平衡,是理想的中文編碼方式。

 
五、在Java Web中涉及的編解碼

    前面已經提到了I/O操作會引起編碼,而大部分I/O引起的亂碼都是網絡I/O,因爲現在幾乎所有的應用程序都涉及網絡操作,而數據經過網絡傳輸時是以字節爲單位的,所以所有的數據都必須能夠被序列化爲字節。在Java中數據要被序列化,必須繼承Serializable接口。

    用戶從瀏覽器發起一個HTTP請求,存在編碼的地方是URL、Cookie、Parameter。服務器端接收到HTTP請求後要解析HTTP,其中URL、Cookie和Post表單參數需要解碼,服務器端可能還需要讀取數據庫中的數據——本地或網絡中其他地方的文本文件,這些數據都可能存在編碼問題。當Servlet處理完所有請求的數據後,需要將這些數據再編碼,通過Socket發送到用戶請求的瀏覽器裏,再經過瀏覽器解碼成文本。這個過程如圖:

1 - URL的編碼解碼

    瀏覽器編碼URL是將非ASCII字符按照某種編碼格式編碼成16進制數字後將每個16進制數字表示的字節前加上%。

    用戶提交一個URL,在這個URL中可能存在中文,因此需要編碼,如圖爲用戶提交的一個URL:

 

    以Tomcat作爲ServletEngine爲例,把他們分別對應到配置文件中,Port對應在Tomcat->Server.xml的<Connector port="8080" />中配置,而ContextPath在context.xml的<context path="/examples">中配置,ServletPath在Web應用的web.xml的<url-pattern>中配置,PathInfo是我們請求的具體的Servlet,QueryString是要傳遞的參數

    注意這裏是在瀏覽器裏直接輸入URL,所以是通過Get方法請求的,如果通過Post方法請求QueryString將通過表單方式提交到服務器端

    <servlet-mapping>
        <servlet-name>junshangExample</servlet-name>
        <url-pattern>/servlets/servlet/*</url-pattern>
    </servlet-mapping>

    當我們在瀏覽器直接輸入這個URL時,在瀏覽器端和服務器端會如何編碼和解析這個URL呢

    1)瀏覽器端編碼

    我們在fireFox瀏覽器上測試可以發現瀏覽器對PathInfo呵呵QueryString採用的編碼方式是不一樣的,在Chrome中PathInfo是採用UTF-8編碼,而QueryString則是GBK,不同瀏覽器對與PathInfo的編碼方式可能還不一樣,這就爲服務端的解析帶來了困難。

    2)服務器端解析

    對於URL的URI部分進行解碼的字符集是在connector的<Connector URIEncoding="UTF-8" />中定義的,如果沒有定義,那麼默認將會採用默認的編碼ISO-8859-1進行解析(ISO-8859-1不包含中文)。所以有中文URL的時候最好把URIEncoding設置成UTF-8編碼。

    對於QueryString的解析過程:以Get方式HTTP請求的QueryString與以POST方式的HTTP請求的表單參數都是作爲Parameters保存的,都是通過request.getParameter獲取參數值。對他們的解碼是在request.getParameter方法第一次被調用的時進行的。

    QueryString的編碼字符集要麼是Header中ContentType定義的Charset,要麼是默認的ISO-8859-1,要使用ContentType中定義的編碼,就要將connector的<Connector URIEncoding="UTF-8" useBodyEncodingForURI="True" />中的useBodyEncodingForURI設置爲True。這個配置項容易使人產生混淆,他並不是對整個URI都採用BodyEncoding進行解碼,而僅僅是對QueryString使用BodyEncoding解碼這一點需要特別注意。

    從上面URL編碼和解碼過程來看,比較複雜而且編碼和解碼不是在我們應用程序中能完全控制的,在我們的應用程序中,應該儘量避免在URL中使用非ASCII字符,不然可能會遇到亂碼問題。當我們的服務器端最好設置<Connector />中的URIEncoding和useBodyEncodingForURI這兩個參數

2 - HTTP Header的編碼解碼

    當客戶端發起一個HTTP請求的時候,除了URL之外還可能會在Header中傳遞其他的參數,例如Cookie、redirectPath等,這些用戶設置的值可能也會存在編碼的問題。Tomcat對於他們是怎麼解碼的呢?

    對於Header中的項進行解碼也是在調用request.getHeader時進行的。如果請求的Header項沒有解碼則調用MessageBytes的toString方法,這個方法對於從byte到char的轉化使用的默認編碼也是ISO-8859-1而我們也不能設置Header的其他編碼格式,所以如果你設置的Header中有非ASCII字符,解碼中肯定會有亂碼。

    我們在添加Header時,如果一定要傳非ASCII字符,可以先將這些字符使用org.apache.catalina.util.URLEncoder編碼,再添加到Header中,這樣在瀏覽器到服務器的傳遞過程中就不會丟失信息了,我們要訪問這些項時在按照相應的字符集解碼即可

 

3 - POST表單的編解碼

    POST表單提交的參數的解碼是在第一次調用request.getParameter時發生的,POST表單的參數傳遞方式與QueryString不同,它是通過HTTP的BODY傳遞到服務端的。

    當我們在頁面上單擊提交按鈕時,瀏覽器首先將根據ContentType的Charset編碼格式對錶單中填入的參數進行編碼,然後提交到服務器端,在服務器端同樣也是採用ContentType中的字符集進行解碼的,這個字符集我們也可以在服務端通過request.setCharacterEncoding(Charset)來進行設置。

    另外針對multipart/for-data類型的參數,也就是上傳的文件編碼,同樣也是使用ContentType定義的字符集編碼。注意,上傳文件是用字節流的方式傳遞到服務器的本地臨時目錄,這個過程並沒有涉及字符編碼,而真正的編碼是將文件內容添加到Parameters中時,如果不能使用這種編碼方式則會使用默認編碼ISO-8859-1來編碼。

 

4 - HTTP Body的編碼與解碼

    當用戶請求的資源已經成功獲取後,這些內容將會通過Response返回給客戶端瀏覽器。這個過程要先經過編碼,再到瀏覽器進行解碼。編碼字符集可以通過response.setCharacterEncoding來設置,它將會覆蓋request.setCharacterEncoding的值,並且通過Header的Content-Type返回客戶端,瀏覽器接收到返回的Socket流時將通過Content-Type的charset來解碼

    如果返回的HTTP Header中的Content-Type沒有設置charset,那麼瀏覽器將根據HTML的<meta HTTP-equiv="Content-Type" content="text/html; charset=GBK" />中指定的charset來解碼。如果沒有定義,那麼瀏覽器將使用默認的編碼來解碼。

    訪問數據庫都是通過客戶端JDBC驅動來完成的,使用JDBC來存取數據時要和數據的內置編碼保持一致,可以通過設置JDBC URL來指定,如MySQL:url=”jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK”

 
六、在JS中涉及的編碼解碼

 

1 - 外部引入JS文件

在一個單獨的JS文件中包含中文字符串輸入的情況,例如:

    <html>
    <head>
    <script src="static/javascript/script.js" charset="gbk"></script>

如果引入一個script.js腳本,這個腳本中含有如下代碼:

docuemnt.write("這是一段中文");

這時如果script沒有設置charset,瀏覽器就會以當前這個頁面默認的字符集解析這個JS文件。如果外部的JS文件的編碼格式與當前頁面的編碼格式一致,那麼就可以不設置這個charset。但是如果script.js文件與當前頁面的編碼格式不一致,如script.js是UTF-8編碼而頁面時GBK編碼,上面代碼中的中文輸入就會變成亂碼。

 

2 - JS的URL編碼

    通過JS發起異步調用的URL默認的編碼也是受瀏覽器的影響,如果使用原始Ajax的http_request.open('GET',url,true)調用,URL的默認編碼在IE是操作系統的默認編碼而在Firefox下則是UTF-8編碼,另外不同的JS框架可能對於URL的編碼處理也不一樣。

    處理JS的URL編碼問題:

1)encodeURI()與decodeURI()

JS用來對URL編碼的函數,他可以將整個URL中的字符(一些特殊字符除外)進行UTF-8編碼,在每個碼值前加上"%"。

2)encodeURIComponent()和decodeURIComponent()

    encodeURIComponent()這個函數比encodeURI()編碼更爲徹底。通常用於將一個URL當做一個參數放在另一個URL中

3)Java與JS的編碼解碼問題。在Java端處理URL編碼解碼的有兩個類,分別是java.net.URLDecoder和java.net.URLEncoder。這兩個類可以將所有“%”加UTF-8碼值使用UTF-8解碼,從而得到原始的字符。Java端的URLEncode和URLDecoder與前端JS對應的是encodeURIComponent和decodeURIComponent。

    注意:前端用encodeURIComponent編碼後,到服務端用URLDecoder解碼可能會出現亂碼,這一定是兩個字符編碼類型不一致導致的。JS編碼默認的是UTF-8編碼,而服務器中文解碼一段都是GBK或者GB2312,所以用encodeURIComponent編碼後是UTF-8,而java用GBK去解碼顯然不對。

    解決的辦法是用encodeURIComponent兩次編碼,如encodeURIComponent(encodeURIComponent(str))。這樣在Java端通過request.getParamter()用GBK解碼後取得的就是UTF-8編碼的字符串,如果Java端需要使用這個字符串,則再用UTF-8解碼一次;如果是將這個結果直接通過JS輸出到前端,那麼這個UTF-8字符串可以直接在前端正常顯示。

 
七、常見問題的分析

    基於前面的瞭解的Java web編碼解碼知識之後我們知道出現亂碼問題唯一的原因就是在編碼解碼過程中採用的字符集不一致導致的,因爲在一次操作中經常涉及多次編碼和解碼,因此出現亂碼問題的時候也給我們排查帶來的難度,下面分析幾種常見的情景:

1 - 中文變成看不懂的字符

    例如,字符串“淘!我喜歡!”變成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”編碼過程如下圖所示

    字符串在解碼時所用的字符集與編碼字符集不一致導致漢字變成了看不懂的亂碼,而且是一個漢字字符變成兩個亂碼字符。這種情景在開發中經常發生,例如在瀏覽器中輸入一個帶有中文字符串參數的URL一些瀏覽器默認對QueryString採用的是GBK編碼方式,但是由於在web中間件例如tomcat沒有做相關配置,在服務端讀取請求參數時也沒有指定編碼方式,於是默認使用ISO-8859-1進行解碼導致亂碼。這種出現亂碼且亂碼字符串長度是原編碼前字符串的兩倍的原因可能是採用2字節編碼例如GBK、UTF-16等然後使用單字節進行解碼例如ISO-8859-1導致的

 

2 - 一個漢字變成一個問號

例如,字符串“淘!我喜歡!”變成了“??????”編碼過程如下圖所示

 

    將中文和中文符號經過不支持中文的 ISO-8859-1 編碼後,所有字符變成了“?”,這是因爲用 ISO-8859-1 進行編解碼時遇到不在碼值範圍內的字符時統一用 3f 表示,這也就是通常所說的“黑洞”,所有 ISO-8859-1 不認識的字符都變成了“?”。

 

3 - 一個漢字變成兩個問號

例如,字符串“淘!我喜歡!”變成了“????????????”編碼過程如下圖所示

 

    這種情況比較複雜,中文經過多次編碼,但是其中有一次編碼或者解碼不對仍然會出現中文字符變成“?”現象,出現這種情況要仔細查看中間的編碼環節,找出出現編碼錯誤的地方。

 

4 - 一種不正常的正確編碼解碼

還有一種情況是在我們通過 request.getParameter 獲取參數值時,當我們直接調用下面代碼會出現亂碼

String value = request.getParameter(name);

但是如果用下面的方式解析時取得的 value 會是正確的漢字字符

String value = new String(request.getParameter(name).getBytes("ISO-8859-1"), "GBK");

這種情況是怎麼造成的呢?看下圖:

    這種情況是這樣的,ISO-8859-1 字符集的編碼範圍是 0000-00FF,正好和一個字節的編碼範圍相對應。這種特性保證了使用 ISO-8859-1 進行編碼和解碼可以保持編碼數值“不變”。雖然中文字符在經過網絡傳輸時,被錯誤地“拆”成了兩個歐洲字符,但由於輸出時也是用 ISO-8859-1,結果被“拆”開的中文字的兩半又被合併在一起,從而又剛好組成了一個正確的漢字。雖然最終能取得正確的漢字,但是還是不建議用這種不正常的方式取得參數值,因爲這中間增加了一次額外的編碼與解碼,這種情況出現亂碼時因爲 Tomcat 的配置文件中 useBodyEncodingForURI 配置項沒有設置爲”true”,從而造成第一次解析式用 ISO-8859-1 來解析才造成亂碼的。

 
八、Java Web編碼解碼問題總結

    要解決中文編碼問題,首先要搞清楚哪些地方會引起字符到字節的編碼以及字節到字符的解碼,最常見的地方就是存儲數據到磁盤或者數據要經過網絡傳輸。其次應針對這些地方搞清楚操作這些數據的框架或系統是如何控制編碼的。最後正確設置編碼格式,避免使用軟件默認的或者操作系統平臺默認的編碼格式。
————————————————



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章