首頁  >  文章  >  Java  >  解析Java Web 中的中文編碼問題

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

怪我咯
怪我咯原創
2017-04-10 10:32:531758瀏覽

  背景:

#   編碼問題一直困擾著程式開發人員,尤其是在 Java 中更加明顯,因為 Java 是跨平台的語言,在不同平台的編碼之間的切換較多。接下來將介紹Java 編碼問題出現的根本原因;在Java 中經常遇到的幾種編碼格式的區別;在Java 中經常需要編碼的場景;出現中文問題的原因分析;在開發Java Web 中可能存在編碼的幾個地方;一個HTTP 請求怎麼控制編碼格式;如何避免中文編碼問題等。

  1、幾種常見的編碼格式

  1.1 為什麼要編碼

  • # 在電腦中儲存資訊的最小單元是 1 個位元組,即 8 個 bit, 所以能表示的字元範圍是 0 ~ 255 個。

  • 要表示的符號太多,無法用 1 個位元組來完全表示。

#   1.2 如何翻譯

  計算機中提供多種翻譯方式,常見的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16等。這些都規定了轉換的規則,依照這個規則就可以讓電腦正確的表示我們的字元。以下介紹這幾種編碼格式:

  • ASCII 碼

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

  • ISO-8859-1



  • # 128 個字符顯然是不夠用的,所以 ISO 組織在 ASCII 的基礎上擴展,他們是 ISO-8859-1 至 ISO-8859-15,前者涵蓋大多數字符,應用最廣。 ISO-8859-1 仍是單字節編碼,它總歸能表示 256 個字元。

  • GB2312


  • 它是雙位元組編碼,總的編碼範圍是 A1 ~ F7,其中 A1 ~ A9 是符號區,總共包含 682 個符號;B0 ~ F7 是漢字區,包含 6763 個漢字。

  • GBk

  • ## GBK 為《漢字內碼擴展規範》,為GB2312 的擴展,它的編碼範圍是8140 ~ FEFE(去掉XX7F),總共有23940 個碼位,能表示21003 個漢字,和GB2312的編碼兼容,不會有亂碼。


    UTF-16
  • 它具體定義了 Unicode 字元在電腦中的存取方法。 UTF-16 用兩個位元組來表示 Unicode 的轉換格式,它採用定長的表示方法,即不論什麼字元用兩個位元組表示。兩個位元組是 16 個 bit,所以叫 UTF-16。它表示字符非常方便,沒兩個字節表示一個字符,這就大大簡化了字串操作。

    UTF-8

    • # 雖然UTF-16 統一採用兩個位元組表示一個字元很簡單方便,但是很大一部分字元用一個位元組就可以表示,如果用兩個位元組表示,儲存空間放大了一倍,在網路頻寬有限的情況下會增加網路傳輸的流量。 UTF-8 採用了一種變長技術,每個編碼區域有不同的字元長度不同類型的字元可以由 1 ~ 6 個位元組組成。

      UTF-8 有以下編碼規則:
    • #


# 如果是1 個位元組,最高位元(第8 位元)為0,則表示這是一個ASCII 字元(00 ~ 7F)

如果是1 個位元組,以11 開頭,則連續的1 的個數暗示這個字元的位元組數

##########如果是1 個位元組,以10 開頭,表示它不是首字節,則需要向前尋找才能得到目前字元的首字節###########################   2、在 Java 中需要編碼的場景#######   2.1 在 I/O 操作中存在的編碼###### ###### ########## 如上圖:Reader 類別是在Java 的I/O 中讀取符號的父類,而InputStream 類別是讀取位元組的父類, InputStreamReader 類別就是關聯位元組到字元的橋樑,它負責在I/O 過程中處理讀取位元組到字元的轉換,而對特定位元組到字元的解碼實現,它又委託StreamDecoder 去做,在StreamDecoder 解碼過程中必須由使用者指定Charset 編碼格式。值得注意的是,如果你沒有指定 Charset,則會使用本地環境中預設的字元集,如在中文環境中將使用 GBK 編碼。 ###

  如下面一段程式碼,實作了檔案的讀寫功能:

 String file = "c:/stream.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 count = 0; 
 try { 
    while ((count = reader.read(buf)) != -1) { 
        buffer.append(buffer, 0, count); 
    } 
 } finally { 
    reader.close(); 
 }

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

  2.2 在記憶體運算中的編碼

  在記憶體中進行從字元到位元組的資料型別轉換。

  1、String 類別提供字串轉換到位元組的方法,也支援將位元組轉換成字串的建構子。

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

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

Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = charset.encode(string);
CharBuffer charBuffer = charset.decode(byteBuffer);

  ...

#   3、在 Java 中如何編解碼

