前言
本文章主要討論了在Java web系統中亂碼產生的內在原理, 是認識和解決亂碼問題的基礎. 如果您對亂碼問題還沒有一個清晰的概念, 請嘗試閱讀本文. 另外, 本文也討論了最近流行的Ajax技術中的亂碼問題, 如果您在使用Ajax技術中遇到了亂碼, 本文對您也有一定的參考價值<1>。
2006年5月30日
為什么會出現亂碼
我們都知道, 在馮·諾伊曼(Neumann János)<2>體系的計算機中, 任何數據都是以二進制的形式存在的。我們在鍵盤上輸入以及我們在屏幕上看到的中文、日文、英文字符,最終都是內存或者硬盤上的二進制數據。那么,這種二進制數據和屏幕上的中文、日文、英文字符之間相互的關系就需要通過一種映射來表達,我們把這種關系稱之為“字符映射表”<3>。
圖 1-1 亂碼產生的原因
譬如說, 字符集Shift_JIS規定“う”這個字保存到存儲介質中為“82A4”;而字符集GBK規定存儲介質中的“82A4”代表字符“鷛”。 因此,我們在日文系統<4>中把“あいうえお”這個字符串保存在某個文件中,然后這個文件被帶到一個中文系統上,讀取這個文件后就產生了亂碼,如圖1-1。
在計算機的存儲介質<5>中,保存的皆為二進制的數據。計算機本身并沒有一種方法知道當前的數據是日文的“う”還是中文的“鷛”。<6>
任何一段文本(或者字符串)被保存到存儲介質中的時候都需要有一個字符映射表與之相對應。我們在處理文本(或者字符串)的時候需要清楚地知道當前文本(或者字符串)的編碼方式<7>是什么。
javac –encoding …
Java作為現在最流行的一種開發語言運行在Java虛擬機上。 運行前,需要把Java的源代碼編譯成byte code,編譯后的byte code被Java虛擬機解釋執行。<8>
Java虛擬機認為運行在其上的byte code的編碼方式是Unicode<9>,而Java源代碼以本地的(local)文本文件的形式存在,那么Java編譯器(通常是javac.exe)就需要知道當前的Java源文件對應的字符映射表,然后把其中的字符轉化為Unicode的字符。
非常幸運地是,一般情況下,Java有一套很好的機制幫助我們完成了后面的種種編碼轉換工作,而使得編程人員不需要太在意代碼的字符集以便把注意力集中在應用程序的邏輯實現上。
假設我們使用Java編寫一個小程序,在控制臺打印“あいうえお”五個字符。(如圖2-1) 由于“任何一段文本(或者字符串)被保存到存儲介質中的時候都需要有一個字符映射表與之相對應”,所以當我們使用文本編輯器(包括Eclipse等,非Microsoft Office)把一段文本保存到硬盤上的時候,我們需要指定當前這段文本所對應的編碼方式。(一般地,如果不指定字符映射表,文本編輯器會采用系統默認的字符映射表保存文本。) 也就是說,我們在日文Windows XP下編寫的Java源代碼會采用MS932<4>這個字符映射表保存到文件中(如圖2-1的①處)。
圖 2-1 編寫、編譯并運行Java代碼
因為Java虛擬機認為運行在其上的byte code的編碼方式是Unicode(如圖2-1的②處),所以Java編譯器會把Java源代碼編譯成Unicode形式的byte code。但是因為計算機本身并沒有一種方法知道當前Java源文件的編碼方式,所以,如果不指定編碼方式的話,默認地,Java編譯器會采用系統默認的字符映射表讀取Java源文件。即,如果在Windows XP日文版下編譯此Java程序,那么就會使用MS932格式轉化其中的字符串,如果在Windows XP中文版下編譯此Java程序,那么就會使用MS936格式轉化其中的字符串。如果把Windows XP日文版下編寫的Java源代碼拿到Windows XP中文版下編譯自然就會出現錯誤了。
因此,Java編譯器(特指javac.exe)向我們提供了一個-encoding的參數,我們可以使用-encoding告訴Java編譯器采用哪種字符映射表來讀取Java源代碼。
(如圖2-1的③處)那么在控制臺上可以正確地顯示“あいうえお”又是為什么呢?Java的System.out.println函數,默認地采用當前系統的默認字符映射表來輸出字符串。即,在WindowsXP日文版下會把Unicode的“あいうえお”按照MS932的格式輸出出來;在WindowsXP中文版下會把Unicode的“あいうえお”按照MS936的格式輸出出來。所以,編譯后的Java byte code可以在任何系統上正確地運行。也就是Java所謂的“Write Once, Run Anywhere.”
另外,我們因此也知道,如果使用System.out.println輸出的字符串是亂碼的話,也并不能說明此字符串是有問題的。
和web相關的編碼問題
在Java web系統中,我們主要使用HTTP協議在網絡上通訊。 我們把瀏覽器稱為HTTP客戶端,把web服務器稱為HTTP服務端。兩者通過請求(request)和響應(response)的方式傳遞數據。
圖3-1 Web中的編碼方式
設想在Windows XP日文版上使用IE瀏覽器提交“あいうえお”幾個字符,然后HTTP服務端再把這幾個字符打印在HTTP客戶端的屏幕上。(如圖3-1) 其中可能在五處發生了字符集的轉換,一處是輸入的時候,二處是把字符串通過網絡提交到服務器的時候,三處是在服務器端處理字符串的時候,四處是把字符串通過網絡返回給客戶端的時候,五處是在客戶端顯示的時候。
因為HTTP協議是一個文本傳輸協議,所以通過HTTP協議在網絡上傳輸的數據一定是有一個對應的字符集的。一般地,這個字符集是ISO-8859-1<11>。所以在2處和4處“あいうえお”的編碼方式是ISO-8859-1。我們通過實驗也可以證明,在1處和5處是HTTP客戶端指定的編碼方式<28>,在3處是服務器轉碼后的編碼方式<29>。
由于現有的HTTP客戶端和服務器端已經幫我們很好的封裝了HTTP協議的實現,所以一般我們在做Java Web Programming程序的時候不考慮在網上傳遞的數據格式。
在Java web系統中指定編碼方式
在Java web系統中, 我們遇到的最多的項目就是采用JSP和Servlet技術的項目了。那么,在使用JSP和Servlet技術的web系統中,設置字符集的地方可能有五處。 下面我們先來討論和響應相關的四處。
一、 pageEncoding<12>
我們可以在JSP頁面上加入指令(directives)
<%@page pageEncoding=“Shift_JIS”%>
JSP在運行前會被JSP編譯器編譯成Servlet,然后服務器加載此Servlet處理客戶端請求。 JSP中,指令是傳遞給JSP編譯器的參數,即告訴JSP編譯器如何編譯JSP。 Page指令中的pageEncoding屬性即告訴JSP編譯器使用的是哪種字符映射表來讀取當前JSP文件的源代碼。<30>如果沒有指定pageEncoding屬性,默認地,JSP編譯器采用當前系統的默認字符映射表來讀取JSP頁面。
圖3-1 Page指令的pageEncoding屬性
譬如,我們經常遇到的在Windows下編寫的Java web應用程序發布到Solaris后,JSP不能編譯,通常是由于沒有指定pageEncoding造成的。
二、 ContentType
另外,我們可以在JSP頁面上加入含有ContentType的page指令
<%@ page contentType=“text/html; charset=Shift_JIS”%>
這個指令的效果等同于
response.setContentType(“text/html; charset=Shift_JIS”);
達到的目的有兩個。
其一,在響應的HTTP文本中加入Content-type報頭(header)。客戶端會根據Content-type來讀取網絡上傳輸的數據。<13>
其二,通知web容器如何把文本(或者字符串)轉化為網絡上傳輸的二進制數據。<14>
需要注意的是,我們使一個字符串在網絡上傳輸和把一個字符串保存到文件中本質上是相同的,我們都需要一個字符映射表來映射字符和byte之間的關系。
圖3-2 關于設置ContentType的作用
假設我們需要把“あいうえお”這個字符串發送給客戶端,那么我們可以通過上面兩種方式(即page指令和response對象)設置ContentType為“Shift_JIS”。 設置后,服務器會認為是“あいうえお”是使用“Shift_JIS”編碼的字符串,并且以此變為比特流發送到客戶端。
客戶端在接收到HTTP響應后并不知道服務器端是“あいうえお”,它得到的只是一堆比特數據,那么它會根據HTTP響應的報頭Content-type中的設置,把這堆比特數據轉化為“あいうえお”。,<15>
一、 Meta Data
最后一個設置字符集的地方就是HTML頁面的meta標簽。 一般地
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
這里設置的字符集是告訴瀏覽器如何顯示HTML頁面。<16>
圖3-3 關于meta data的作用
總結, 對于客戶端頁面顯示亂碼,如果服務器端數據正常的話,那么可能是以上四種地方設置有誤。如果pageEncoding設置錯誤,一般表現為JSP頁面無法編譯,或者編譯后JSP頁面中固有的字符串不能正常顯示;如果Content Type設置錯誤,一般表現為JSP頁面全部或者大部分為亂碼,調整瀏覽器的顯示編碼格式后,仍然不能解決;如果meta data設置錯誤,一般表現為JSP頁面全部為亂碼,調整瀏覽器的顯示編碼格式后,可以解決。
每種變量如果不設置,則采用缺省值。如果不設置pageEncoding,JSP編譯器采用當前系統默認的字符映射表來讀取JSP文件;如果不設置Content-type,則采用ISO-8859-1<17>來作為Content-Type。
提交數據的編碼方式
一般地,客戶端提交給服務器的數據有兩種形式,GET和POST。 使用GET方式提交數據的時候,HTTP消息中沒有報體(Message Body),提交的數據存在于URL中<18>;使用POST提交數據的時候, 提交的數據存在于HTTP消息的報體中。 (另外,最近比較流行使用XMLHtttpRequest對象提交數據,我們將在下一節中討論。)
無論以哪種方式提交數據,這些數據都要經過編碼和URL Encoding<19>兩個過程。
圖4-1 URL Character Encoding<19>
在服務端會做一遍上述編碼過程的逆過程,從而得到“あいうえお”。 那么,問題就是服務端如何知道客戶端傳遞過來的字符串使用什么編碼方式?
首先,假設我們在頁面表單里輸入了“あいうえお”,那么HTTP客戶端(瀏覽器)會使用什么編碼方式對它進行編碼?HTTP客戶端(瀏覽器)會使用當前頁面的顯示字符集對它進行編碼。
顯示字符集是指顯示某個頁面的時候所使用的字符集。既不是meta中指定的字符集<20>,也非Content-type中指定的字符集。
在Microsoft Internet Explorer中,顯示字符集可以在下面這里看到。
圖4-2 IE的顯示字符集
在Mozilla Firefox中,顯示字符集可以在下面這里看到。
圖4-3 Firefox的顯示字符集
客戶端會根據當前頁面的顯示字符集編碼當前頁面上表單中的數據,并提交到服務端。
或者說,可以通過改變頁面的顯示字符集來改變提交數據的編碼方式。
其次,當數據提交到服務端,服務器端如何知道客戶端傳遞過來的數據是采用什么編碼方式?答案是因不同服務器不同而不同。
對于Tomcat, Tomcat會認為客戶端提交的數據全部采用ISO-8859-1的方式編碼,所以Tomcat會采用ISO-8859-1的形式解碼;而Weblogic會采用響應客戶端頁面的編碼方式解碼。但是無論哪種服務器采用哪種方式,都不能保證是我們想要的!
那么我們如何來指定我們想要的解碼方式呢?
在Java web系統中,我們可以采用request.setEncoding(<encoding_str>)來指定客戶端的編碼方式。 這行代碼即告訴request對象,客戶端的編碼方式是<encoding_str>。由此,我們就可以保證客戶端提交的數據和服務器端接收的數據采用同樣的編碼方式。
關于Ajax系統編碼方式的討論
2005年,Ajax作為最熱門的名詞之一使使用Ajax技術的項目一下多起來。 因為初次使用這種技術,所以其中最大的問題之一就是編碼問題。
Ajax的核心是XMLHttpRequest對象。總體來說, XMLHttpRequest對象是一個簡單的對象。 我們可以調用它的send方法向服務器端發送數據,以及可以調用它的responseText和responseXML來接收從服務器端返回的數據。
根據W3C的定義<21>, 我們使用XMLHttpRequest對象的send方法發送的數據總是為Unicode(UTF-8)的編碼方式;使用XMLHttpRequest對象的responseText接收的數據根據Content-type<22>中指定的不同而不同。使用XMLHttpRequest對象的responseXML接收的數據必須符合XML規范,且Content-type中的字符集必須和XML的字符集相同。譬如指定返回Shift_JIS編碼的XML數據。
response.setContentType("text/xml;charset=Shift_JIS");
response.getWriter().write("<?xml version=\"1.0\" encoding=\"Shift_JIS\"?>");
如果沒有指定XML的prolog, 則默認編碼為UTF-8<23>。 所以必須指定Content-type也應該為UTF-8<24>。
我們在提交數據的編碼方式中討論過,使用表單提交數據需要通過兩個階段的編碼,一個是使用某種字符集編碼,然后再使用URL Character Encoding編碼。這樣做是因為在HTTP協議下網絡中傳輸的是ISO-8859-1的字符,我們通過這種方式可以把任何字符集轉化為符合ISO-8859-1字符集的字符串。 但是,我們使用XMLHttpRequest提交數據的時候,就沒有URL Character Encoding這一步。
圖5-1沒有經過URL Character Encoding的數據
當然,幸運地是現在的服務器都非常健壯,可以做上述操作的逆操作,這樣我們編寫的代碼還是可以運行的。但是這不是一個好習慣。
一般地,我們使用Ajax技術的時候,需要手工的進行URL Character Encoding。 有三個Javascript函數可以幫助我們做這一點。escape, encodeURI和encodeURIComponent<25>。推薦使用encodeURIComponent<26>。
小結
在Java系統中,因為底層(byte code)為UTF-8的,所以我們的程序采用什么樣的編碼方式跟操作系統并沒有什么關系。在Java web系統中,無論客戶端的操作系統或者服務器的操作系統,只要能保證數據傳輸的時候采用統一的編碼方式,那么就不會有亂碼問題出現。
當在某個特定的系統下發生亂碼時,我們需要判斷亂碼發生的地方。此時,使用一些HTTP分析工具<27>是一個好辦法。 有時候,我們使用一些服務器或者框架(framework),它們為我們做了一些編碼轉換的工作,這時候問題就變得復雜起來。解決這種問題有兩點,其一是本文中提交的基本原理,其二是這種服務器(或者框架)的特性。
有時候我們開發的系統頁面非常復雜,在上面使用了框架(frame),Ajax甚至flash等技術,這樣有可能導致在一個瀏覽器窗口中存在幾種編碼方式,這種情況是可能存在的,遇到亂碼問題就需要具體問題具體分析了。
THE END
參考
<1> 對這篇文章感興趣的同學可以給我寫郵件
<2> 馮·諾伊曼結構也稱為普林斯頓結構,參考Wikipedia中關于“馮·諾伊曼結構”的介紹
<3> 參考INNA中關于的Character Sets的定義
<4> 默認在Windows XP日文版中系統的編碼方式為MS932,基本等同于IANA的Shift_JIS; 默認在Windows XP中文版中系統的編碼方式為MS936,基本等同于IANA的GBK
<5> 這里的存儲介質指內存中的變量,磁盤上的文件以及網絡上傳輸的數據等
<6> 其實這種說法并不準確,有些系統有默認的編碼方式,譬如XML默認編碼方式是UTF-8 (參考W3C中XML 1.0的規范文檔Extensible Markup Language (XML) 1.0 (Third Edition)的2.2 Characters);有些系統在字符串中加入了編碼方式,譬如郵件的標題前面帶有“=?GB2312?”這樣制定的字符集(參考RFC2047的2. Syntax of encoded-words)
<7> 下文中“編碼方式”,“字符映射表”,“字符集”均指同一個意思。
<8> 參考Java虛擬機規范 —— The JavaTM Virtual Machine Specification
<9> 參考Java虛擬機規范 2.1 Unicode
<10> HTTP協議的描述在RFC2616中
<11> 根據RFC2616,HTTP協議是基于文本的協議,在HTTP層傳遞的是使用ISO-8859-1編碼的文本數據。關于這個問題的具體討論在我講的《Java Web Programming》中第一章第二節。
<12> 本節內容可以參考JavaServer Pages Specification。 目前最新版的JavaServer Pages Specification 2.1是JSR245
<13> 在HTTP 1.0規范中, 如何確定網絡上傳輸的數據格式是采用Content-Type的,但是在HTTP 1.1規范中采用了Content-Language這個報頭。 另外,需要注意,編碼方式(字符集)和報頭Content-Encoding沒有關系。 參考RFC2616
<14> 這句話不準確,根據HTTP協議(RFC2616),HTTP協議是基于文本的協議,雖然在TCP/IP層傳遞的是比特流,但是在HTTP層傳遞的應該是文本數據。所以這句話應該說是在HTTP層傳遞的是使用ISO-8859-1編碼的文本數據。但是這樣會導致我們討論的問題復雜化,所以我們簡單認為在網絡上傳輸的是比特流。進一步討論可以給我寫郵件
<15> 在客戶端接收到服務器端傳遞過來的數據的時候,實際上是文本數據,采用ISO-8859-1編碼。 因為HTTP報頭完全符合ISO-8859-1的編碼格式,所以客戶端可以正確的讀取HTTP報頭中的信息。然后,客戶端會把HTTP報文按照ISO-8859-1轉化為比特數據,接著把這些比特數據按照HTTP報頭中Content-type的設置轉化為相應的字符。 我們在討論中為了簡單起見省略了這些步驟。
<16> 如果有Content-type報頭,那么瀏覽器一般會優先采用Content-type報頭中定義的字符集。
<17> 對于Tomcat服務器
<18> 雖然在RFC中沒有規定URL的最大長度,但是使用Internet Explorer支持的URL最大長度為2083個字符。 參考 KB208427
<19> 關于URL Character Encoding 可以參考RFC1738 2.2節
<20> 但是設置meta data會影響客戶端的字符集設置
<21> 參考W3C的《The XMLHttpRequest Object》
<22> 此處為HTTP 1.0規范的定義, 在HTP 1.1規范中, 編碼方式定義在Content-Language中, 參考RFC2616
<23> 參考W3C的《XML Base》
<24> 如果沒有指定,默認的是ISO-8859-1
<25> MSDN中對這三個函數的描述分別如下 escape, encodeURI 和 encodeURIComponent
<26> 在很多目前流行的Ajax框架中都使用這個函數。而仍然有些舊的系統在使用escape,我以為這是個誤區
<27> 在我講的《Java Web Programming》中使用了一種叫Webscrab的工具,另外我寫這篇文章時使用一個叫做Http Analyzer的工具
<28> 非客戶端操作系統默認的編碼方式, 在“提交數據的編碼方式”一節中具體討論
<29> 非服務器端操作系統的默認編碼方式, 在“提交數據的編碼方式”一節中具體討論
<30> 參考我的講座《Java Web Programming》中第六章第一節的內容