在基于J2EE的B/S應(yīng)用中,中文亂碼是一個永恒的主題,永遠(yuǎn)都無法回避。誠然對于一般的程序員,我們沒有必要對編碼進行深刻的研究。但是至少我們需要了解:
①編碼基礎(chǔ)
②String的getBytes([encoding])方法內(nèi)幕
③String的toCharArray()方法內(nèi)幕
④輸出時的編碼與亂碼原因
⑤UTF-8的編碼規(guī)則和GBK如何轉(zhuǎn)換到UTF-8
⑥字符在各種表現(xiàn)形式下的值
⑦native2ascii命令的用法
正因為Java中采用了Unicode編碼作為中介,所以任何初始的輸入和最終的輸出都會有:
①從byte[]----》encode字符---》Unicode的輸入轉(zhuǎn)換
②從Unicode---》encode字符---》byte[]的輸出轉(zhuǎn)換
一個典型的J2EE B/S應(yīng)用,從客戶端輸出到最終服務(wù)器端的輸出,需要經(jīng)歷如下的流程
其中,客戶端包括了:操作系統(tǒng)、Web瀏覽器、靜態(tài)網(wǎng)頁、JS等。服務(wù)器端包括了:Servlet、JSP、Filter、配置文件等。這中間的任何一步配置不當(dāng)或轉(zhuǎn)換不當(dāng)都有可能導(dǎo)致亂碼或者?的出現(xiàn)。下面就將針對各個流程環(huán)節(jié)中可能出現(xiàn)的亂碼地方,做一次全面的總結(jié)并給出基本的對策:
【客戶端】
①操作系統(tǒng)
對于Windows操作系統(tǒng),中文語言的設(shè)置在控制面板中進行
如果把最后的“高級”中改成英文(美國),你會發(fā)現(xiàn)很多不支持多國語言的軟件,菜單或工具欄的中文立馬變成?了。
對于Linux系統(tǒng),臨時的設(shè)置可以通過下面的設(shè)置來指定系統(tǒng)使用中文環(huán)境和UTF-8編碼格式

如果是需要永久性的起作用,必須在 /home/<user>/.bashrc文件中設(shè)置
②瀏覽器
現(xiàn)代的瀏覽器都可以動態(tài)的設(shè)置瀏覽頁面的編碼
【服務(wù)器端】
①服務(wù)器的配置文件
對于Tomcat,可以在server.xml文件中進行配置



其中黃色高亮部分就是將請求的內(nèi)容使用UTF-8進行編碼。但要注意:這只是針對Get請求的!如果要連Post請求的內(nèi)容都使用UTF-8編碼,那么就要使用下面的過濾器了。
②過濾器
它是在請求未到達(dá)對應(yīng)的servlet或JSP之前進行攔截,然后通過設(shè)置request的character encoding來一次性進行轉(zhuǎn)碼。










































③HTTP Request封裝器
在上面的filter代碼中我們見到了一句注釋:HttpRequestWrapper req = new HttpRequestWrapper(hreq);這是下面要介紹的一種方法。如果學(xué)習(xí)過設(shè)計模式我們會知道所謂的“Wrapper”類,其實都是一個Decorator/Proxy模式的實現(xiàn)。即通過在“Wrapper”類中包含一個對象,然后暴露和該對象同樣方法的接口,在客戶端調(diào)用該對象的方法之前做一些手腳。JDK中的動態(tài)代理DynamicProxy原理就和這個類似。
在Servlet的API中有一個類:HttpServletRequestWrapper。如果查看API我們會發(fā)現(xiàn)有趣的地方:
原來這個類也是實現(xiàn)了HttpServletRequest接口的。于是乎我們可以通過創(chuàng)建一個繼續(xù)于該類的“Wrapper”,然后覆蓋HttpServletRequest的getParameter(key)和getParameters()方法,在這兩個方法返回字符串之前,進行重新編碼。














































