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