java web中文編碼問題

深入分析Java Web中文編碼

文章開始之前我們先考慮一個問題,我們爲什麼要編碼?能不能不編碼呢,答案肯定是否定的(不然也不存在今天要討論的問題了0.0)的。因爲計算機是無法直接理解我們人類所用的語言符號的,反之我們也無法直接理解計算機的語言。注意:計算機中的基本存儲單元爲一個字節(byte),而人類的語言符號太多,因此必須經過一些拆分和翻譯才能讓計算機理解我們的語言。

舉個例子,我們把計算機能夠理解的語言假定爲英語,其他語言要在計算機中使用,必須得經過一次翻譯,將其翻譯爲英語。這個翻譯的過程即編碼。

是的,如果大家都說英語,而且計算機中存儲信息的最小單元是英文單詞,這樣就不存在編碼問題了。
總結來說,之所以存在編碼問題原因在於:

  • 計算機中存儲信息的最小單元是1個字節,即8bit,所能表示的字符範圍是0-255。
  • 人類要表示的符號太多,無法用一個字節完全表示
    因此要解決上述矛盾必須要有一個新的數據結構——char,而從char到byte必須要編碼(反之亦然)

如何“翻譯”?

計算機中的翻譯方式有很多,常見的有ASCII、ISO-8859-1、GB2312、GBK、UTF-16、UTF-8等。我們可以把這些看作是一個字典,它們規定了轉化的規則,我們只要按照這個規則就能讓計算機正確表示我們的字符。關於如何選擇編碼方式,要考慮很多因素,例如,是存儲空間重要還是編碼效率重要等問題。這些編碼方式的具體區別和優勢在此不展開討論,下面詳細說說java中需要編碼的情況。
簡單比較一下幾種編碼格式:

GBK2312和GBK編碼規則類似,GBK的範圍更大,能處理所有漢字,因此二者之間處理漢字的話首選GBK。UTF-16和UTF-8都是處理Unicode編碼,相對來說UTF-16的編碼效率高,字符到字節的轉換簡單高效,字符串操作也更好,適合在磁盤和內存之間使用。但它不適合網絡傳輸,因爲網絡傳輸容易損壞字節流,而UTF-16中一個字符嗎值損壞後面的所有碼值都會受到影響,另外對於單字節的字符處理UTF-16會在高位補0(變成16位),浪費了存儲空間。UTF-8對ASCII字符采用單字節存儲,單個字符損壞不影響傳輸,在編碼效率上介於GBK和UTF-16之間,在編碼效率和安全性上做了平衡,是理想的中文編碼方式。

Java中需要編碼的場景

設計編碼的操作一般都在字節和字符的轉化上,而需要這種轉換的場景主要是I/O操作,包括磁盤I/O和網絡I/O。

1. 在I/O操作中

在java中,Reader類是JAVA的I/O中讀字符的父類,而InputStream類是讀字節的父類,InputStreamReader類就是關聯字節到字符的橋樑,它負責在I/O過程中處理讀取字節到字符的轉換,而對字節到字符的解碼實現,則委託StreamDecoder去做,在解碼過程中需要用戶指定Charset編碼格式,如果未指定編碼格式,將按照本地環境的默認字符集。寫的情況也類似,字符的父類是Writer,字節的父類是OutPutStream,通過OutPutStreamWriter轉換字符到字節,StreamEncoder類負責將字符編碼成字節,編碼格式和默認編碼規則與解碼是一致的。

例如:實現文件讀寫功能代碼

        String file = "C:/test.txt";
        String charset = "UTF-8";
        //寫字符轉換成字節流
        FileOutputStream outputStream = new FileOutputStream(file);
        OutputStreamWriter writer = new OutputStreamWriter(outputStream,charset);
        try {
            writer.write("這是要保存的中文字符");
        }finally{
            writer.close();
        }
        //讀取字節轉換成字符
        FileInputStream inputStream = new FileInputStream(file);
        InputStreamReader reader = new InputStreamReader(inputStream,charset);
        StringBuffer buffer = new StringBuffer();
        char [] buf = new char[64];
        int end = 0;
        try {
            while((end = reader.read(buf))!= -1){
                buffer.append(buf,0,end);
            }
        } finally {
            reader.close();
        }

在設計I/O操作的程序中,我們要注意指定統一的編碼charset字符集,如果不指定,會默認使用操作系統默認編碼,這樣程序的編碼格式和運行環境綁定起來,在跨環境時就可能會出現亂碼。

2. 在內存操作中

在Java開發中除了I/O設計編碼外,最常用的應該是內存中進行字符和字節的相關轉換.

String類就提供了轉換方式。

String s = "中文字符串";
byte[] b = s.getBytes("UTF-8");
String n = new String(b,"UTF-8");

Charset類也提供了encode和decode對應編碼和解碼:

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

Java Web中涉及的編解碼

對於中文來說,有I/O的地方就會涉及編碼,而如今大部分I/O亂碼問題都涉及到網絡I/O。
當用戶從瀏覽器發起一個HTTP請求,對於請求的URL、Cookie、Paramiter來說都需要編碼,服務端接收到HTTP後要解析HTTP,其中URL、Cookie和POST表單參數需要解碼,服務器端可能還需要讀取數據庫中的數據,本地或網絡中其他地方的文本文件,這些數據都可能存在編碼問題,當Servlet處理完所有請求後,需要再編碼通過Socket發送到用戶請求的瀏覽器裏,瀏覽器再解碼成文本。一次HTTP請求需要編解碼的地方很多,下面詳細討論:

1. URL的編解碼

