早起的國內(nèi)互聯(lián)網(wǎng)都使用GBK編碼,久之,所有項(xiàng)目都以GBK來編碼了。對于J2EE項(xiàng)目,為了減少編碼的干擾通常都是設(shè)置一個(gè)編碼的Filter,強(qiáng)制將Request/Response編碼改為GBK。例如一個(gè)Spring的常見配置如下:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>GBK</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter>
毫無疑問,這在GBK編碼的頁面訪問、提交數(shù)據(jù)時(shí)是沒有亂碼問題的。但是遇到Ajax就不一樣了。Ajax強(qiáng)制將中文內(nèi)容進(jìn)行UTF-8編碼,這樣導(dǎo)致進(jìn)入后端后使用GBK進(jìn)行解碼時(shí)發(fā)生亂碼。網(wǎng)上的所謂的終極解決方案都是扯淡或者過于復(fù)雜,例如下面的文章:
- GBK編碼下jQuery Ajax中文亂碼終極暴力解決方案
- 關(guān)于AJAX-gbk(gb2312)中文亂碼的解決方案
- Ajax 中XmlHttp 亂碼 的解決方法 (UTF8,GB2312 編碼 解碼)
這樣的文章很多,顯然:
- 使用VB進(jìn)行UTF-8轉(zhuǎn)換成GBK提交是完全不可行(多瀏覽器、多平臺完全不可用)
- 使用復(fù)雜的js函數(shù)進(jìn)行一次、多次編碼,后端進(jìn)行一次、多次解碼也是不靠譜的,成本太高,無法重復(fù)使用
如果提交數(shù)據(jù)的時(shí)候能夠告訴后端傳輸?shù)木幋a信息是否就可以避免這種問題?比如Ajax請求告訴后端是UTF-8,其它請求告訴后端是GBK,這樣后端分別根據(jù)指定的編碼進(jìn)行解碼是不是就解決問題了。
有兩個(gè)問題:
- 如何通過Ajax告訴后端的編碼?Header過于復(fù)雜,Cookie成本太高,使用參數(shù)最方便。
- 后端何時(shí)進(jìn)行解碼?每一個(gè)請求進(jìn)行解碼,過于繁瑣;獲取參數(shù)時(shí)解碼,此時(shí)已經(jīng)亂碼;在Filter里面動(dòng)態(tài)設(shè)置編碼是最完善的方案。
- 如何從參數(shù)中獲取編碼?如果是POST的body顯然無法獲取,因此在獲取之前所有參數(shù)就已經(jīng)按照某種編碼解碼過了,無法還原。所以通過URL傳遞編碼最有效。支持GET/POST,同時(shí)成本很低。
解決了上述問題,來看具體實(shí)現(xiàn)方案。 列一段Java代碼:
import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.util.ClassUtils; import org.springframework.web.filter.OncePerRequestFilter; /** 自定義編碼過濾器 * @author imxylz (imxylz#gmail.com) * @sine 2011-6-9 */ public class MutilCharacterEncodingFilter extends OncePerRequestFilter { static final Pattern inputPattern = Pattern.compile(".*_input_encode=([\\w-]+).*"); static final Pattern outputPattern = Pattern.compile(".*_output_encode=([\\w-]+).*"); // Determine whether the Servlet 2.4 HttpServletResponse.setCharacterEncoding(String) // method is available, for use in the "doFilterInternal" implementation. private final static boolean responseSetCharacterEncodingAvailable = ClassUtils.hasMethod(HttpServletResponse.class, "setCharacterEncoding", new Class[] { String.class }); private String encoding; private boolean forceEncoding = false; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String url = request.getQueryString(); Matcher m = null; if (url != null && (m = inputPattern.matcher(url)).matches()) {//輸入編碼 String inputEncoding = m.group(1); request.setCharacterEncoding(inputEncoding); m = outputPattern.matcher(url); if (m.matches()) {//輸出編碼 response.setCharacterEncoding(m.group(1)); } else { if (this.forceEncoding && responseSetCharacterEncodingAvailable) { response.setCharacterEncoding(this.encoding); } } } else { if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) { request.setCharacterEncoding(this.encoding); if (this.forceEncoding && responseSetCharacterEncodingAvailable) { response.setCharacterEncoding(this.encoding); } } } filterChain.doFilter(request, response); } public void setEncoding(String encoding) { this.encoding = encoding; } public void setForceEncoding(boolean forceEncoding) { this.forceEncoding = forceEncoding; } }
解釋下:
- 如果URL的QueryString中包含_input_encode就使用此編碼進(jìn)行設(shè)置Request編碼,以后參數(shù)按照此編碼進(jìn)行解析,例如如果是Ajax就傳入U(xiǎn)TF-8,如果是普通的GBK請求則無視此參數(shù)。
- 如果無視此參數(shù),則按照web.xml中配置的編碼規(guī)則進(jìn)行反編碼,如果是GBK就按照GBK規(guī)則解析。
- 對于輸出編碼同樣使用上述規(guī)則。需要輸出編碼依賴輸入編碼,也就是說如果有一個(gè)_output_encode的輸出編碼,則同時(shí)需要有一個(gè)_input_encode編碼來指定輸入編碼。當(dāng)然你可以改造成不依賴輸入編碼。
- 完全兼容Spring的org.springframework.web.filter.CharacterEncodingFilter編碼規(guī)則,只需要替換類即可。
- 沒有繼承org.springframework.web.filter.CharacterEncodingFilter類的原因是,org.springframework.web.filter.CharacterEncodingFilter里面的encoding參數(shù)和forceEncoding參數(shù)是private,子類無法使用。在有_input_encode而無_output_encode的時(shí)候想依然保持Spring的Response解析規(guī)則的話無法做到,所以將里面的代碼拷貝過來使用。(為了展示方便,注釋都刪掉了)
- 正則表達(dá)式可以改進(jìn)成只需要匹配一次,從而可以提高一點(diǎn)點(diǎn)效率。
- 所有后端請求將無視編碼的存在,前端Ajax的GET/POST請求也將無視編碼的存在,只是在URL地址中加上一個(gè)_input_encode=UTF-8的參數(shù)。僅此而已。
- 如果想輸出的編碼也是UTF-8,比如手機(jī)端請求、跨站請求等,則需要URL地址參數(shù)_input_encode=UTF-8&_output_encode=UTF-8。
- 對于POST請求,編碼參數(shù)不能寫在body里面,否則無法解析。
- 顯然,這種終極解決方案,在任何編碼中都可以解決,GBK/UTF-8/ISO8859-1等編碼任何組合都可以實(shí)現(xiàn)。
- 唯一局限性是,此解決方案限制在J2EE項(xiàng)目中,其它平臺不知是否有類似Filter這樣的組件能夠設(shè)置編碼的概念。