#   Java 編碼類別圖

# 首先根據指定的charsetName 透過Charset.forName(charsetName) 設定Charset 類,然後根據Charset 建立CharsetEncoder 對象,再呼叫CharsetEncoder.encode 對字串進行編碼,不同的編碼類型都會對應到一個類別中,實際的編碼過程是在這些類別中完成的。下面是 String. getBytes(charsetName) 編碼過程的時序圖

  Java 編碼時序圖

# 從上圖可以看出根據charsetName 找到Charset 類,然後根據這個字符集編碼生成CharsetEncoder,這個類是所有字符編碼的父類,針對不同的字符編碼集在其子類中定義瞭如何實現編碼,有了CharsetEncoder 物件後就可以呼叫encode 方法去實作編碼了。這個是 String.getBytes 編碼方法,其它的如 StreamEncoder 中也是類似的方式。

  常常會出現中文變成「?」很可能就是錯誤的使用了 ISO-8859-1 這個編碼所導致的。中文字符經過 ISO-8859-1 編碼會丟失訊息,通常我們稱之為“黑洞”,它會把不認識的字符吸收掉。由於現在大部分基礎的 Java 框架或系統預設的字元集編碼都是 ISO-8859-1,所以很容易出現亂碼問題,後面將會分析不同的亂碼形式是怎麼出現的。

  幾種編碼格式的比較

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

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

# 對於使用中文來說,有I/O 的地方就會涉及到編碼,前面已經提到了I/O 操作會引起編碼,而大部分I/O 引起的亂碼都是網路I/O,因為現在幾乎所有的應用程式都涉及網路操作,而資料經過網路傳輸都是以位元組為單位的,所以所有的資料都必須能夠被序列化為位元組。在 Java 中資料被序列化必須繼承 Serializable 介面。

一段文字它的實際大小該怎麼計算,我曾經碰到過一個問題:就是要想辦法壓縮Cookie 大小,減少網路傳輸量,當時有選擇不同的壓縮演算法,發現壓縮後字元數是減少了,但是並沒有減少位元組數。所謂的壓縮只是將多個單字節字元透過編碼轉變成一個多位元組字元。減少的是 String.length(),並沒有減少最終的位元組數。例如將“ab”兩個字符通過某種編碼轉變成一個奇怪的字符,雖然字符數從兩個變成一個,但是如果採用UTF-8 編碼這個奇怪的字符最後經過編碼可能又會變成三個或更多的位元組。同樣的道理例如整數數字1234567 如果當成字符來存儲,採用UTF-8 來編碼佔用7 個byte,採用UTF-16 編碼將會佔用14 個byte,但是把它當成int 型數字來存儲只需要4 個byte 來存放。所以看一段文字的大小,看字元本身的長度是沒有意義的,即使是一樣的字元採用不同的編碼最終儲存的大小也會不同,所以從字元到位元組一定要看編碼類型。

  我们能够看到的汉字都是以字符形式出现的,例如在 Java 中“淘宝”两个字符,它在计算机中的数值 10 进制是 28120 和 23453,16 进制是 6bd8 和 5d9d,也就是这两个字符是由这两个数字唯一表示的。Java 中一个 char 是 16 个 bit 相当于两个字节,所以两个汉字用 char 表示在内存中占用相当于四个字节的空间。

  这两个问题搞清楚后,我们看一下 Java Web 中那些地方可能会存在编码转换?

  用户从浏览器端发起一个 HTTP 请求,需要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据都可能存在编码问题,当 Servlet 处理完所有请求的数据后,需要将这些数据再编码通过 Socket 发送到用户请求的浏览器里,再经过浏览器解码成为文本。这些过程如下图所示:

  一次 HTTP 请求的编码示例

  4.1 URL 的编解码

  用户提交一个 URL,这个 URL 中可能存在中文,因此需要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL:

  上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:

  Port 对应在 Tomcat 的 2d40a7d47a79d09f1e13ed7afa569ba8 中配置,而 Context Path 在 cc9811071a6b9ec4ee8fd6740dade5e9 中配置,Servlet Path 在 Web 应用的 web.xml 中的

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

  66e1775cbd9d5002635ae3285442ba88 中配置,PathInfo 是我们请求的具体的 Servlet,QueryString 是要传递的参数,注意这里是在浏览器里直接输入 URL 所以是通过 Get 方法请求的,如果是 POST 方法请求的话,QueryString 将通过表单方式提交到服务器端。

  上图中 PathInfo 和 QueryString 出现了中文,当我们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的我选择的是360极速浏览器并通过 Postman 插件观察我们请求的 URL 的实际的内容,以下是 URL:

  HTTP://localhost:8080/examples/servlets/servlet/君山?author=君山

  君山的编码结果是:e5 90 9b e5 b1 b1,和《深入分析 Java Web 技术内幕》中的结果不一样,这是因为我使用的浏览器和插件和原作者是有区别的,那么这些浏览器之间的默认编码是不一样的,原文中的结果是:

 

  君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码,至于为什么会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”,所以最终的 URL 就成了上图的格式了。

  从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对服务器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。

  解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:

