閱讀465 返回首頁    go 技術社區[雲棲]


java小技巧-關於亂碼的那些個破事

 這篇文章說難不難,說簡單不簡單,其實更多的在乎與經驗,不過就本文來說,我更多的想闡述為什麼會產生亂碼,什麼情況下會產生亂碼,然後如何去解決亂碼,對於有哪些亂碼情況非常多,並不一定是那一種情況導致的,清楚了過程和原理,那麼亂碼都不在乎是什麼大問題:

 

本文綱要:

1、亂碼的來源與本質。

2、什麼時候會產生亂碼?

3、如何分析亂碼和解決亂碼?

4、我所遇到過的亂碼情況。

 

第一部分:亂碼的來源與本質:

其實,亂碼的來源要追溯到語言文字在計算機中的表達方法了,也就是在計算機中存儲和顯示過程中,計算機本身並不識別文字,而隻是識別數字本身,所以對文字的存儲和顯示以及轉換我們都需要一個編碼的過程,在常見的阿拉伯數字和英文字母,以及常見的英文符號,計算機在早期使用了256個字符就足以表達,那個時候也不存在什麼亂碼的問題;隨著計算機的普及,需要適應越來越多的文字,所以,256個字符以已經不能滿足大家的需求,所以為了適應更多的文字,出現了各種各樣的編碼,有些是為了本身的語言,有些是為了國際標準,類似可以表達中文的字符集就有:Unicode、GB2312、GBK、GB18030、UTF-8等等,以GB打頭的都是支持基本的字符和中文的,GB2312隻能支持六千多個字符,GBK可以支持兩萬多,GB18030可以支持兩萬七千多,他們向下兼容,采用2個字節表達中文,而UTF-8是采用3字節表達一個中文。

那麼亂碼產生的原因就可以簡單歸結為:在進行數據的編碼、解碼的過程中,編碼和解碼的所使用的方法不一樣;這個說起來貌似簡單但是抽象,我們先定位到這裏,然後用下麵的內容來充實它(我們通常將數據編碼和解碼的過程,如果是在程序中是麵向對象的,就稱之為序列化和反序列化的過程;就像webService也是基於RPC協議,在對象進行序列化和反序列化的時候也可能會出現亂碼)。

編碼本身也首先由對應的操作係統所能支持的字符集來決定,其實就目前來說,這些字符集幾乎所有的操作係統都會支持,所以這個問題不用考慮太多(查看操作係統所能支持的字符集locale -e);

那麼操作係統默認會使用一種字符集,所謂默認使用也就是啟動進程的時候,或者說某個運行於這個操作係統的進程發生數據交換的過程中,如果沒有指定字符集,就會使用默認的字符集。

操作係統本身的字符集可以通過環境變量來控製,對於一個類似於通過終端登錄上去的用戶,也可以默認指定字符集,就是在用戶級別下麵設置對應的環境變量即可。

同一個用戶下麵可以啟動不同的進程可以在進程啟動時通過export設置對應的默認字符集,這也是可以的;

如果某個進程要去進行某種IO操作,而這個IO操作的來源編碼和進程默認字符集不一樣,那麼就要在讀取的時候指定轉換的字符集或手工將二進製數據轉換為字符格式的數據。

 

我們再反過來說,也就是當一個程序發生某種交互的過程中,如果當前程序中有指定的字符集,就用指定的,如果沒有就會逐層向上去找默認的:從當前進程、父進程、用戶、OS逐層向上找;所以它的情況就會變得比較複雜了。

 

OK,可能看到這裏你會更加的暈,那麼一般情況下編碼和解碼同時發生一般隻會發生在兩個進程之間或者兩個時間點上,因為同一個進程內部通信並且發生在同步調用上是沒有必要編碼的,就像你在同一段程序中,要傳遞一個String到子方法中是不需要發生編碼的一樣。

 

那麼我們將亂碼產生的原因再細化一層就是:在數據進行了兩個進程之間的通信或發生在兩個時間點的編碼和解碼工作,就有可能會發生亂碼。

 

