使用 Tomcat 開發一個 Java Web 項目的時候,相信大多數人都遇到過url出現中文亂碼的狀況,絕大多數人爲了不出現這種問題,因此設計 url 通常都會盡可能設計成都是英文字符。但總避免一種狀況就是當你的系統中擁有搜索功能時,你沒法預料到用戶輸入的是中文仍是其餘符號,此時仍是會存在中文亂碼的問題,那麼爲何會產生中文亂碼問題,下面給你們詳細解析。html
URL 叫統一資源定位符,也能夠說成咱們平時在地址欄輸入的路徑。經過這個url(路徑)咱們能夠發送請求給服務器,服務器尋找具體的服務器組件而後再向用戶提供服務。java
url 編碼簡單來講就是對 url 的字符 按照必定的編碼規則進行轉換。apache
人類的語言太多,不可能用一個通用的格式去表示這麼多的字符,因此則須要編碼,按照不一樣的規則來表示不一樣的字符。
瀏覽器
那麼如今進入正題
GET 請求 和 POST請求是如何進行url編碼的
對於 GET 請求,咱們都知道請求參數是直接跟在url後面,當 url 組裝好以後瀏覽器會對其進行 encode 操做。此過程主要是對 url 中一些特殊字符進行編碼以轉換成 能夠用 多個 ASCII 碼字符表示。具體會以什麼樣的編碼格式是由瀏覽器決定的(具體的規則能夠參見 http://www.ruanyifeng.com/blog/2010/02/url_encoding.html )
進行URL encode以後,瀏覽器就會以iso-8859-1的編碼方式轉換爲二進制隨着請求頭一塊兒發送出去。
服務器
當請求發送到服務器以後,Tomcat 接收到這個請求,會對請求進行解析。具體的解析過程就不在這裏詳解,能夠去參看一下 Tomcat 的源碼,但在使用請求參數有中文時,我相信確定不少人都會出現 404 的狀況app
下面將分別以Tomcat七、Tomcat8兩種版原本說明這其中出現404的緣由jsp
第一種狀況:URL 含有中文,出現404ide
當前測試的 Servletpost
直接訪問的結果測試
從測試圖能夠看出當 URL 含有中文時,直接在瀏覽器訪問會出現 404,瀏覽器已經正確的發出了 HTTP 請求,因此這能夠排除是瀏覽器的問題,那麼問題應該是出如今服務器端,那麼這個問題就應該從 Tomcat 如何解析請求着手查起。
Tomcat 解析請求時經過調用 AbstractInputBuffer.parseRequestLine 方法,這是一個抽象類,通常都將會委託org.apache.coyote.http11.InternalInputBuffer 子類來執行,那麼我如今來看看 parseRequestLine 方法是如何執行的
public boolean parseRequestLine(boolean useAvailableDataOnly) throws IOException { //前面省略,主要都是經過流的讀取字節的操做解析請求的內容 // // Reading the URI,這段代碼主要是從流中讀取 URL 的字節到buf中,再將buf的字節set進請求中 // boolean eol = false; while (!space) { // Read new bytes if needed if (pos >= lastValid) { if (!fill()) throw new EOFException(sm.getString("iib.eof.error")); } // Spec says single SP but it also says be tolerant of HT if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) { space = true; end = pos; } else if ((buf[pos] == Constants.CR) || (buf[pos] == Constants.LF)) { // HTTP/0.9 style request eol = true; space = true; end = pos; } else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) { questionPos = pos; } pos++; } request.unparsedURI().setBytes(buf, start, end - start); if (questionPos >= 0) { request.queryString().setBytes(buf, questionPos + 1, end - questionPos - 1); request.requestURI().setBytes(buf, start, questionPos - start); } else { request.requestURI().setBytes(buf, start, end - start); } //後面同樣省略,都是對請求流中的內容讀取字節出來,set到請求對應的內容塊 return true; }
由於請求有不少內容,這個方法只是按照內容塊將對應的字節 set 進請求,接下來 Tomcat 會基於請求來進一步解析,下一步是調用 AbstractProcessor.prepareRequest 方法,該方法主要是檢查請求的內容是否合法,若都合法,則會將 request、response委託給 adapter 去調用service方法
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { //省略代碼 //service會調用該方法去解析請求,並對url進行解碼 boolean postParseSuccess = postParseRequest(req, request, res, response); //後面省略 }
protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res, Response response) throws Exception { //省略 // Copy the raw URI to the decodedURI,解碼從這裏開始 // 這一步只是將未解碼的 URL 字節複製給 decodedURL MessageBytes decodedURI = req.decodedURI(); decodedURI.duplicate(req.requestURI()); // Parse the path parameters. This will: // - strip out the path parameters // - convert the decodedURI to bytes parsePathParameters(req, request); // 這一步是將 URL 中含有%的16進制數據合併 // URI decoding // %xx decoding of the URL try { req.getURLDecoder().convert(decodedURI, false); } catch (IOException ioe) { res.setStatus(400); res.setMessage("Invalid URI: " + ioe.getMessage()); connector.getService().getContainer().logAccess( request, response, 0, true); return false; } // 真正對 URL 解碼操做在這一步 convertURI(decodedURI, request);
protected void convertURI(MessageBytes uri, Request request) throws Exception { ByteChunk bc = uri.getByteChunk(); int length = bc.getLength(); CharChunk cc = uri.getCharChunk(); cc.allocate(length, -1); // 這一步是獲取解碼使用編碼格式,從這裏能夠看出編碼格式與 connector 有關 // 在默認狀況下,若是沒有配置Encoding,則爲 null String enc = connector.getURIEncoding(); if (enc != null) { //根據編碼格式來對 URL 進行解碼 } // 因此當咱們沒有配置時,會直接跳下去執行,以 ISO-8859-1的編碼格式來解碼 URL // Default encoding: fast conversion for ISO-8859-1 byte[] bbuf = bc.getBuffer(); char[] cbuf = cc.getBuffer(); int start = bc.getStart(); for (int i = 0; i < length; i++) { cbuf[i] = (char) (bbuf[i + start] & 0xff); } uri.setChars(cbuf, 0, length); }
在Tomcat 7 裏面,沒有配置 connector 的編碼,它會默認使用 ISO-8859-1 的編碼格式來解碼,因此該 URL 最後解碼的結果是
能夠看出解碼後的 URL 出現了中文亂碼,因此最後由於沒有匹配到對應的 Servlet ,因此出現404
那麼當咱們在 Tomcat 的配置文件配置編碼格式以後,再使用一樣的 URL 去訪問,這時就能成功訪問了
URL 解碼結果
測試結果
問題來了
當咱們使用 Tomcat 8的時候,無論咱們是否有設置 connector 的編碼,當咱們使用含有中文 URL 去訪問資源,均會出現404的狀況
注:Tomcat 8的默認編碼是 UTF-8,而Tomcat 7 的默認編碼是ISO-8859-1
那麼既然Tomcat 8是以 UTF-8 進行解碼的,因此 URL 可以正確解碼成功,不會出現 URL 亂碼,那麼問題是出如今哪裏呢?
咱們知道請求最終會委託給一個請求包裝對象,若是找不到,那麼就會訪問失敗,因此如今從這裏請求映射開始着手找緣由。
Tomcat 匹配請求的 Mapper 有多種策略,通常是使用全名匹配
private final void internalMapExactWrapper (Wrapper[] wrappers, CharChunk path, MappingData mappingData) { Wrapper wrapper = exactFind(wrappers, path); if (wrapper != null) { mappingData.requestPath.setString(wrapper.name); mappingData.wrapper = wrapper.object; if (path.equals("/")) { // Special handling for Context Root mapped servlet mappingData.pathInfo.setString("/"); mappingData.wrapperPath.setString(""); // This seems wrong but it is what the spec says... mappingData.contextPath.setString(""); } else { mappingData.wrapperPath.setString(wrapper.name); } } }
在 Tomcat 7 下 wrappers 對象集的內存快照
能夠看到 wrappers 對象存在咱們要訪問的資源,因此使用Tomcat 7 咱們能夠最終訪問到目標資源
在 Tomcat 8 下,wrapper 對象的內存快照
能夠看到Mapper 對象的 name 出現亂碼
因此之因此會形成這種緣由是由於不一樣版本的 Tomcat 在生成 Servlet 對應的 Mapper對象時,解析路徑使用的編碼格式不一樣,具體編碼能夠去查看 Tomcat 如何解析 Servlet。
最後總結:
開發 Java Web 項目的時候,儘可能避免設計含有中文字符的 URL,而且統一開發環境,好比Tomcat 版本。由於可能有些bug或問題出現緣由是源於版本的不一樣,與本身的源程序邏輯無關,一旦出現這種問題,要找出問題的緣由是須要花費不少時間的。
在 Web 開發中,咱們一般會有許多帶有請求參數的請求,通常來講咱們須要調用 request.setCharacterEncoding(「utf-8」); 方法來設置解析參數的編碼,可是通常狀況下,該方法只對於 Post請求有用,而對於 Get 請求獲取參數仍然會出現亂碼。
測試的 Servelt
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); String name = request.getParameter("name"); System.out.println(name); request.getRequestDispatcher("Test.jsp").forward(request, response); }
測試結果
能夠看到即便設置了編碼,可是請求參數仍然是亂碼。
那麼 Tomcat 是如何解析請求參數的呢?
Tomcat 源碼以下
protected void parseParameters(){ //以上代碼省略 //獲取咱們設置的編碼 String enc = getCharacterEncoding(); boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); if (enc != null) { parameters.setEncoding(enc); if (useBodyEncodingForURI) { parameters.setQueryStringEncoding(enc); } } else { parameters.setEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING); if (useBodyEncodingForURI) { parameters.setQueryStringEncoding (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING); } } parameters.handleQueryParameters(); }
public void handleQueryParameters() { if( didQueryParameters ) { return; } didQueryParameters=true; if( queryMB==null || queryMB.isNull() ) { return; } if(log.isDebugEnabled()) { log.debug("Decoding query " + decodedQuery + " " + queryStringEncoding); } try { decodedQuery.duplicate( queryMB ); } catch (IOException e) { // Can't happen, as decodedQuery can't overflow e.printStackTrace(); } // 解析 get 請求的參數是經過 parameter裏面的 queryStringEncoding 來解碼的 processParameters( decodedQuery, queryStringEncoding ); }
從源碼能夠看出 Tomcat 經過 String enc = getCharacterEncoding(); 來獲取咱們設置的編碼,當前設置爲 utf-8,可是當useBodyEncodingForURI 爲 false 時,它只會講 enc 的值賦值給 encoding 而不會賦值給 queryStringEncoding。
在解析參數時,對於 Post 請求,Tomcat 使用 encoding 來解碼;對於 get 請求,Tomcat 使用 queryStringEncoding 來解析參數,由於此時 useBodyEncodingForURI 爲 false 時,Tomcat 使用默認編碼來解析,Tomcat 7的默認編碼是 ISO-8859-1,因此解析以後參數出現亂碼;Tomcat 8 默認編碼是 UTF-8,所以解析不會出現亂碼。
對於使用 Tomcat 7 出現請求參數亂碼的解決方法:
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="UTF-8" useBodyEncodingForURI="true"/>
public class EncodingRequest extends HttpServletRequestWrapper { private HttpServletRequest request; private boolean hasEncode = false; public EncodingRequest(HttpServletRequest request) { super(request); this.request = request; } @Override public String getParameter(String name) { String[] values = getParameterValues(name); if (values == null) { return null; } return values[0]; } @Override public String[] getParameterValues(String name) { Map<String, String[]> parameterMap = getParameterMap(); String[] values = parameterMap.get(name); return values; } @Override public Map getParameterMap() { Map<String, String[]> parameterMap = request.getParameterMap(); String method = request.getMethod(); if (method.equalsIgnoreCase("post")) { return parameterMap; } if (!hasEncode) { Set<String> keys = parameterMap.keySet(); for (String key : keys) { String[] values = parameterMap.get(key); if (values == null) { continue; } for (int i = 0; i < values.length; i++) { String value = values[i]; try { value = new String(value.getBytes("ISO-8859-1"), "utf-8"); values[i] = value; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } hasEncode = true; } } return parameterMap; } }