protected void convertURI(MessageBytes uri, Request request) 
 throws Exception { 
        ByteChunk bc = uri.getByteChunk(); 
        int length = bc.getLength(); 
        CharChunk cc = uri.getCharChunk(); 
        cc.allocate(length, -1); 
        String enc = connector.getURIEncoding(); 
        if (enc != null) { 
            B2CConverter conv = request.getURIConverter(); 
            try { 
                if (conv == null) { 
                    conv = new B2CConverter(enc); 
                    request.setURIConverter(conv); 
                } 
            } catch (IOException e) {...} 
            if (conv != null) { 
                try { 
                    conv.convert(bc, cc, cc.getBuffer().length - 
 cc.getEnd()); 
                    uri.setChars(cc.getBuffer(), cc.getStart(), 
 cc.getLength()); 
                    return; 
                } catch (IOException e) {...} 
            } 
        } 
        // Default encoding: fast conversion 
        byte[] bbuf = bc.getBuffer(); 
        char[] cbuf = cc.getBuffer(); 
        int start = bc.getStart(); 
        for (int i = 0; i < length; i++) { 
            cbuf[i] = (char) (bbuf[i + start] & 0xff); 
        } 
        uri.setChars(cbuf, 0, length); 
 }

  从上面的代码中可以知道对 URL 的 URI 部分进行解码的字符集是在 connector 的 fb5e5a423fbf569b89d2849dcbd1c1d3 中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。

  QueryString 又如何解析? GET 方式 HTTP 請求的 QueryString 與 POST 方式 HTTP 請求的表單參數都是作為 Parameters 保存,都是透過 request.getParameter 取得參數值。對它們的解碼是在 request.getParameter 方法第一次被呼叫時進行的。 request.getParameter 方法被呼叫時將會呼叫 org.apache.catalina.connector.Request 的 parseParameters 方法。這個方法將會對 GET 和 POST 方式傳遞的參數進行解碼,但是它們的解碼字元集有可能不一樣。 POST 表單的解碼將在後面介紹,QueryString 的解碼字元集是在哪定義的呢?它本身是透過 HTTP 的 Header 傳到服務端的,而且也在 URL 中,是否和 URI 的解碼字元集一樣呢?從前面瀏覽器對 PathInfo 和 QueryString 的編碼採取不同的編碼格式不同可以猜測到解碼字元集肯定也不會是一致的。的確是這樣QueryString 的解碼字元集要不是Header 中ContentType 中定義的Charset 要嘛就是預設的ISO-8859-1,要使用ContentType 中定義的編碼就要設定connector 的68bb9799e142932663394f93e704e7c1 中的useBodyEncodingForURI 設定為true。這個配置項目的名字有點讓人產生混淆,它並不是對整個 URI 都採用 BodyEncoding 進行解碼而僅僅是對 QueryString 使用 BodyEncoding 解碼,這一點還要特別注意。

從上面的URL 編碼和解碼過程來看,比較複雜,而且編碼和解碼並不是我們在應用程式中能完全控制的,所以在我們的應用程式中應該盡量避免在URL 中使用非ASCII 字符,不然很可能會碰到亂碼問題,當然在我們的伺服器端最好設定5de43e088bab17594573baec6d1f4ce2 中的URIEncoding 和useBodyEncodingForURI 兩個參數。

  4.2 HTTP Header 的編解碼

#   當客戶端發起一個 HTTP 請求除了上面的 URL 外還可能會在 Header 中傳遞其它參數如 Cookie、redirectPath 等,這些用戶設定的值很可能也會存在編碼問題,Tomcat 對它們又是怎麼解碼的呢?

對Header 中的項目進行解碼也是在呼叫request.getHeader 是進行的,如果請求的Header 項沒有解碼則調用MessageBytes 的toString 方法,這個方法將從byte 到char 的轉換使用的預設編碼也是ISO-8859-1 ,而我們也不能設定Header 的其它解碼格式,所以如果你設定Header 中有非ASCII 字元解碼肯定會有亂碼。

我們在添加Header 時也是同樣的道理,不要在Header 中傳遞非ASCII 字符,如果一定要傳遞的話,我們可以先將這些字符用org.apache.catalina.util.URLEncoder 編碼然後再添加到Header 中,這樣在瀏覽器到伺服器的傳遞過程中就不會遺失資訊了,如果我們要存取這些項目時再按照對應的字元集解碼就好了。

  4.3 POST 表單的編解碼

