java中文乱码解决之道 (阅读别人博客摘抄)

第一部分摘抄

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

第二部分

  • ASCII: 该码只有七位数字来表示编码,最高的一位是用来保证传输可靠性的奇偶校验!
  • ASCII拓展码:将128-255进行编码,不同国家之间无法统一

  • GB2312 是双字节编码,支持大概七千个汉字,编码规则:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了

  • GBK也是双字节编码机制,是GB2312的拓展,手炉20000多个字符

  • GB18030 是变长字节编码,貌似收录的字符更多

ps:反正前128的数字都是与ASCII码兼容的

第三部分

  • 而目前几乎所有的电脑系统都支持基本拉丁字母,并各自支持不同的其他编码方式。Unicode为了和它们相互兼容,其首256字符保留给ISO 8859-1所定义的字符,使既有的西欧语系文字的转换不需特别考量;并且把大量相同的字符重复编到不同的字符码中去,使得旧有纷杂的编码方式得以和Unicode编码间互相直接转换,而不会丢失任何信息

  • 一个字符的Unicode编码是确定的,但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)
    Unicode是字符集,它主要有UTF-8、UTF-16、UTF-32三种实现方式

ps:unicode是字符集(数字与字符的映射集,一个数字唯一对应一个字符,但是这个数字的表现形式是什么就是utf解决的问题)

  • 由于各个系统平台的设计不同,可能会导致某些平台对字符的理解不同(比如字节顺序的理解)。这时将会导致同意字节流可能会被解释为不同的内容。如某个字符的十六进制为4E59,拆分为4E、59,在MAC上读取时是欧诺个低位开始的,那么MAC在遇到该字节流时会被解析为594E,找到的字符为“奎”,但是在Windows平台是从高字节开始读取,为4E59,找到的字符为“乙”。也就是说在Windows平台保存的“乙”跑到MAC平台上就变成了“奎”。这样势必会引起混乱,于是在Unicode编码中采用了大头(Big endian)、小头(Little endian)两种方式来进行区分。即第一个字节在前,就是大头方式,第二个字节在前就是小头方式。:
  • 那么这个时候就出现了一个问题:计算机怎么知道某个文件到底是采用哪种编码方式的呢?Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。)

ps:这里要补充一个点,是我以前看书看到的就是BOM的概念:这个概念参看这里BOM是什么感谢这个博主,BOM我的理解就是解决一串二进制流过来,怎么判断是unicode的哪种传输方式和 字节大小端读取顺序

UTF-8以字节为编码单元因此不需要 BOM 来表明字节顺序,但可以用 BOM 来表明编码方式。
字符 "Zero Width No-Break Space" 的 UTF-8 编码是 EF BB BF。所以如果接收者收到以 EF BB BF 开头的字节流,就知道这是 UTF-8编码了。



BOMs 文件头:

   00 00 FE FF    = UTF-32, big-endian

   FF FE 00 00    = UTF-32, little-endian

   EF BB BF       = UTF-8,

   FE FF          = UTF-16, big-endian

   FF FE          = UTF-16, little-endian


(至于为什么不需要编码bom也行,我想了一会,因为看了utf-8编码的结构就知道UTF8是以一个字节为基本的编码单元,bom不用指定字节的大小端,
而且他的编码格式就暗含了编码的大小端序吧"毕竟第一个字节的编码顺序太明显不同了",可以用 BOM(EF BB BF 称为零宽无间断间隔)来表明编码方式,
如果接收者收到以 EF BB BF 开头的字节流,就知道这是 UTF-8 编码)

几种常见名词:

ANSI:记事本的默认的编码方式,对于英文文件是ASCII编码,对于简体中文文件是GB2312编码。注意:不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中

Unicode:UCS-2编码方式,即直接用两个字节存入字符的Unicode码。该方式是"小头"little endian方式。

Unicode big endian:UCS-2编码方式,"大头"方式。


