做Web開發(fā)的IT人,,如果工作中沒遇到過幾次亂碼的問題,,估計都不好意思說自己是開發(fā)工程師。 :) 而亂碼問題也是各種各樣,,有保存到數(shù)據(jù)庫中是亂碼的,,有在服務端接收到參數(shù)是亂碼的,有在后臺返回到客戶端時候出現(xiàn)亂碼的…… 這形形色色的亂碼問題,,如果處理起來不得要領,,著實會讓開發(fā)人員費不少工夫。 本文,,將從亂碼的產(chǎn)生原因,,應用服務器內(nèi)部對參數(shù)的處理各方面詳解原理及解決方案。 在開發(fā)中,,只要有IO的地方,,都會涉及到編碼。例如下面的代碼:
你猜這個會輸出什么呢,? 這個其實是和你的文件編碼有很大關系的,,可能會輸出
也有可能會輸出成下面這個樣子
你可以改動IDE中的文件編碼重復試幾次,。那為什么會產(chǎn)生這個原因呢? 我們來看這行代碼執(zhí)行過程中的調(diào)用棧: 我們看到,,簡單的一句輸出,,也是有編碼的。 看看CharsetEncoder,,實現(xiàn)類真多啊 再比如我們都無比熟悉的equals方法,,先聲明常量STR如下:
你認為,,這個時候會有輸出嗎,? 答案是看情況! 當我把Constant類以UTF-8為編碼保存后,,把包含if邏輯的代碼以GBK保存之后,,equals執(zhí)行時比較的兩個參數(shù)就變成了下面的樣子:
這也是亂碼產(chǎn)生的原因,, 原因即解碼時采用的encoding與編碼時用的encoding不一致所造成的,。在Web應用中,亂碼的成因和上述分析是一致的,。 我們向應用服務器發(fā)送一個這樣的請求:
服務器用request.getParameter('abc')來獲取,,這個時候有亂碼問題嗎? 答案依然是It depends. 這次的看情況是要看哪些情況呢,?有以下這些。
Tomcat默認對于不同的通道(Connector),,都可以獨立設置相關的編碼屬性,,例如默認是下面這樣的配置: Connector port='8080' protocol='HTTP/1.1' connectionTimeout='20000' redirectPort='8443' /> 而我們可以增加自定義的編碼配置: Connector port='8080' protocol='HTTP/1.1' connectionTimeout='20000' redirectPort='8443' URIEncoding='UTF-8'/> 紅色的URIEncoding即為添加的屬性,這個參數(shù)是和Tomcat的版本有關系的,。 在Tomcat8中,,其對應的官方文檔是這樣說明的:
也就是不設置 -Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=true 那默認的編碼會采用UTF-8。 但是在Tomcat的8.0之前版本官方文檔里是這樣寫的說明:
也就是不特殊指定,,默認將使用ISO-8859-1進行編碼,。 看Connector的構造方法中,也是明確按此進行配置的 public Connector(String protocol) { setProtocol(protocol); ... //注意下面的代碼 if (!Globals.STRICT_SERVLET_COMPLIANCE) { URIEncoding = 'UTF-8'; URIEncodingLower = URIEncoding.toLowerCase(Locale.ENGLISH); } } 這個配置又是如何作用于參數(shù)解析的呢,?看下面 public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { // Set query string encoding req.getParameters().setQueryStringEncoding (connector.getURIEncoding()); //注意這里,,具體去設置的是Parameters類的queryStringEncoding,這個屬性會在后面解析URL中包含的參數(shù)時用到。} public void setQueryStringEncoding( String s ) { queryStringEncoding=s;} 而具體參數(shù)處理時,,傳進去的就是這個queryStringEncoding processParameters( decodedQuery, queryStringEncoding ); 這種情況,,在Tomcat8中就不需要再顯示的配置URIEncoding了,而之前的版本則需要配置,。有上面的代碼參照,,我們看到,,對于URL中傳入的參數(shù),除了設置URIEncoding這個配置之外,,是沒有辦法保證的,。因為其解析參數(shù)時使用的是queryStringEncoding這個參數(shù),因此只有才保證傳到Tomcat的參數(shù)編碼和解碼正確了,。 而如果配置了統(tǒng)一的編碼過濾器,,則過濾器內(nèi)設置request的編碼一定要在解析參數(shù)前,即調(diào)用getParameter前設置,,否則并不生效,。這是因為parameter只會解析一次,之后就放到一個List中直接根據(jù)key返回了,。 那是不是設置一個統(tǒng)一的編碼Filter,,一切就萬事大吉了呢? 答案還是看情況吧,? 恭喜,,你會搶答啦! 我們都知道在jsp中,,可以設置這樣一個jsp頭 <%@ page contentType='text/html;charset=iso-8859-1' language='java' %> 那這個時候如果你的頁面中要輸出一些返回的中文數(shù)據(jù),,這個時候,頁面妥妥的出現(xiàn)了亂碼,。原因自然是iso-859-1不支持中文有關,。注意這里charset不寫依然是按iso-8859-1為默認值。 這時,,你想到了Filter,。在Filter中你大膽的設置了 resp.setCharacterEncoding(encoding); 這個時候,頁面展示卻依然華麗的亂碼了,。擦,,這是啥原因? 那這個時候,,在jsp中顯示數(shù)據(jù)的時候,,依然還是會出現(xiàn)亂碼的,此時注意觀察下響應頭: 看下面的代碼,,由于response在輸出的時候,。會獲取設置的encoding /** * Return the writer associated with this Response. * * @exception IllegalStateException if getOutputStream has* already been called for this response * @exception IOException if an input/output error occurs */ @Override public PrintWriter getWriter() throws IOException { if (usingOutputStream) { throw new IllegalStateException (sm.getString('coyoteResponse.getWriter.ise')); } if (ENFORCE_ENCODING_IN_GET_WRITER) { /* * If the response's character encoding has not been specified as * described in getCharacterEncoding (i.e., the method* just returns the default value ISO-8859-1 ),* getWriter updates it to ISO-8859-1 * (with the effect that a subsequent call to getContentType() will * include a charset=ISO-8859-1 component which will also be * reflected in the Content-Type response header, thereby satisfying * the Servlet spec requirement that containers must communicate the * character encoding used for the servlet response's writer to the * client). */ setCharacterEncoding(getCharacterEncoding()); } usingWriter = true; outputBuffer.checkConverter(); if (writer == null) { writer = new CoyoteWriter(outputBuffer); } return writer; } 而這個encoding是什么設置的呢? public void setContentType(String type) { if (isCommitted()) { return; } if (SecurityUtil.isPackageProtectionEnabled()){ AccessController.doPrivileged(new SetContentTypePrivilegedAction(type)); } else { response.setContentType(type); //這里在設置contentType,由于配置中同時包含charset } } String[] m = MEDIA_TYPE_CACHE.parse(type); //這個是在解析contentType參數(shù) if (m == null) { // Invalid - Assume no charset and just pass through whatever // the user provided. coyoteResponse.setContentTypeNoCharset(type); return; } coyoteResponse.setContentTypeNoCharset(m[0]); if (m[1] != null) { // Ignore charset if getWriter() has already been called if (!usingWriter) { coyoteResponse.setCharacterEncoding(m[1]); //這里就用解析出來的參數(shù)設置,。 isCharacterEncodingSet = true; } } 而我們一般為了處理這種亂碼問題統(tǒng)一寫的filter,,在請求處理前就先把request和response的encoding都設置好。 而這里默認提供的charset為ISO-8859-1,就出了亂碼問題了,。另外一個在處理JSP時容易出的問題,,就是contentType中指定的charset和filter中已經(jīng)設置的encoding,,兩者不一致,比如你的jsp中忘記改了,,使用的是默認的ISO-8859-1.此時,,先通過filter設置的encoding會先于contentType的設置執(zhí)行,因此,,依然會出現(xiàn)亂碼問題,。 總結 在本文中,深入分析了亂碼背后產(chǎn)生的原因:編碼和解碼時采用的encoding不一致,。而解決問題的最樸素的道理就是保持多種數(shù)據(jù)來源編碼的一致性,,無論是數(shù)據(jù)庫的,文件的,,還是輸入輸出的,,都采用一致的編碼,可以簡少很多問題,。另外,,許多文件中有一些默認編碼,開發(fā)中可能不太注意,,此處也是容易出現(xiàn)問題的地方,。 |
|