那麼兩個進程通信是什麼意思?發生在兩個時間點是什麼意思?為什麼需要編碼和解碼?

所謂兩個進程通信就是指程序和程序之間的交互,例如:程序之間通過RPC、HTTP、RMI等傳遞數據,這些通信可能是網絡交互,可能就是本機的交互,但是他們始終是進程與進程之間的通信,最簡單的例子就是服務器向客戶端瀏覽器通信,客戶端的瀏覽器本身就是一個進程,每種瀏覽器可能會采用不同的線程、緩存處理方法來與服務器端通信,不過總體上會基於一個國際標準的協議規範。

而發生在兩個時間點是指,將數據放在某個中間位置,這個中間位置是需要被編碼的,另一個時間點有一個進程去讀取這個中間數據(這個進程可能是同一個進程,可能不是);例如:程序將一個帶有中文內容的信息保存到一個文件中,然後這個文件可以被用戶所下載,下載的時候就需要讀取這個文件的內容,讀取這個文件的內容可以是另一個進程來完成,也可以是同一個,隻要兩者的編碼和解碼方法不一樣,就會產生亂碼。

 

第二部分:什麼時候會產生亂碼?

通過第一部分的閱讀,相信大家在對亂碼的認識上有一個原理上了解,我們沒有必要糾結於原理上的非常細節的細節,最終你隻需要知道他們在什麼情況下產生亂碼了;這樣說好像很抽象,我們舉一些常見的web應用係統的例子,可能對大家的理解會更加好一點,在一個web請求中,可能會發生以下動作:

1、請求普通的JSP或VM的渲染頁麵;

 

首先客戶端發起請求,客戶端的請求內容將會在提交前被瀏覽器所編碼,這個編碼和字符集以及協議有一些編碼,而且會形成請求的http頭部信息,如果沒有手工去做編碼的過程,那麼瀏覽器為你的編碼一般是瀏覽器在請求這個資源文件時默認的編碼;然後服務器端接受的是一個二進製數據(注意,網絡中傳輸的就是二進製數據,這個二進製數據就是被編碼後的數據);

 

如果你的URL上麵有中文的話,要注意了,你可能會遇到時而好用,時而不好用的情況,因為瀏覽器會自動將這個中文進行編碼,至於它怎麼編碼,就又和瀏覽器本身和訪問有些關係了;如果你需要指定一種編碼集的話,需要提前將其編碼掉,可以使用java的URLEncoding或者js中提供的一些編碼方法。

 

然後服務器端需要進行反解析,這部分可能會被服務器進程所控製,也可能被應用本身所控製,也有可能會被應用中的某個框架所控製,也可能會被程序本身所控製,但是控製的基本原理都是實用request請求上設置的字符集所決定,也就是request設置這個字符集是告訴request對來源數據應當用什麼樣的字符集去解析,這部分可能會被框架本身所做,如果沒做回到上麵一章就是逐步向上找;

 

服務器處理完請求後,就開始在程序內部進行運行,這個時候可能會去讀寫取數據庫,其實,應用程序和數據庫交互是通過jdbc去交互的,jdbc本身是和數據庫之間建立一種TCP協議,也是打開一個socket流,傳輸過程仍然是進程和進程之間的交互,所以在交付前,jdbc需要進行對應的編碼工作,而返回時jdbc會進行相應的解碼工作,同樣數據庫端也需要相應的編碼工作才能返回數據;數據庫內部也有進程和進程之間交互,包括和磁盤之間的交互;在Oracle內部一般是統一字符集,而MySQL會存在很多級別的字符集,所以如果MySQL表級別的字符集和數據庫級別的字符集不一樣的時候,需要在URL上設置字符集才能好用,jdbc本身的實現過程是由廠商來決定的,它中間可以自己指定字符集或和數據庫之間通信拿到字符集都可以完成。

 

服務器端得到信息後,然後開始渲染數據到頁麵,這個過程也存在字符集的問題,這個也可能會被框架本身所決定,如果是jsp渲染一般有pageEncoding來決定渲染的字符集(但是這裏並不代表瀏覽器就要用這種字符集來解析),velocity一般是由配置文件告訴框架,當然也可以自己response中將對應的中文字符串通過.getBytes("GBK")來進行編碼,當response輸出後,剩下就是瀏覽器的解析了。

 

瀏覽器會如果是首次請求這個站點或已經失效,那麼會使用:<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>中指定的字符集,否則會去采用默認的某種字符集進行嚐試,一般第一次請求這個站點也不會出現什麼問題;如果你發現請求一個點偶爾出現亂碼,清空緩存就沒有了,那麼就是瀏覽器記住了某些東西,這種問題一般是靜態資源的編碼導致了一些站點記憶並且服務器端沒有告訴瀏覽器應該用什麼方法來解析,此時即使在head部分加上上麵那段信息也不好用,這個頭部隻是頁麵的頭部,而並不是真正http交互的頭部;所以對於這類情況,需要在服務器端輸出的時候指定Content-Type,在JSP中就是通過:<%@ page contentType="text/html;charset=GBK" %>,在java中是通過:response.setContentType("text/html;charset=GBK");這句話是告訴瀏覽器,這是一個文本類型的html格式數據,請你通過GBK進行方解析。

 

2、瀏覽器請求一個靜態資源(JS這一類)

其實CSS不太可能有亂碼,因為這個裏麵幾乎不會有非英文字符,而圖片是二進製數據;JS有可能是用戶自己編寫,那麼也有可能有亂碼,請求的過程和上麵差不多,差別有兩點;其一是沒有數據庫操作,其二是JS如果沒有用戶自己處理的話,web容器在處理在調度時發現資源沒有處理的servlet,那麼就會向容器上層進行拋,每一種容器都會有自己的一個默認的servlet來處理靜態資源,一般我們叫他:DefaultServlet,mapping的時候,就是使用 / 關鍵字來mapping,就是找不到的就走這裏(換言之,如果你不想用服務器端默認的DefaultServlet,你可以自己寫一個,最簡單的寫法就是,將文件直接用二進製讀出來輸出去(用二進製讀出來不會進行編碼,直接就是文件本身的編碼,所以隻需要瀏覽器能知道該怎麼解碼就可以了,這樣性能也會更好),當然如果這個Servlet不是使用二進製處理的,那麼它應該就會有一個參數讓你設置它的defaltEncoding,這個要看具體的應用服務器的實現了)。

這個servlet一般會用二進製來處理這些靜態資源,而且會判定資源的lastModified以判定文件是否需要重新加載,以及如果文件沒有被修改過,那麼就想客戶端輸出304狀態(瀏覽器會認為這個文件沒有被修改過),如果被修改則直接被裝在輸出,狀態為200,但是靜態文件在服務器端一般是不會被修改的。

如果是304狀態,瀏覽器此時也有可能會采用站點以前的某種字符集,這個一般不會出現什麼問題,除非你的JS文件本身的字符集發生了變化,其次就可能是瀏覽器的bug了;如果JS真的字符集有可能會發生變化,而且不想因為瀏覽器的問題導致亂碼,那麼js文件上可以增加一個charset告訴瀏覽器是什麼字符集解析<script type="text/javascript" src="abc.js" charset="GBK"></script>注意,這裏的字符集和文件本身的字符集關係好就可以;如果你覺得這樣還不夠帥,那麼告訴你一個狠招,那麼就是將DefaultServlet重寫掉,衝寫的時候,在輸出文件時就需要設置:response.setContentType("text/javascript; charset=GBK");代表是一個文本類型javascript,頭部告訴瀏覽器使用GBK進行解碼。

 

3、服務器進程之間通信

其實這種問題上麵有說過,就是程序和數據庫之間就是類似的例子,不同的程序通信也是這個道理,在java方麵,還存在一類特殊的操作就是對象序列化,其實所謂的對象序列化就是在數據結構方麵做了一個特殊的標誌而是,本身對象是沒有序列化的能力的;它最終還是需要傳遞數據,隻是結構和數據按照某種特定的格式傳輸,也就是說理解對象的序列化同樣可以使用上麵的說明來理解;

