什么是 URL

URL 叫统一资源定位符,也可以说成我们平时在地址栏输入的路径。
通过这个url(路径)我们可以发送要求给做事器,做事器探求详细的做事器组件然后再向用户供应做事。

什么是 URL 编码

url 编码大略来说便是对 url 的字符 按照一定的编码规则进行转换。

为什么须要 URL 编码

人类的措辞太多,不可能用一个通用的格式去表示这么多的字符,以是则须要编码,按照不同的规则来表示不同的字符。

jspurl传递中文乱码关于应用Tomcat搭建的Web项目涌现 URL 中文乱码的问题解析 Docker

那么现在进入正题

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

直接访问的结果

从测试图可以看出当 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 neededif (pos >= lastValid) {if (!fill)throw new EOFException(sm.getString(\"大众iib.eof.error\"大众));}// Spec says single SP but it also says be tolerant of HTif (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 requesteol = 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 字节复制给 decodedURLMessageBytes decodedURI = req.decodedURI;decodedURI.duplicate(req.requestURI);// Parse the path parameters. This will:// - strip out the path parameters// - convert the decodedURI to bytesparsePathParameters(req, request);// 这一步是将 URL 中含有%的16进制数据合并// URI decoding// %xx decoding of the URLtry {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,则为 nullString enc = connector.getURIEncoding;if (enc != null) {//根据编码格式来对 URL 进行解码}// 以是当我们没有配置时,会直接跳下去实行,以 ISO-8859-1的编码格式来解码 URL// Default encoding: fast conversion for ISO-8859-1byte 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 有多种策略,一样平常是利用全名匹配

全名匹配:根据要求的全路径来设置对应 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 servletmappingData.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 overflowe.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 涌现要求参数乱码的办理方法:

在 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\"大众/>创建一个要求包装工具,重写要求的获取参数方法,并通过过滤器将要求委托给包装工具,详细代码如下:

public class EncodingRequest extends HttpServletRequestWrapper {private HttpServletRequest request;private boolean hasEncode = false;public EncodingRequest(HttpServletRequest request) {super(request);this.request = request;}@Overridepublic String getParameter(String name) {String values = getParameterValues(name);if (values == null) {return null;}return values[0];}@Overridepublic String getParameterValues(String name) {Map<String, String> parameterMap = getParameterMap;String values = parameterMap.get(name);return values;}@Overridepublic 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;}}

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