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

直接访问的结果

  从测试图可以看出当 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 有多种策略,一般是使用全名匹配

  • 全名匹配:根据请求的全路径来设置对应 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 对象集的内存快照


可以看到 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 出现请求参数乱码的解决方法:

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

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

时间: 2024-12-16 15:58:47

关于使用Tomcat搭建的Web项目,出现 URL 中文乱码的问题解析的相关文章

执行web项目控制台输出中文乱码

idea中文乱码问题有很多种,我这里之说web项目,就是运行在tomcat下,中文乱码问题,大家可以先检查下这里 idea->>左上角new->>setting->>在搜索框中搜索encoding->>在红框中都选择utf-8,一般都默认的utf-8 下边进入重点: 部署完tomcat之后,选择Edit Configuration.. 在配置项VM options文本框中输入-Dfile.encoding=UTF-8,点击Apply或OK即可. 运行项目:

带领技术小白入门——基于java的微信公众号开发(包括服务器配置、java web项目搭建、tomcat手动发布web项目、微信开发所需的url和token验证)

微信公众号对于每个人来说都不陌生,但是许多人都不清楚是怎么开发的.身为技术小白的我,在闲暇之余研究了一下基于java的微信公众号开发.下面就是我的实现步骤,写的略显粗糙,希望大家多多提议! 一.申请服务器 1.我购买的是阿里云服务器,购买后要设置一下服务器密码,默认用户名是administrator,购买好后如下: 2.申请好后,copy一下此服务器的IP地址(公有),在本地ping一下看看是否可用,j键盘Win+R,输入cmd,输入ping+IP回车,如下即为成功: 二.配置服务器 1.下载远

使用MyEclipse搭建java Web项目开发

转自:http://blog.csdn.net/jiuqiyuliang/article/details/36875217 首先,在开始搭建MyEclipse的开发环境之前,还有三步工具的安装需要完成,只要在安装配置成功之后才可以进入下面的java Web项目开发环境的搭建. 1.安装工具 第一步,下载并安装JDK,到官网上下载安装即可,之后需要细心的配置环境变量,我给大家推荐百度文库的一篇文章,猛戳这里. 第二步,下载Tomcat,当然可以去Apache Tomcat的官网,同样,您可以移驾到

在Eclipse中使用Struts和Hibernate框架搭建Maven Web项目

前言 学习使用Java还是2012年的事情,刚开始学习的Java的时候,使用的是MyEclipse工具和SSH框架.初学者适合使用MyEclipse,因为他将struts.Spring和Hibernate等框架集成在IDE中,可以直接通过添加Capacities将这些框架集成到Web Project中,相应的Jar包也会自动添加到lib目录下,开发过程中还是需要手动添加一些jar包.再后来开始接触Maven,在Eclipse下搭建Maven Web项目.在后来就转C#项目开发了,项目做了快两年的

【java项目实战】一步步教你使用MyEclipse搭建java Web项目开发环境(一)

首先,在开始搭建MyEclipse的开发环境之前,还有三步工具的安装需要完成,只要在安装配置成功之后才可以进入下面的java Web项目开发环境的搭建. 1.安装工具 第一步,下载并安装JDK,到官网上下载安装即可,之后需要细心的配置环境变量,我给大家推荐百度文库的一篇文章,猛戳这里. 第二步,下载Tomcat,当然可以去Apache Tomcat的官网,同样,您可以移驾到我的资源下载,外送API文档(免资源分). 第三步,下载MyEclipse,MyEclipse官网,傻瓜式安装即可. ===

Jetty和Tomcat运行Maven Web项目几种方法

目录 1.简介 2.采用Jetty和Tomcat运行Maven Web项目 1.简介 上一篇我们介绍了用Eclipse创建了Maven Web项目,接下来我们介绍怎么运行Maven Web项目方便我们开发和调试,这里使用的是Maven插件的形式引用进来的,这样耦合性比较低,我们需要什么样的工具就引入对应的插件就可以用.  2.采用Jetty和Tomcat运行Maven Web项目      1)采用Jetty运行Maven Web项目 第一步:安装Jetty插件到Eclipse上,首先Eclip

keepalived+nginx+tomcat搭建高性能web服务器集群

使用keepalived+nginx+tomcat搭建高性能web服务器集群,系统采用centos6.9,前端用nginx做反向代理实现负载均衡,同时结合keepalived对nginx实现高可用,后端使用两台tomcat做动态jsp解析,实现了动静分离. 搭建环境 准备四台服务器 vip: 192.168.75.130master: 192.168.75.131 (安装nginx做反向代理实现负载匀衡,结合keepalived实现高可用)backup: 192.168.75.132 (同上)w

eclipse的tomcat运行mave web项目

1. 安装Maven(配置Maven环境变量+Eclipse中的Maven插件) 这步就不多说了,网上一搜一大把,要说明的只有一点,第一次配好Maven插件,一定要更新一次远程库的索引,这样添加依赖的时候就不会出现搜不到那个jar包的情况了(当然要先启用远程库的index,不然也搜不到jar包的) 2. 新建Maven项目.配置 新建了Maven项目后,属性->Project Facets->"convert to faceted form...",在随后出现的Projec

在linux下用tomcat部署java web项目的过程与注意事项

在linux下用tomcat部署java web项目的过程与注意事项(原创) 一.安装JDK到http://www.oracle.com/technetwork/java/javase/downloads/index.html下载Java SE 6 Update 27根据操作系统选择Linux x64 - RPM Installer或Linux x86 - RPM Installer下载jdk-6u27-linux-x64-rpm.bin后放在/home目录中,当然其它地方也行.注意:LINUX