同樣和文件的交互就是類似的IO操作,存儲在磁盤上肯定是二進製的信息,所以需要在存儲前將其編碼,在java中如果使用默認的FiltReader和FileWriter,而沒有進行編碼那麼就會像第一章所述采用一些默認的信息;這也是為什麼有些人說自己的程序在自己調試程序的時候是好用的,為什麼放到服務器上就不好用了,因為服務器上某些默認的環境信息和你的本地不一樣,這種情況不僅僅針對於讀寫文件,在進程的通信各方麵都有這種說明;要對文件進行字節轉換字符,或字符轉換為字節進行磁盤讀寫,有兩種方法保證字符集的一致性,一種方法就是文件的內容提前轉換為byte數據,通過InputStream和OutputStream數據來讀寫;另一種方法就是通過FileInputStream和FileOutputStream,轉換為對應的Reader和Writer的時候,需要在參數上設置字符集,如:new InputStreamReader(new FileInputStream(file) , "GBK");至於是否進行Buffer是另外的問題了,這裏就是告訴如何進行編碼和解碼的設置過程,同理在對象序列化的時候也是通過這類似的方法來進行包裝。

 

 

第三部部分:如何分析亂碼和解決亂碼?

通過上麵兩章的說明,應該知道亂碼的原因和常見的亂碼情況,那麼如何分析亂碼呢,我想你無論是那一種亂碼也逃不掉我們在第二部分所討論的亂碼大的種類,但是小的種類應當如何去定位呢,這個一個是逐步細化,一個是經驗積累,有些亂碼問題及時知道是怎麼回事也未必知道如何去解決,尤其是麵對一些客戶端瀏覽器本身的問題,所以解決方法也是很重要的。

 

分析亂碼你看到了,中間隻要有發生任何兩個進程通信或時間點差異的都有可能會發生亂碼,最基本的你要學會斷點跟蹤,一般這類問題是首先要跟蹤,也就是在那兩個進程之間交互的時候發生了亂碼,也許是一個小小的細節,也許是連鎖反應,也許是蝴蝶效應,看具體情況而定,按照上麵每種情況再加上應用場景中框架本身的情況,那麼亂碼的種類我們估計可以數出上千種、上萬種,經驗固然重要,不過如果頭兩次遇到亂碼解決了總結經驗是一種成長,遇到數十次還是沒有理解為什麼是亂碼,那麼永永遠遠都會出現亂碼,而且可能在偶然的時候出現自己可能理解不了的亂碼。

 

所以分析亂碼剛開始隻能靠自己碰到或者自己去模擬,強製將一些字符集設置得不一樣;而有一些亂碼遇到的經驗,就可以解決一些,這種就是憑借經驗了,但是遇到新的情況就又要慢慢去“猜”了;如果要成為亂碼解決的高手,就要理解原理,而不是猜,亂碼的情況多種多樣,科學的理解亂碼,原理是為了理解亂碼出現的原因(其實就兩點:不同的進程、不同的時間點),跟蹤是為了定位亂碼出現在哪裏,認識本質一般就能定位到對應應用場景為什麼會出現亂碼,根據應用場景去做對應的調整就可以解決本質性的問題(所謂對應的場景,就是這種亂碼出現的抽象粒度,在對應的抽象粒度去修改而不影響其他的代碼;其次,如果是框架、服務器本身所提供,那麼就從框架服務器本身所需要的配置信息上去解決即可,第二點也可以歸結為抽象粒度,隻是較高級別的抽象粒度)。

 

OK,這這一段貌似看起來像是廢話,因為什麼也沒說,也沒說怎麼定位亂碼,沒說怎麼解決亂碼;但是我並不這麼認為,因為亂碼本身的定位就是場景所決定,這裏宗旨是首先學會去理解原理,然後跟蹤定位,通過本質認識原因和抽象級別,進而和對應場景結合來解決,就場景而言千變萬化,沒有說誰就是正確的,關鍵的是你需要有整個對應場景處理過程的理解,以及清晰冷靜的跟蹤到問題的點上來,進而通過原理去分析這個過程;亂碼的分析和解決,切忌之處為妄下結論,過於依賴於經驗本身,甚至於認為是java本身的問題,這樣很難解決問題甚至自己不可能解決問題;下麵第四部分給出一些常見的場景應用場景:

 

