师门技术论坛:字符编码与java web application中的乱码

来源:互联网 发布:pnp4nagios nginx 编辑:程序博客网 时间:2024/06/10 06:02

问题起源

       对于计算机而言,它仅认识两个0和1,不管是在内存中还是外部存储设备上,我们所看到的文字、图片、视频等等“数据”在计算机中都是以二进制形式存在的。不同字符对应二进制数的规则,就是字符的编码。字符编码的集合称为字符集。

       在早期的计算机系统中,使用的字符是非常少的,他们只包括26个英文字母、数字符号和一些常用符号,对于这些字符进行编码,用1个字节就足够了,但是随着计算机的不断发展,为了适应全世界其他各国民族的语言,这些少得可怜的字符编码肯定是不够的。于是人们提出了UNICODE编码,它采用双字节编码,兼容英文字符和其他国家民族的双字节字符编码。

       每个国家为了统一编码都会规定该国家/地区计算机信息交换用的字符集编码,为了解决本地字符信息的计算机处理,于是出现了各种本地化版本,引进LANG,Codepage 等概念。现在大部分具有国际化特征的软件核心字符处理都是以 Unicode 为基础的,在软件运行时根据当时的Locale/Lang/Codepage 设置确定相应的本地字符编码设置,并依此处理本地字符。在处理过程中需要实现Unicode 和本地字符集的相互转换。

       同然,java内部采用的就是Unicode编码,所以在java运行的过程中就必然存在从Unicode编码与相应的计算机操作系统或者浏览器支持的编码格式相互转化的过程,这个转换的过程有一系列的步骤,如果某个步骤出现错误,则输出的文字就会是乱码。

       所以产生java乱码的问题就在于JVM与对应的操作系统/浏览器进行编码格式转换时出现了错误。

       其实解决 JAVA 程序中的汉字编码问题的方法往往很简单,但理解其背后的原因,定位问题,还需要了解现有的汉字编码和编码转换。

常见字符编码

       常见的字符编码主要包括:ASCII编码、GB**编码、Unicode。

1.ASCII编码

标准的ASCII编码使用的是7(2^7 = 128)位二进制数来表示所有的大小写字母、数字和标点符号已经一些特殊的控制字符,最前面的一位统一规定为0。其中0~31及127(共33个)是控制字符或通信专用字符,32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字,65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。

2.GBK***编码

GB2312,用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆。

GB18030

GBK,汉字编码标准之一,全称《汉字内码扩展规范》,GBK是GB2312的扩展,它 向下与 GB 2312 编码兼容

3.Unicode编码

Unicode编码的作用就是能够使计算机实现夸平台、跨语言的文本转换和处理。它几乎包含了世界上所有的符号,并且每个符号都是独一无二的。在它的编码世界里,每一个数字代表一个符号,每一个符号代表了一个数字,不存在二义性。

同时Unicode是字符集,它存在很多几种实现方式如:UTF-8、UTF-16.

UTF-8是Unicode的实现方式之一

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。 
UTF-8的编码规则很简单,只有两条: 
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

JavaWeb中编码&解码

       通过下图我们可以了解在javaWeb中有哪些地方有转码:


       用户想服务器发送一个HTTP请求,需要编码的地方有url、cookie、parameter,经过编码后服务器接受HTTP请求,解析HTTP请求,然后对url、cookie、parameter进行解码。在服务器进行业务逻辑处理过程中可能需要读取数据库、本地文件或者网络中的其他文件等等,这些过程都需要进行编码解码。当处理完成后,服务器将数据进行编码后发送给客户端,浏览器经过解码后显示给用户。在这个整个过程中涉及的编码解码的地方较多,其中最容易出现乱码的位置就在于服务器与客户端进行交互的过程。

       上面整个过程可以概括成这样,页面编码数据传递给服务器,服务器对获得的数据进行解码操作,经过一番业务逻辑处理后将最终结果编码处理后传递给客户端,客户端解码展示给用户。所以下面我就请求对javaweb的编码&解码进行阐述。

请求

       客户端想服务器发送请求无非就通过四中情况:

       1、URL方式直接访问。

       2、页面链接。

       3、表单get提交

       4、表单post提交

