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

URL編碼問題

問題描述

  使用 Tomcat 開發一個 Java Web 項目的時候,相信大多數人都遇到過url出現中文亂碼的狀況,絕大多數人爲了不出現這種問題,因此設計 url 通常都會盡可能設計成都是英文字符。但總避免一種狀況就是當你的系統中擁有搜索功能時,你沒法預料到用戶輸入的是中文仍是其餘符號,此時仍是會存在中文亂碼的問題,那麼爲何會產生中文亂碼問題,下面給你們詳細解析。html

什麼是 URL

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

什麼是 URL 編碼

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

爲何須要 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 的狀況app

下面將分別以Tomcat七、Tomcat8兩種版原本說明這其中出現404的緣由jsp

 

關於URL含有中文致使404

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

當前測試的 Servletpost

 

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;
}
}

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

相關文章
相關標籤/搜索