第四部分:我所遇到過的亂碼情況。

我所遇到的亂碼情況也不能一一列舉,隻能說在兩年前自己通過一些較為底層的接觸,了解了亂碼原因後,後來解決亂碼問題已經不是什麼太困難的事情,直到前端時間我遇到了和瀏覽器本身的緩存所導致的顯示亂碼,才將我折騰了一翻,不過這也算是一種場景下的表現,在對應的服務器、對應的框架、對應的代碼、對應的瀏覽器下偶然的發生了,讓人難以捉摸,不過還好,最終將偶然變成必然進行了模擬,更加了解到交互的細節,解決了這個問題,這個問題也是兩年多來遇到最難搞定的亂碼問題了。

 

1、在請求一個資源時,URL上帶上中文,沒有編碼,導致了亂碼:這種不推薦,但是非要用,就用JS對URL進行Encode操作,同理,兩個程序之間通過Http進行交互,如果需要攜帶中文也通過這種方法編碼,否則瀏覽器為你編碼,編出來是什麼就不好說了,不同的瀏覽器會采用不同一些默認值和緩存手段來處理。

 

2、請求服務器端一個資源,數據中包含有中文,中文在客戶端使用了utf-8編碼規則,服務器端接受的是亂碼,通過上麵第二章的分析,我們可以看到一個請求到服務器上是請求一個進程,這個進程會交給對應的應用下的程序去處理,應用下有可能有框架去處理,框架最終交給業務代碼去處理,也就是中間任何一個部分都可以進行解碼操作,如果發生的轉碼行為不一樣或者發生了兩次不一樣的轉碼就會出現亂碼(發生兩次不同的編碼就有可能會產生不好解決的問題),如在很多tomcat中默認的請求是按照IOS8859-1來處理的,struts你可以通過設置struts.i18n.encoding來控製它的編碼,但是不論在哪裏設置,最終是在講byte轉換為char的過程中進行不同的處理,如果服務器和框架本身沒有對它進行處理,那麼你可以使用一個:request.setCharacterEncoding()來處理,但是這個一定要和提交請求的編碼一致,這個代碼如果過濾器後麵沒有其他的處理,就可以放在過濾器中保證這個應用下麵都是用這種字符集來接受請求的參數;

如果轉碼的和客戶端發送的編碼一致,但是程序處理需要另一種編碼,那麼就你就需要先按照對應編碼轉換為byte[]數組,然後再然後自己需要的編碼進行解析出來,這個過程就是new String(a.getByte("IOS-8859-1"),"UTF-8");這樣就OK了。

在weblogic服務器上可能會需要設置(在web.xml中增加,這個為全局參數,這個參數會被weblogic服務器在啟動對應的應用時讀取並使用):