URL方式

       对于URL,如果该URL中全部都是英文的那倒是没有什么问题,如果有中文就要涉及到编码了。URL的组成部分:


       在这URL中浏览器将会对path和parameter进行编码操作。为了更好地解释编码过程,使用如下URL

       http://127.0.0.1:8080/perbank/我是cm?name=我是cm

       将以上地址输入到浏览器URL输入框中,通过查看http 报文头信息我们可以看到浏览器是如何进行编码的。下面是IE、Firefox、Chrome三个浏览器的编码情况:


       可以看到各大浏览器对我是的编码情况如下:

path部分

Query String

Firefox

E6 88 91 E6 98 AF

E6 88 91 E6 98 AF

Chrome

E6 88 91 E6 98 AF

E6 88 91 E6 98 AF

IE

E6 88 91 E6 98 AF

CE D2 CA C7

       对于path部分Firefox、chrome、IE都是采用UTF-8编码格式,对于Query String部分Firefox、chrome采用UTF-8,IE采用GBK。加上%,这是因为URL的编码规范规定浏览器将ASCII字符非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”。

       当然对于不同的浏览器,相同浏览器不同版本,不同的操作系统等环境都会导致编码结果不同。由于各大浏览器、各个操作系统对URL的URI、QueryString编码都可能存在不同,这样对服务器的解码势必会造成很大的困扰。

tomcat是对URL进行解码操作:

       解析请求的 URL 是在org.apache.coyote.HTTP11.InternalInputBuffer 的parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char是在org.apache.catalina.connector.CoyoteAdapter 的convertURI 方法中完成的:

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

                    String enc = connector.getURIEncoding();     //获取URI解码集

                    if (enc != null) {

                        B2CConverter conv = request.getURIConverter();

                        try {

                            if (conv == null) {

                                conv = new B2CConverter(enc);

                                request.setURIConverter(conv);

                            }

                        } catch (IOException e) {...}

                        if (conv != null) {

                            try {

                                conv.convert(bc, cc, cc.getBuffer().length - cc.getEnd());

                                uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength());

                                return;

                            } catch (IOException e) {...}

                        }

                    }

                    // Default encoding: fast conversion

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

    }

从上面的代码可知,对URI的解码操作是首先获取Connector的解码集,该配置在server.xml中

<Connector URIEncoding="utf-8"  />

如果没有定义则会采用默认编码ISO-8859-1来解析。

       对于QueryString部分,我们知道无论我们是通过get方式还是POST方式提交,所有的参数都是保存在Parameters,然后我们通过request.getParameter,解码工作就是在第一次调用getParameter方法时进行的。在getParameter方法内部它调用org.apache.catalina.connector.Request 的 parseParameters 方法,这个方法将会对传递的参数进行解码。下面代码只是parseParameters方法的一部分:

          //获取编码

             String enc = getCharacterEncoding();

            //获取ContentType 中定义的 Charset

            boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();

            if (enc != null) {    //如果设置编码不为空,则设置编码为enc

                parameters.setEncoding(enc);

                if (useBodyEncodingForURI) {   //如果设置了Chartset,则设置queryString的解码为ChartSet

                    parameters.setQueryStringEncoding(enc);   

                }

            } else {     //设置默认解码方式

                parameters.setEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);

                if (useBodyEncodingForURI) {

                    parameters.setQueryStringEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);

                }

            }

从上面代码可以看出对query String的解码格式要么采用设置的ChartSet要么采用默认的解码格式ISO-8859-1。注意这个设置的ChartSet是在 httpHeader中定义的ContentType,同时如果我们需要改指定属性生效,还需要进行如下配置:

<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>

上面部分详细介绍了URL方式请求的编码解码过程。其实对于我们而言,我们更多的方式是通过表单的形式来提交。

表单GET

       我们知道通过URL方式提交数据是很容易产生乱码问题的,所以我们更加倾向于通过表单形式。当用户点击submit提交表单时,浏览器会根据设定的编码来编码数据传递给服务器。通过GET方式提交的数据都是拼接在URL后面来提交的,所以tomcat服务器在进行解码过程中URIEncoding就起到作用了。tomcat服务器会根据设置的URIEncoding来进行解码,如果没有设置则会使用默认的ISO-8859-1来解码。假如我们在页面将编码设置为UTF-8,而URIEncoding设置的不是或者没有设置,那么服务器进行解码时就会产生乱码。这个时候我们一般可以通过newString(request.getParameter("name").getBytes("iso-8859-1"),"utf-8")的形式来获取正确数据。

表单POST

       对于POST方式,它采用的编码也是由页面来决定的即contentType。当我通过点击页面的submit按钮来提交表单时,浏览器首先会根据contentType的charset编码格式来对POST表单的参数进行编码然后提交给服务器,在服务器端同样也是用contentType中设置的字符集来进行解码(这里与get方式就不同了),这就是通过POST表单提交的参数一般而言都不会出现乱码问题。当然这个字符集编码我们是可以自己设定的:request.setCharacterEncoding(charset)。

JSP页面编码过程

我们知道JSP页面是需要转换为servlet的,在转换过程中肯定是要进行编码的。在JSP转换为servlet过程中下面一段代码起到至关重要的作用。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="GBK" %>

在上面代码中有两个地方存在编码:pageEncoding、contentType的charset。其中pageEncoding是jsp文件本身的编码,而contentType的charset是指服务器发送给客户端时的内容编码。

jsp在转换为Servlet的过程中是需要经过主要的三次编码转换过程(除去数据库编码转换、页面参数输入编码转换):

       第一次:转换为.java文件;

       第二次:转换为.class文件;

       第三次:业务逻辑处理后输出。

       第一阶段

       JVM将JSP编译为.jsp文件。在这个过程中pageEncoding就起到作用了,JVM首先会获取pageEncoding的值,如果该值存在则采用它设定的编码来编译,否则则采用file.encoding编码来编译。

       第二阶段

       JVM将.java文件转换为.class文件。在这个过程就与任何编码的设置都没有关系了,不管JSP采用了什么样的编码格式都将无效。经过这个阶段后.jsp文件就转换成了统一的Unicode格式的.class文件了。

       第三阶段

       后台经过业务逻辑处理后将产生的结果输出到客户端。在这个过程中contentType的charset就发挥了功效。如果设置了charset则浏览器就会使用指定的编码格式进行解码,否则采用默认的ISO-8859-1编码格式进行解码处理。

流程如如下:


解决URL中文乱码问题

我们主要通过两种形式提交向服务器发送请求:URL、表单。而表单形式一般都不会出现乱码问题,乱码问题主要是在URL上面。URL向服务器发送请求编码过程实在是实在太混乱了。不同的操作系统、不同的浏览器、不同的网页字符集,将导致完全不同的编码结果。

要把每一种结果都考虑进去,保证客户端只用一种编码方法向服务器发出请求,主要提供以下几种方法

javascript

       使用javascript编码不给浏览器插手的机会,编码之后再向服务器发送请求,然后在服务器中解码。在掌握该方法的时候,我们需要料及javascript编码的三个方法:escape()、encodeURI()、encodeURIComponent()。

escape

       采用SIOLatin字符集对指定的字符串进行编码。所有非ASCII字符都会被编码为%xx格式的字符串,其中xx表示该字符在字符集中所对应的16进制数字。例如,格式对应的编码为%20。它对应的解码方法为unescape()。


       事实上escape()不能直接用于URL编码,它的真正作用是返回一个字符的Unicode编码值。比如上面“我是cm”的结果为%u6211%u662Fcm,其中“我”对应的编码为6211,“是”的编码为662F,“cm”编码为cm。

       注意,escape()不对"+"编码。但是我们知道,网页在提交表单的时候,如果有空格,则会被转化为+字符。服务器处理数据的时候,会把+号处理成空格。所以,使用的时候要小心。 

encodeURI

       对整个URL进行编码,它采用的是UTF-8格式输出编码后的字符串。不过encodeURI除了ASCII编码外对于一些特殊的字符也不会进行编码如:! @ #$& * ( ) = : / ; ? + '。


encodeURIComponent

       把URI字符串采用UTF-8编码格式转化成escape格式的字符串。相对于encodeURI,encodeURIComponent会更加强大,它会对那些在encodeURI()中不被编码的符号(; / ? : @ & = + $ , #)统统会被编码。但是encodeURIComponent只会对URL的组成部分进行个别编码,而不用于对整个URL进行编码。对应解码函数方法decodeURIComponent。

       当然我们一般都是使用encodeURI方来进行编码操作。所谓的javascript两次编码后台两次解码就是使用该方法。javascript解决该问题有一次转码、两次转码两种解决方法。

一次转码

       javascript转码:

  1. var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm';  
  2. window.location.href = encodeURI(url);  

       转码后的URL:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%E6%88%91%E6%98%AFcm

       后台处理:

  1. String name = request.getParameter("name");  
  2. System.out.println("前台传入参数:" + name);  
  3. name  = new String(name.getBytes("ISO-8859-1"),"UTF-8");  
  4. System.out.println("经过解码后参数:" + name);  

       输出结果:

       前台传入参数:??????cm 
       经过解码后参数:我是cm

二次转码

       javascript

  1. var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm';  
  2. window.location.href = encodeURI(encodeURI(url));  

       转码后的url:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%25E6%2588%2591%25E6%2598%25AFcm

       后台处理:

  1. String name = request.getParameter("name");  
  2. System.out.println("前台传入参数:" + name);  
  3. name  = URLDecoder.decode(name,"UTF-8");  
  4. System.out.println("经过解码后参数:" + name);  

       输出结果:

       前台传入参数:E68891E698AFcm 

       经过解码后参数:我是cm

 

filter

1.设置编码方式

    <filter>

        <filter-name>chineseEncoding2</filter-name>

        <filter-class>testEncode.CharacterEncoding2</filter-class>

    </filter>

    <filter-mapping>

        <filter-name>chineseEncoding2</filter-name>

        <url-pattern>/*</url-pattern>

    </filter-mapping>

2在过滤器中进行解码操作

    publicvoid doFilter(ServletRequest servletRequest,

            ServletResponse servletResponse,FilterChain chain) throws IOException,

            ServletException {

        HttpServletRequest request =(HttpServletRequest) servletRequest;

        HttpServletResponse response =(HttpServletResponse) servletResponse;

 

        // 获得请求的方式 (1.post or 2.get),根据不同请求方式进行不同处理

        String method = request.getMethod();

        // 1. post方式提交的请求 , 直接设置编码为 UTF-8

        if (method.equalsIgnoreCase("post")) {

            try {

                request.setCharacterEncoding("UTF-8");

            } catch (UnsupportedEncodingException e) {

                e.printStackTrace();

            }

        }

        // 2. get方式提交的请求

        else {

            // 取出客户提交的参数集

            Enumeration<String>paramNames = request.getParameterNames();

            // 遍历参数集取出每个参数的名称及值

            while (paramNames.hasMoreElements()) {

                String name =paramNames.nextElement(); // 取出参数名称

                String values[] =request.getParameterValues(name);// 根据参数名称取出其值

                // 如果参数值集不为空

                if (values !=null) {

                    // 遍历参数值集

                    for (int i = 0; i < values.length; i++) {

                        try {

                            // 回圈依次将每个值调用 toUTF(values[i])方法转换参数值的字元编码

                            String vlustr =toUTF(values[i]);

                            values[i] = vlustr;

                        } catch(UnsupportedEncodingException e) {

                           e.printStackTrace();

                        }

                    }

                    // 将该值以属性的形式藏在 request

                    request.setAttribute(name,values);

                }

            }

 

        }

        // 设置响应方式和支持中文的字元集

        response.setContentType("text/html;charset=UTF-8");

 

        // 继续执行下一个 filter,无一下个 filter则执行请求

        chain.doFilter(request, response);

}     

其他注意

       1、设置pageEncoding、contentType

  1. <%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>  

       2、设置tomcat的URIEncoding

       在默认情况下,tomcat服务器使用的是ISO-8859-1编码格式来编码的,URIEncoding参数对get请求的URL进行编码,所以我们只需要在tomcat的server.xml文件的<Connector>标签中加上URIEncoding="utf-8"即可。

 

0 0