關於使用Tomcat搭建的Web項目,出現 URL 中文亂碼的問題解析


URL編碼問題

問題描述

  使用 Tomcat 開發一個 Java Web 項目的時候,相信大多數人都遇到過url出現中文亂碼的情況,絕大多數人為了避免出現這種問題,所以設計 url 一般都會盡量設計成都是英文字符。但總避免一種情況就是當你的系統中擁有搜索功能時,你無法預料到用戶輸入的是中文還是其他符號,此時還是會存在中文亂碼的問題,那么為什么會產生中文亂碼問題,下面給大家詳細解析。

什么是 URL

URL 叫統一資源定位符,也可以說成我們平時在地址欄輸入的路徑。通過這個url(路徑)我們可以發送請求給服務器,服務器尋找具體的服務器組件然后再向用戶提供服務。

什么是 URL 編碼

url 編碼簡單來說就是對 url 的字符 按照一定的編碼規則進行轉換。

為什么需要 URL 編碼

人類的語言太多,不可能用一個通用的格式去表示這么多的字符,所以則需要編碼,按照不同的規則來表示不同的字符。 

那么現在進入正題 
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 的情況

下面將分別以Tomcat7、Tomcat8兩種版本來說明這其中出現404的原因

 

關於URL含有中文導致404

第一種情況:URL 含有中文,出現404

當前測試的 Servlet

 

Alt text

直接訪問的結果

 

Alt text

  從測試圖可以看出當 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 最后解碼的結果是

 

 

Alt text
可以看出解碼后的 URL 出現了中文亂碼,所以最后因為沒有匹配到對應的 Servlet ,所以出現404

那么當我們在 Tomcat 的配置文件配置編碼格式之后,再使用同樣的 URL 去訪問,這時就能成功訪問了

Alt text 

 


URL 解碼結果

 

Alt text

測試結果

 

Alt text

問題來了 
當我們使用 Tomcat 8的時候,不管我們是否有設置 connector 的編碼,當我們使用含有中文 URL 去訪問資源,均會出現404的情況 
注:Tomcat 8的默認編碼是 UTF-8,而Tomcat 7 的默認編碼是ISO-8859-1 
那么既然Tomcat 8是以 UTF-8 進行解碼的,所以 URL 能夠正確解碼成功,不會出現 URL 亂碼,那么問題是出現在哪里呢? 
我們知道請求最終會委托給一個請求包裝對象,如果找不到,那么就會訪問失敗,所以現在從這里請求映射開始着手找原因。

Tomcat 匹配請求的 Mapper 有多種策略,一般是使用全名匹配

  • 全名匹配:根據請求的全路徑來設置對應 wrappers 對象 
    匹配方法如下
 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 對象集的內存快照

 

 

 

Alt text
可以看到 wrappers 對象存在我們要訪問的資源,所以使用Tomcat 7 我們可以最終訪問到目標資源

在 Tomcat 8 下,wrapper 對象的內存快照

 

Alt text 
可以看到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);
}

測試結果

 

Alt text

可以看到即使設置了編碼,但是請求參數仍然是亂碼。

那么 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 出現請求參數亂碼的解決方法:

  1. 在 Tomcat 的 server,xml 的配置文件中,對於 connector 的配置中,加上如下的配置,那么對於 get 請求,也能夠通過request.setCharacterEncoding(“utf-8”); 來設定編碼格式
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"
URIEncoding="UTF-8" useBodyEncodingForURI="true"/>

 

  1. 創建一個請求包裝對象,重寫請求的獲取參數方法,並通過過濾器將請求委托給包裝對象,具體代碼如下:
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;
}
}

本文只是個人的測試的結果,如有錯誤,請提出,互相交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM