使用 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;
}
}
本文只是我的的測試的結果,若有錯誤,請提出,互相交流。