第四部分

java 的编码转换过程

- 第一步:当我们用编辑器编写java源文件,程序文件在保存时会采用操作系统默认的编码格式(一般我们中文的操作系统采用的是GBK编码格式)形成一个.java文件。java源文件是采用操作系统默认支持的file.encoding编码格式保存的。下面代码可以查看系统的file.encoding参数值。

System.out.println(System.getProperty("file.encoding"));
  • 第二步:当我们使用javac.exe编译我们的java文件时,JDK首先会确认它的编译参数encoding来确定源代码字符集,如果我们不指定该编译参数,JDK首先会获取操作系统默认的file.encoding参数,然后JDK就会把我们编写的java源程序从file.encoding编码格式转化为JAVA内部默认的UNICODE格式放入内存中。(先把硬盘上的二进制字节流,通过os上的file.encoding变成字符,再利用unicode 码表将字符变成unicode数字?就是变成一一映射的的十六进制数字了?)
  • 第三步:JDK将上面编译好的且保存在内存中信息写入class文件中,形成.class文件。此时.class文件是Unicode编码的,也就是说我们常见的.class文件中的内容无论是中文字符还是英文字符,他们都已经转换为Unicode编码格式了。
  • 第四步:运行编译的类:在这里会存在一下几种情况

1、直接在console上运行。

这种情况下,JVM首先会把保存在操作系统中的class文件读入到内存中,这个时候内存中class文件编码格式为Unicode,然后JVM运行它。如果需要用户输入信息,则会采用file.encoding编码格式对用户输入的信息进行编码同时转换为Unicode编码格式保存到内存中。程序运行后,将产生的结果再转化为file.encoding格式返回给操作系统并输出到界面去。整个流程如下:
Pf6mLR.md.png

2、JSP/Servlet类。

由于JSP文件最终也会转换为servlet文件(只不过存储的位置不同而已),所以这里我们也将JSP文件纳入其中。

当用户请求Servlet时,WEB容器会调用它的JVM来运行Servlet。首先JVM会把servlet的class加载到内存中去,内存中的servlet代码是Unicode编码格式的。然后JVM在内存中运行该Servlet,在运行过程中如果需要接受从客户端传递过来的数据(如表单和URL传递的数据),则WEB容器会接受传入的数据,在接收过程中如果程序设定了传入参数的的编码则采用设定的编码格式,如果没有设置则采用默认的ISO-8859-1编码格式,接收的数据后JVM会将这些数据进行编码格式转换为Unicode并且存入到内存中。运行Servlet后产生输出结果,同时这些输出结果的编码格式仍然为Unicode。紧接着WEB容器会将产生的Unicode编码格式的字符串直接发送置客户端,如果程序指定了输出时的编码格式,则按照指定的编码格式输出到浏览器,否则采用默认的ISO-8859-1编码格式。整个过程流程图如下:

image
3、java类与数据库之间。

我们知道java程序与数据库的连接都是通过JDBC驱动程序来连接的,而JDBC驱动程序默认的是ISO-8859-1编码格式的,也就是说我们通过java程序向数据库传递数据时,JDBC首先会将Unicode编码格式的数据转换为ISO-8859-1的编码格式,然后在存储在数据库中,即在数据库保存数据时,默认格式为ISO-8859-1。

(ps:我们不仅需要设置数据库的字符集,在spring中指定数据源url字符串时,也会指定编码字符集)
image

第五部分

java的 I/O编解码操作

在我们读取文件时,读取字节的编码取决于文件所使用的编码格式,而在转换为String过程中也会涉及到编码的问题,如果两者之间的编码格式不同可能会出现问题。例如存在一个问题test.txt编码格式为UTF-8,那么通过字节流读取文件时所获得的数据流编码格式就是UTF-8,而我们在转化成String过程中如果不指定编码格式,则默认使用系统编码格式(GBK)来解码操作,由于两者编码格式不一致,那么在构造String过程肯定会产生乱码