<context-param>
<param-name>weblogic.httpd.inputCharset./*</param-name>
<param-value>GBK</param-value>
</context-param>
補充一點:這類請求也同時可以解決你在通過了一個servlet後forward到下一個jsp頁麵使用request.getParameter的出現亂碼的問題,因為request對象都是一個。

 

3、頁麵在輸出時本身為亂碼,如果是velocity的配置就要看看在配置velocity中的參數:output.encoding是否正確;如果是JSP那麼就要看看頁麵頭部指定的:pageEncoding="UTF-8",或者contentType="text/html;charset=UTF-8",默認會使用後者,前者主要是在輸出時使用,但是沒有的話後者可以起作用,而前者不會去設置輸出的頭部,也就是瀏覽器獲取到頭部信息,而contentType正好是瀏覽器的頭部信息,如:

指定了contentType這裏就會變成對應的頭部,或者設置Header時,將Key設置為:Content-Type即可;不設置這個,並不代表瀏覽器解析出來就是亂碼,要看情況,但是設置了這個基本不太可能是亂碼,因為即使本地有緩存,頭部信息瀏覽器都會去請求服務器端的,注意這個頭部並不是客戶端瀏覽器在<head></head>內部設置的,這個設置在客戶端某些瀏覽器有一些站點緩存的時候,不會被生效,所以最終極的方法還是在頭部去設置。

 

當然頁麵輸出亂碼有可能本身某個步驟讀取出來的數據就是亂碼,而並不一定是渲染的時候,那麼這個部分就需要跟蹤代碼了,比如數據庫中本身存的就是亂碼、或者讀取數據庫的時候變成了亂碼、獲取讀取某個文件的時候出現的問題、或者和其他進程中交互中出現的亂碼,其原理一致 ,框架能控製的在框架控製,框架不方便控製的就用代碼控製。

如在weblogic中,你可以在weblogic.xml中配置一段:

<jsp-descriptor> 
<jsp-param>
<param-name>compilerSupportsEncoding</param-name> 
<param-value>true</param-value> 
</jsp-param> 
<jsp-param> 
<param-name>encoding</param-name> 
<param-value>GBK</param-value> 
</jsp-param> 
</jsp-descriptor>

來說明你的就jsp要使用什麼樣的一個編碼集來進行編譯(jsp渲染的時候是首先要將jsp內部本身轉換為servlet,然後在輸出的)。

4、客戶端瀏覽器本身的頭部問題,上麵已經說明,通過contentType的設置即可解決問題,這類問題一般是偶爾出現,而且是因為站點引用資源多種編碼集導致的緩存在瀏覽器中的一些內容,而服務器端沒有返回contentType的時候就會發生。

5、客戶端JS或CSS的靜態資源亂碼,其實上麵也是說過了,就是DefaultServlet如果提供了字符集的選擇就將其設置對應的字符集,如果沒有,那麼它一般是采用二進製處理的話傳輸過程中不會出現問題,客戶端接收的時候,在js上設置一個charset,即:<script type="text/javascript" src="abc.js" charset="GBK"></script>,如果這樣設置顯得過於麻煩,或者在某些部分很惡心的瀏覽器上不見效,那麼就將DefaultServelt進行重寫,在輸出內容時候,設置response.setContentType("text/javascript; charset=GBK");至於靜態資源如何去做緩存,或是否做緩存,是業務上的問題,默認靜態資源會做一定程度的緩存,這個具體要看WEB服務器的默認指標和服務器設置。

6、讀寫文件時亂碼,如果讀寫文件不需要識別內部的數據,進而作一些處理,那麼就不需要轉換為字符,直接使用字節流進行處理即可,這樣最快速,減少兩個步驟的轉碼過程;如果是需要處理內容,那麼在Reader和Writer部分,設置字符集,如:new InputStreamReader(new FileInputStream(file) , "GBK");如果你不設置,而直接用FileReader和FileWriter將會采用某種父進程的默認字符集。

7、和遠程的程序進行簡單socket交互時,同上不需要任何處理數據轉換,那麼用二進製處理最穩定、最快速;

8、和遠程的程序進行類似RPC協議,也就是傳遞的都是具有數據類型的信息,序列化的過程需要交給框架,此時需要在框架內部指定序列化和反序列化的字符集,或者在程序中指定,否則使用默認。

9、URL上本身有中文,那麼就需要提前編碼,可以使用java中的URLEncoding提前編碼好渲染到前端,也可以使用JS提供Encoding方法來完成。

 

其實還有很多很多亂碼的場景,上麵說的應該不是8種亂碼,而是8大類,其實也算是最初提出3大類的細化版本,還可以繼續細化,總之如果你真想出現了亂碼,可以讓他千變萬化,要讓它不出現亂碼關鍵是編碼和解碼的方法要一致,現今的亂碼不能說用一種公共的方法來解決,因為標準和個性化是並存的,到目前為止不能誰將誰幹掉;但是可以用一種較為科學的流程和方法來處理;每種應用場景都有可能遇到它所存在的亂碼的可能性,關鍵是要了解其實質上在哪裏發生的,哪裏發生了一個錯誤的轉換。

最後更新:2017-04-02 06:52:02

  上一篇:go VS2010中使用JSONCPP方法
  下一篇:go 麵向對象的幾個問題