#   在前面提到了 POST 表單提交的參數的解碼是在第一次呼叫 request.getParameter 發生的,POST 表單參數傳遞方式與 QueryString 不同,它是透過 HTTP 的 BODY 傳遞到服務端的。當我們在頁面上點擊 submit 按鈕時瀏覽器首先將根據 ContentType 的 Charset 編碼格式對表單填的參數進行編碼然後提交到伺服器端,在伺服器端同樣也是用 ContentType 中字元集進行解碼。所以透過 POST 表單提交的參數一般不會有問題,而且這個字元集編碼是我們自己設定的,可以透過 request.setCharacterEncoding(charset) 來設定。

另外針對multipart/form-data 類型的參數,也就是上傳的檔案編碼同樣也是使用ContentType 定義的字元集編碼,值得注意的地方是上傳檔案是用位元組流的方式傳輸到伺服器的本地臨時目錄,這個過程並沒有涉及到字元編碼,而真正編碼是在將檔案內容新增到parameters 中,如果用這個編碼不能編碼時將會用預設編碼ISO-8859-1 來編碼。

  4.4 HTTP BODY 的編解碼

  當使用者要求的資源已經成功取得後,這些內容將透過 Response 傳回給客戶端瀏覽器,這個過程先要經過編碼再到瀏覽器進行解碼。這個過程的編解碼字元集可以透過response.setCharacterEncoding 來設置,它將會覆蓋request.getCharacterEncoding 的值,並且透過Header 的Content-Type 傳回客戶端,瀏覽器接受到傳回的socket 串流時將會透過Content-Type的charset 來解碼,如果回傳的HTTP Header 中Content-Type 沒有設定charset,那麼瀏覽器將根據Html 的be788df14b429afb10ce53d71a4f8e7d

  访问数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如 MySQL:url="jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK"。

  5、常见问题分析

  下面看一下,当我们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题唯一的原因都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于往往一次操作涉及到多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题,下面就几种常见的现象进行分析。

  5.1 中文变成了看不懂的字符

  例如,字符串“淘!我喜欢!”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程如下图所示:

 

  字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符。

  5.2 一个汉字变成一个问号

  例如,字符串“淘!我喜欢!”变成了“??????”编码过程如下图所示:

 

  将中文和中文符号经过不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是因为用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是通常所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。

  5.3 一个汉字变成两个问号

  例如,字符串“淘!我喜欢!”变成了“????????????”编码过程如下图所示:

 

  这种情况比较复杂,中文经过多次编码,但是其中有一次编码或者解码不对仍然会出现中文字符变成“?”现象,出现这种情况要仔细查看中间的编码环节,找出出现编码错误的地方。

  5.4 一种不正常的正确编码

  还有一种情况是在我们通过 request.getParameter 获取参数值时,当我们直接调用

  String value = request.getParameter(name); 会出现乱码,但是如果用下面的方式

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

  解析时取得的 value 会是正确的汉字字符,这种情况是怎么造成的呢?

  看下如所示:

 

  这种情况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码可以保持编码数值“不变”。虽然中文字符在经过网络传输时,被错误地“拆”成了两个欧洲字符,但由于输出时也是用 ISO-8859-1,结果被“拆”开的中文字的两半又被合并在一起,从而又刚好组成了一个正确的汉字。虽然最终能取得正确的汉字,但是还是不建议用这种不正常的方式取得参数值,因为这中间增加了一次额外的编码与解码,这种情况出现乱码时因为 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而造成第一次解析式用 ISO-8859-1 来解析才造成乱码的。

  6、總結

#   本文首先總結了幾種常見編碼格式的區別,然後介紹了支援中文的幾種編碼格式,並比較了它們的使用場景。接著介紹了 Java 那些地方會牽涉到程式設計問題,已經 Java 中如何對程式設計的支援。並以網路 I/O 為例重點介紹了 HTTP 請求中的存在編碼的地方,以及 Tomcat 對 HTTP 協定的解析,最後分析了我們平常遇到的亂碼問題出現的原因。

綜上所述,要解決中文問題,首先要搞清楚哪些地方會引起字符到字節的編碼以及字節到字符的解碼,最常見的地方就是讀取會存儲數據到磁盤,或者數據要經過網絡傳輸。然後針對這些地方搞清楚操作這些資料的框架的或系統是如何控制編碼的,正確設定編碼格式,避免使用軟體預設的或是作業系統平台預設的編碼格式。


以上是解析Java Web 中的中文編碼問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn