搜尋
首頁Javajava教程解析Java Web 中的中文編碼問題

  背景:

#   編碼問題一直困擾著程式開發人員,尤其是在 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 的 中配置,而 Context Path 在 中配置,Servlet Path 在 Web 应用的 web.xml 中的

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

   中配置,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 的 中定义的,如果没有定义,那么将以默认编码 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 的 中的useBodyEncodingForURI 設定為true。這個配置項目的名字有點讓人產生混淆,它並不是對整個 URI 都採用 BodyEncoding 進行解碼而僅僅是對 QueryString 使用 BodyEncoding 解碼,這一點還要特別注意。

從上面的URL 編碼和解碼過程來看,比較複雜,而且編碼和解碼並不是我們在應用程式中能完全控制的,所以在我們的應用程式中應該盡量避免在URL 中使用非ASCII 字符,不然很可能會碰到亂碼問題,當然在我們的伺服器端最好設定 中的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 的

銆€銆€4.5 鍏跺畠闇€瑕佺紪鐮佺殑鍦版柟

銆€銆€闄や簡 URL 鍜屽弬鏁扮紪鐮侀棶棰樺

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

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中