关于使用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;
    }
}
相关文章
相关标签/搜索