于是我們可以修改一些前面的filter,去掉那句注釋和下面的request.setCharacterEncoding("UTF-8")。為什么呢?因為我們知道reuqest.getParameters(...)方法是對GET和POST請求同樣的,現(xiàn)在我們已經(jīng)在每次獲取參數(shù)值之前做了轉(zhuǎn)換,所以不論是GET還是POST請求,都可以正常的轉(zhuǎn)碼了。
要記住:request.setCharacterEncoding(...)方法只對將數(shù)據(jù)保存在HTTP信息體中的POST請求有效,對于將數(shù)據(jù)直接粘附與URL后面的GET請求是無效的。但是不論GET還是POST請求,他們都是通過request.getParameter(...)來獲取參數(shù)的。這其實是一個一勞永逸的方法!
④Servlet
如果在前面我們正確地設(shè)置了Tomcat,過濾器,封裝器,那么對于Servlet來說,要獲取參數(shù)就是簡單的request.getParameter(PARAM_NAME);了,但是如果沒有設(shè)置過濾器或封裝器,就只能手工地調(diào)用下面的語句:
String tmp = request.getParameter(PARAM_NAME);
String param = new String(tmp.getBytes("ISO-8859-1"), encoding);
通常來說這個encoding就是你應(yīng)用的編碼,亦是JSP頁面的編碼。如果JSP頁面是GBK,而你的代碼是UTF-8,那么用UTF-8做encoding就肯定亂碼了。此時就得用上前面所說的GBK轉(zhuǎn)UTF-8方法,得到原始字節(jié)后進行擴展,使之成為UTF-8格式的字符數(shù)組再進行解碼了
PS:上面這種情況是經(jīng)常出現(xiàn)的。特別是在你要抓取別人的網(wǎng)頁內(nèi)容時,對方可能使用的是GBK/GB2312編碼,而你的Web應(yīng)用是UTF-8編碼。
在解決完請求的亂碼問題之后,接下來要解決的就是響應(yīng)的亂碼問題了。響應(yīng)的亂碼問題通常發(fā)生于響應(yīng)內(nèi)容的編碼和JSP頁面,HTML網(wǎng)頁的編碼不同,所以在前面的過濾器中我們一并把這個問題給解決了:

或者調(diào)用

來看看這個用法和HttpServletRequest.setCharacterEncoding有什么區(qū)別吧!
Sets the character encoding (MIME charset) of the response being sent to the client, for example, to UTF-8. If the character encoding has already been set by setContentType(java.lang.String) or setLocale(java.util.Locale), this method overrides it. Calling setContentType(java.lang.String) with the String of text/html and calling this method with the String of UTF-8 is equivalent with calling setContentType with the String of text/html; charset=UTF-8.
This method can be called repeatedly to change the character encoding. This method has no effect if it is called after getWriter has been called or after the response has been committed.
和前者不同,這個方法可以在getWriter()被調(diào)用后再調(diào)用,而且可以被多次調(diào)用。而request.setCharacterEncoding只能在request.getParameter之前調(diào)用。
其次就是這個方法的優(yōu)先級是最高的。如果之前已經(jīng)通過setContentType或setLocale指定編碼,再次調(diào)用該方法會覆蓋之前的配置。
⑤JSP
在JSP頁面中,和編碼相關(guān)的幾個參數(shù)有
A.contentType
B.pageEncoding
這兩個參數(shù)有什么不同呢?contentType是用來告訴瀏覽器,當(dāng)接收到服務(wù)器傳輸回來的響應(yīng)體內(nèi)容后要用什么編碼解析,這個是沒有疑問的。但pageEncoding呢?
我們知道JSP頁面在運行時會被容器編譯成.class文件,對于文件必然有“文件編碼”這一說。對了!pageEncoding就是告訴容器要以什么方式讀入JSP文件。如果你的頁面有中文字符或其他雙字節(jié)字符,那么就必須用UTF-8,GBK,Big5等支持雙字節(jié)的字符集了。
值得注意的是,雖然容器以pageEncoding指定的方式讀入JSP文件,但在最終編譯成.class文件后,還是以Unicode保存的。例如JSP頁面的"中文"二字,在.class文件中會以"\u4e2d;\u6587;"保存。
⑥HTML
如果說我們可以通過response.setContentType或page指令來指定動態(tài)頁面的編碼格式,那么對于HTML這樣靜態(tài)的頁面就無能為力了。于是就得使用另外一種方式了:

⑦屬性配置文件
Java的國際化(i18N)是很多Web應(yīng)用必備的功能之一,對于菜單文字,提示信息我們都可以用.properties的方式保存,但是在保存之前我們需要將內(nèi)容轉(zhuǎn)換成unicode的值,于是就要使用native2ascii命令了。具體可以參考前面的【Java基礎(chǔ)專題】編碼與亂碼(07)---native2ascii命令的用法
【工具配置】
在團隊合作中,由于每個人的開發(fā)環(huán)境不一致,通常會出現(xiàn)一個人本來可以正常顯示中文的JSP文件或者Java文件,到了另外一個人的機器上就變成亂碼了。通常這都是由于編輯器的編碼不一致而造成的。以Eclipse為例:
在Dreamweaver中,我們可以這樣設(shè)置/改變頁面的編碼
注意:在團隊開發(fā)中,明確開發(fā)環(huán)境的配置時通常會忽略IDE編碼的選擇,導(dǎo)致大量不可逆的亂碼情況出現(xiàn)。應(yīng)該值得注意
良好的編程習(xí)慣,對編碼和數(shù)據(jù)傳輸流程的清晰認(rèn)識,規(guī)范的配置是確保JavaEE應(yīng)用不會出現(xiàn)亂碼的三大法寶
-------------------------------------------------------------
生活就像打牌,不是要抓一手好牌,而是要盡力打好一手爛牌。