关于使用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;
}
}

本文只是个人的测试的结果,如有错误,请提出,互相交流。

posted @ 2016-07-16 21:25  小小小虫  阅读(14014)  评论(0编辑  收藏  举报