URL中可能存在中文,因此需要編碼。URL中的路徑信息(PathInfo)和請求參數(QueryString,即?後面的部分)很有可能會出現中文,一般情況下PathInfo是UTF-8編碼,而QueryString是GBK編碼,至於我們經常看到請求中的%號是因爲瀏覽器編碼URL是將非ASCII字符按照某種編碼格式編碼成16進制數字後再將每個16進製表示的字節加上%(爲什麼要這樣並不知道0.0)。因此瀏覽器對PathInfo和QueryString的編碼是不一樣的,不同的瀏覽器對PathInfo的編碼也可能不一樣,這就導致服務器解碼上的困難。
下面以Tomcat爲例看看如和解碼。

tomcat對URL的URI部分進行解碼的字符集是在connector的<Connector URIEncoding=”UTF-8”/>中定義的,如果未定義使用默認編碼ISO-8859-1解析,所以如果有中文URL時最好把URIEncoding設置成UTF-8。

對於QueryString的解析過程,以GET方式HTTP請求的QueryString與以POST方式請求的表單參數都是作爲Parameters保存的,都通過request.getParameter獲取參數值,對它們的解碼是在request.getParameter方法第一次被調用時進行。請求參數QueryString的解碼集在哪裏定義的呢?它和URI的解碼字符集不一樣,QueryString的解碼字符集要麼是Header中ContentType定義的Charset,要麼是默認的ISO-8859-1要使用ContentType中定義的編碼,就要將connector的<Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”ture”/>中的useBodyEncodingForURI設置爲ture。這個配置項並不是對整個URI都採用BodyEncoding進行編碼,而僅僅是對QueryString使用BodyEncoding解碼

2. HTTP Header的編解碼

當客戶端發起一個HTTP請求時,除了URL,還可能會在Header中傳遞其他參數,如Cookie、redirectPath等,對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表單參數傳遞方式和URL中的請求參數不同,它是通過HTTP的BODY傳遞到服務器端的。提交時編解碼都是使用ContentType的Charset編碼格式,我們可以通過request.setCharacterEncoding(charset)來設置。一定要在第一次調用request.getParameter方法之前就設置request.setCharacterEncoding(charset),否則也有肯出現亂碼。

4. HTTP BODY的編解碼

用戶請求資源成功後,將通過Response返回給客戶端瀏覽器。這個過程需要先經過編碼再到瀏覽器進行解碼,編解碼字符集可以通過response.setCharacterEncoding來設置,它將覆蓋request.getCharacterEncoding的值,並且通過Header的Content-Type返回客戶端,瀏覽器接收到返回的Socket流時將通過Content-Type的charset來解碼。如果返回的HTTP Header中Conten-Type沒有設置charset,那麼瀏覽器將根據HTML的<meta HTTP-equov=”Content-Type” content=”text/html;charset=GBK”/>中的charset來解碼。如果也沒有定義,那麼瀏覽器將使用默認編碼來解碼。

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

在JS中的編碼問題

在web應用中,通過js發起異步請求時遇到編碼問題的情況越來越多。

1. 外部引入js文件

在一個單獨的js文件中包含字符串時:

document.write("中文字符串");

這是如果引入一個script.js腳本需要設置charset:

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

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

2. JS的URL編碼

  • encodeURL()
    encodeURL()可以將整個URL中的字符(特殊字符除外,如“!”“#”“¥”“&”“’”“(”“)”“*”“+”“,”“-”“.”“/”“:”“;”“=”“?”“@”“_”“~”“0-9”“a-z”“A-Z”)進行UTF-8編碼,在每個碼前面加上“%”。解碼則通過decodeURI()函數。
  • encodeURLComponent()
    encodeURLComponent()這個函數比encodeURI()編碼還徹底,他除了對“!”“’”“(”“)”“*”“-”“.”“_”“~”“0-9”“a-z”“A-Z”這幾個字符不編碼,對其他字符都編碼。解碼通過decodeURIComponent()進行解碼。
  • Java和JS編解碼問題
    前面說了JS編解碼問題,如果js進行了編碼,編碼的字符傳到服務器端後可以通過Java來解碼,那麼java又是怎麼解碼的呢?

    我們知道在Java端處理URL編解碼有兩個類,分別是java.net.URLEncoder和java.net.URLDecoder。這兩個類可以將所有“%”加UTF-8碼值用UTF-8解碼,從而得到原始的字符。

    查看URLEncoder的源碼可以發現,URLEncoder受保護的特殊字符少於JS中受保護的特殊字符。java端的URLEncoder和URLDecoder與前端JS對應的是encodeURLComponent和decodeURLComponent。注意,前端用encodeURLComponent編碼後,到服務器端用URLDecoder解碼可能會出現亂碼。因爲JS編碼默認的是UTF-8編碼,而服務器端中文解碼一般都是GBK或者GB2312,所以用encodeURLComponent編碼後是UTF-8,而Java用GBK去解碼顯然不對。解決辦法是用encodeURLComponent兩次編碼,如encodeURLComponent(encodeURLComponent(str))。這樣在java端通過request.getParameter()用GBK解碼後取得的就是UTF-8編碼的字符串,如果java端需要使用這個字符串,則再用UTF-8解碼一次;如果是將這個結果直接通過JS輸出到前端,那麼這個UTF-8字符串可以直接在前端正常顯示。

    常見問題分析

    1. 中文變成了看不懂的字符

    一般是因爲GBK編碼後用ISO-5899-1解碼導致

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

    一般是因爲用了不支持漢字的ISO-5899-1編碼和編碼導致

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

    一般是因爲對中文用了GBK編碼後再使用ISO-5899-1解碼然後再使用了GBK進行了編解碼導致。

借鑑《深入分析Java Web技術內幕》一書

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