graph TD
A[硬盘上文件]-->B[os上成根据文件自身的格式编码成字节流]
B---|指定编码格式,如果缺失默认为操作系统编码格式|编码成string

ps:所以从系统上解码成字符串的编码集(文件自身的编码集)和编码成string的编码集必须一致才不会出现乱码!

buffer.append(new String(bytes,0,n,"UTF-8"));

字节字符流转换

字节转换为字符一定少不了InputStreamReader。API解释如下:InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。 每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。要启用从字节到字符的有效转换,可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节。API解释非常清楚,InputStreamReader在底层读取文件时仍然采用字节读取,读取字节后它需要根据一个指定的编码格式来解析为字符,如果没有指定编码格式则采用系统默认编码格式。

这里file的编码格式是utf-8
String file = "C:\\test.txt"; 
         String charset = "UTF-8"; 
         // 写字符换转成字节流
         FileOutputStream outputStream = new FileOutputStream(file); 
         OutputStreamWriter writer = new OutputStreamWriter(outputStream, charset); 
         try { 
            writer.write("我是 cm"); 
         } finally { 
            writer.close(); 
         } 

         // 读取字节转换成字符
         FileInputStream inputStream = new FileInputStream(file); 
         InputStreamReader reader = new InputStreamReader( 
         inputStream, charset); 
         StringBuffer buffer = new StringBuffer(); 
         char[] buf = new char[64]; 
         int count = 0; 
         try { 
            while ((count = reader.read(buf)) != -1) { 
                buffer.append(buf, 0, count); 
            } 
         } finally { 
            reader.close(); 
         }
         System.out.println(buffer);

内存编码

首先我们看下面这段简单的代码

String s = "我是 cm"; 
         byte[] bytes = s.getBytes(); 
         String s1 = new String(bytes,"GBK"); 
         String s2 = new String(bytes);

在这段代码中我们看到了三处编码转换过程(一次编码,两次解码)。先看String.getTytes():

public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }

内部调用StringCoding.encode()方法操作:

static byte[] encode(char[] ca, int off, int len) {
        String csn = Charset.defaultCharset().name();
        try {
            // use charset name encode() variant which provides caching.
            return encode(csn, ca, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
            return encode("ISO-8859-1", ca, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }

encode(char[] paramArrayOfChar, int paramInt1, int paramInt2)方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作,进一步深入如下:

String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;

同样的方法可以看到new String 的构造函数内部是调用StringCoding.decode()方法:

public String(byte bytes[], int offset, int length, Charset charset) {
        if (charset == null)
            throw new NullPointerException("charset");
        checkBounds(bytes, offset, length);
        this.value =  StringCoding.decode(charset, bytes, offset, length);
    }

decode方法和encode对编码格式的处理是一样的。

对于以上两种情况我们只需要设置统一的编码格式一般都不会产生乱码问题。

博主还有一个例子如下:一开始我不太理解(string.toCharArray())输出结果,后来在线转换一看才知道那是unicode编码组成的数组(\u6211 \u662f \u0020 \u0063 \u006d 可以自己试试)

public class Test02 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        String string = "我是 cm";
        Test02.printChart(string.toCharArray());
        Test02.printChart(string.getBytes("ISO-8859-1"));
        Test02.printChart(string.getBytes("GBK"));
        Test02.printChart(string.getBytes("UTF-8"));
    }

    /**
     * char转换为16进制
     */
    public static void printChart(char[] chars){
        for(int i = 0 ; i < chars.length ; i++){
            System.out.print(Integer.toHexString(chars[i]) + " "); 
        }
        System.out.println("");
    }

    /**
     * byte转换为16进制
     */
    public static void printChart(byte[] bytes){
        for(int i = 0 ; i < bytes.length ; i++){
            String hex = Integer.toHexString(bytes[i] & 0xFF); 
             if (hex.length() == 1) { 
               hex = '0' + hex; 
             } 
             System.out.print(hex.toUpperCase() + " "); 
        }
        System.out.println("");
    }
}
-------------------------outPut:
6211 662f 20 63 6d   这里是unicode编码
3F 3F 20 63 6D 
CE D2 CA C7 20 63 6D 
E6 88 91 E6 98 AF 20 63 6D

第六部分 java web的get\post编码

-GET

通过GET方式提交的数据都是拼接在URL后面(可以当做query String??)来提交的,所以tomcat服务器在进行解码过程中URIEncoding就起到作用了。tomcat服务器会根据设置的URIEncoding来进行解码,如果没有设置则会使用默认的ISO-8859-1来解码。假如我们在页面将编码设置为UTF-8,而URIEncoding设置的不是或者没有设置,那么服务器进行解码时就会产生乱码。这个时候我们一般可以通过new String(request.getParameter(“name”).getBytes(“iso-8859-1”),”utf-8”) 的形式来获取正确数据。
所以一般都在 server.xml中设置

<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/> 来避免get服务器乱码
  • 表单POST

对于POST方式,它采用的编码也是由页面来决定的即contentType。当我通过点击页面的submit按钮来提交表单时,浏览器首先会根据ontentType的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是指服务器发送给客户端时的内容编码。

第八部分 js编码url 和 filter解决参数乱码问题

我们知道URL向服务器发送请求编码过程实在是实在太混乱了。不同的操作系统、不同的浏览器、不同的网页字符集,将导致完全不同的编码结果。使用javascript编码不给浏览器插手的机会,编码之后再向服务器发送请求,然后在服务器中解码。在掌握该方法的时候,我们需要料及javascript编码的三个方法:escape()、encodeURI()、encodeURIComponent()。

  • escape :事实上escape()不能直接用于URL编码,它的真正作用是返回一个字符的Unicode编码值。
  • encodeURI:对整个URL进行编码,它采用的是UTF-8格式输出编码后的字符串。不过encodeURI除了ASCII编码外对于一些特殊的字符也不会进行编码如:! @ # $& * ( ) = : / ; ? + ‘。 这个方法是比较常用的
  • encodeURIComponent():把URI字符串采用UTF-8编码格式转化成escape格式的字符串。相对于encodeURI,encodeURIComponent会更加强大,它会对那些在encodeURI()中不被编码的符号(; / ? : @ & = + $ , #)统统会被编码。

js编码及解码的流程


javascript转码:

var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm';
window.location.href = encodeURI(url);
转码后的URL:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%E6%88%91%E6%98%AFcm

后台处理:

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

filter解码中文乱码问题

public class CharacterEncoding implements Filter {
    protected FilterConfig filterConfig ;
    String encoding = null;

    public void destroy() {
        this.filterConfig = null;
    }

    /**
     * 初始化
     */
    public void init(FilterConfig filterConfig) {
        this.filterConfig = filterConfig;
    }

    /**
     * 将 inStr 转为 UTF-8 的编码形式
     * 
     * @param inStr 输入字符串
     * @return UTF - 8 的编码形式的字符串
     * @throws UnsupportedEncodingException
     */
    private String toUTF(String inStr) throws UnsupportedEncodingException {
        String outStr = "";
        if (inStr != null) {
            outStr = new String(inStr.getBytes("iso-8859-1"), "UTF-8");
        }
        return outStr;
    }

    /**
     * 中文乱码过滤处理
     */
    public void 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);
    }
}
配置:

<!-- 中文过滤器的配置 -->
    <filter>
        <filter-name>chineseEncoding</filter-name>
        <filter-class>com.test.filter.CharacterEncoding</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>chineseEncoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

参考文档chenssy 的个人博客,非常感谢这位博主的博客,终于使我这个菜鸟稍微弄清楚一些乱码的问题

猜你喜欢

转载自blog.csdn.net/whp404/article/details/81917093