IDEA中控制台启动乱码(淇℃伅)的原因简单分析

-1. 乱码情况

我们在使用IDEA的时候,项目通常会配置自己的tomcat,但是有时你会发现项目启动的时候控制台会报错,虽然说这个不会影响项目的运行,但是作为有强迫症的人来说,还是很难忍受这个乱码的。今天我们就来对这个现象做一个简单的探究。以下结果和现象都是本人在使用过程中出现的,每个人的情况可能都不同,希望能对大家有所帮助。

图1.tomcat启动时的报错

图1. tomcat启动时的乱码

图2.中文乱码
图2. 日志中文乱码

图3.文件路径乱码
图3. 文件路径中文乱码

0. 前提知识:UTF-8、GBK编码下的中文

UTF-8编码中,中文主要是占三个字节(补充说明 1:一些繁体中文是占四个字节,详细请查看文章尾部注解)。编码时,一个中文会被编码成三个字节;解码时,同样以三个字节为一组进行解码。

GBK编码中,中文是占两个字节。即一个中文会被编码成两个字节;同理,解码时,以两个字节为一组进行解码。

1. 以UTF-8编码,GBK解码的乱码

如果是以UTF-8编码,GBK解码时,就会出现乱码的现象。

例如:假入有两个字以UTF-8编码时,由上面的知识可知:编码后共有六个字节,而以GBK解码时,就会解析出三个字。

具体例子:“信息"在UTF-8编码下的六个字节是:E4, BF, A1, E6, 81, AF。即E4, BF, A1表示"信”,E6, 81, AF表示"息"。如果以GBK解码的话,两个字节一组解码,分为E4, BF A1, E6 81, AF三组,每组分别对应一个字符,实际上这三个字符分别对应"淇"、“℃”、“伅”。以下为详细说明

:E4 BF A1
(补充说明:以图片中第一个“俘”字为例,B7FD为GBK格式,4FD8为UTF-16格式,E4 BF 98为UTF-8格式,以下类同):
图9

图4. 信的UTF-8编码

:E6 81 AF
图10

图5. 息的UTF-8编码

淇:E4 BF
图6

图6. 淇的GBK编码

℃:A1 E6图7

图7. ℃的GBK编码

伅:81 AF
图8

图8. 伅的GBK编码

问题出现了,因为UTF-8和GBK的编解码格式不同,由此导致了乱码的产生。即UTF-8以3个字节为一组编码,而GBK以2个字节为一组进行解码。

再深究一下,如果是单个中文字符用UTF-8编码,GBK码呢?我们知道,UTF-8下,一个中文占三个字节,奇数个中文同样也是占奇数个字节。那么,在GBK两个一组的解码下,总会留下最后单个字节,那GBK又是会怎么处理的呢?

解答:因为GBK中,单字节的字符默认和ASCII是一样的。下图摘自维基百科
维基百科中GBK编码的说明

图9. 维基百科中GBK编码的说明

  • 如果单字节高位为0开头,则当成ASCII来解码;
  • 如果单字节高位为1开头,而ASCII中的单字节最高位肯定是0开头,此时GBK就不知道如何解码,所以会用一个问号?代替,问号在ASCII中的字节是3F。而在UTF-8的格式中,中文是3个字节,故每个字节的高位肯定是1。所以这种情况下,最后一个字节的内容一定是被修改过了,故再用GBK编码回去的话,最后那个字节就是按3F编码回去的。
  • 同理:GBK码的时候,遇到无法编码的字符(GBK能表示的字符有限,一些国外的文字没纳入GBK的范围,比如说韩国的一些文字),GBK也会以3F来代替该字符。

总结:GBK编码也好,解码也好。失败的情况下都是用字节3F来代替原有的字节。

1.1 额外说明

既然按两个字节或三个字节编解码可能会产生问题,那我们能不能按单个字节来编解码呢,这样的话,无论是什么,我们都能保证原始数据不被破坏,就是显示的时候会有问题,比如乱码之类的,但是这种只是显示的乱码我们可以在后期中还原,因为原始数据没有被破坏。还真有,iso-8859-1就是,大家可以自行来尝试以下。


1.2 代码复现

补充知识

在Java中,是以UTF-16格式作为内存的字符存储格式,但是在编码的过程中,我们可以指定为UTF-8、GBK或其它编码格式,最后存储的时候都会转化为UTF-16格式。中文大多是占用两个字节(注意和UTF-8区分,UTF-8中,大部分常见中文都是三个字节),只有极少部分中文要用四个字节来表示(这个暂时先不讨论这种情况)

无论是UTF-8转GBK,还是GBK转UTF-8等,都不是直接转化的,是先转为Unicode格式(特指UTF-16),然后再转化为另一种格式的。

/**
 * @description: 简单探究tomcat乱码的小原因。
 * @author: zhong
 * @date: 2020/11/3 9:22
 * @motto: talk is cheap, show me your code!
 */
    public static void main(String[] args) {

        System.out.println(Charset.defaultCharset().name());


        List<String> source = new ArrayList<>(10);
        List<String> corrupted = new ArrayList<>(10);

        source.add("信息");
        // "信息"用utf8编码,gbk解码后,显示的文字就是"淇℃伅"。后面的也是同理。
        corrupted.add("淇℃伅");

        source.add("用户登录主页");
        corrupted.add("鐢ㄦ埛鐧诲綍涓婚〉");

        source.add("你");
        corrupted.add("浣�");

        source.add("你好呀");
        corrupted.add("浣犲ソ鍛�");

        encodeAndDecode(source, corrupted);

    }


    /**
     * @description 探究utf8编码,gbk解码;gbk编码,utf8解码这两种形式的转换
     * @param source 里面是正常的中文
     * @param corrupted 里面是以utf8编码,以gbk解码后显示的中文。和source里面正常显示
     *                  的中文是一一对应的。即source第一个索引的是正常中文,则corrupted
     *                  第一个索引的则是其用utf8编码,gbk解码后的中文。
     * @return void
     * @date 2020/11/3 12:32
     */
    public static void encodeAndDecode(List<String> source, List<String> corrupted) {
        for (String s : source) {
            //utf8编码,gbk解码
            try {
                System.out.println("编码前的原文: " + s);
                byte[] utf8Bytes = s.getBytes("utf8");
                System.out.print("utf8编码后的字节: ");
                for (byte b : utf8Bytes) {
                    System.out.print(Integer.toHexString(b & 0xff) + ",");
                }
                System.out.println();

                String utf8ToGbk = null;
                // 直接获取到utf8编码后的字节,然后用gbk解码
                utf8ToGbk = new String(utf8Bytes, "gbk");
                System.out.println("utf8编码,gbk解码后的文字: " + utf8ToGbk + "\n" + "----------------------");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        System.out.println("<==================分割线===================>");
        for (String s : corrupted) {
            // gbk编码,utf8解码
            try {
                System.out.println("编码前的原文: " + s);
                byte[] gbkBytes = s.getBytes("gbk");
                System.out.print("gbk编码后的字节: ");
                for (byte b : gbkBytes) {
                    System.out.print(Integer.toHexString(b & 0xff) + ",");
                }
                System.out.println();

                String gbkToUtf8 = new String(gbkBytes, "utf8");
                System.out.println("gbk编码,utf8解码后的文字: " + gbkToUtf8 + "\n" + "----------------------");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
    }

结果如下:在这里插入图片描述

图10. UTF-8编码,GBK解码的例子
逐一解释说明
  • 信息”、"用户登录主页"这两个例子,汉字都是偶数个,故UTF-8编码后的字节总数为偶数个(偶数×3=偶数),所以用GBK解码的话,只是显示乱码,而原有的字节码没有改变。
  • ”、"你好呀"这两个例子,汉字是奇数个,故UTF-8编码后的总字节数也是奇数个(奇数×3=奇数)。GBK解码,最后会剩下一个单字节。由上面GBK如何解码单字节的内容可知,该字节的最高位肯定是1开头,GBK不知道该如何解码,只能用一个问号代替。所以你会看到GBK解码后,每个例子后面都跟着一个问号。别忘了:此时最后一个字节的内容已经被修改了,后期还原肯定是会出问题的。

结论:以UTF-8编码,用GBK解码时,会以两个字节两个字节的解码,所以会造成中文乱码,但是,此时编码格式还没有乱(中文字符为奇数时,会有乱码,还原不回去,前面已经说过),只需将“淇℃伅”用GBK编码,UTF-8再解码一次即可。如果项目中出现这样的乱码,要注意是不是哪里用了UTF-8编码,然后用GBK解码导致的。

2. 以GBK编码,UTF-8解码的乱码

2.1 GBK解码的方式

  • 基础知识点:UTF-8的编码格式如下。UTF-8最多占6个字节,但是占5、6个字节编码情况较少,我们就不考虑。
    1. 单字节编码:0xxx xxxx
    2. 双字节编码:110x xxxx, 10xx xxxx
    3. 三字节编码:1110 xxxx, 10xx xxxx, 10xx xxxx
    4. 四字节编码:1111 0xxx, 10xx xxxx, 10xx xxxx, 10xx xxxx
    5. 五字节编码:1111 10xx, 10xx xxxx, 10xx xxxx, 10xx xxxx, 10xx xxxx
    6. 六字节编码:1111 110x, 10xx xxxx, 10xx xxxx, 10xx xxxx, 10xx xxxx, 10xx xxxx
  • UTF-8解码的规则:
    • 正常解码:从第一个字节开始,如果第一个字节的高位以0开头,则默认是单字节;如果第一个字节为110开头,则会将此字节和后面一个字节一起解码。如果是第一个字节以1110开头,则会将此字节和后面的两个字节一起解码。依次类推。
    • 非正常解码:如果字节码缺少或者格式不对,例如:第一个字节以110开头,但是后面却没有字节了;或者第一个字节以1110开头,后面只剩一个字节;或者以110开头,第二个字节却没有以10开头等等。遇到此类情况,UTF-8是怎么处理的呢?

在这里插入图片描述

图11. UTF-8的解码(摘自维基百科)

由图11可知,UTF-8遇到无效的字节,则会以字符"�"(U+FFFD)来代替,(但是"�"字符在UTF-8中是存在的,其所在的UTF-8编码是EF, BF, BD,U+FFFD是其UTF-16的表示形式)

在这里插入图片描述

图12. UTF-8编码,GBK解码后的文字,再以GBK编码,UTF-8解码还原

图12逐一解释说明:

  • 淇℃伅”、"鐢ㄦ埛鐧诲綍涓婚〉"例子中没有出现问号字符,说明GBK解码的时候,没有遇到单个字节的情况,原字节数为偶数个,所以再以GBK编码,UTF-8解码回去,是可以将原内容还原的。
  • "浣�"即为中文"你"用UTF-8编码,以GBK解码显示的文字。以GBK编码时,最后那个字符在GBK中无法表示,故只能用3F代替。再以UTF-8再解码回去时,因为e4, bd会解码失败,所以也会用一个问号�代替(注意:�这个问号和后面这个?是不同的,前者在UTF-8下的表示是EF, BF, BD,是三个字节,在UTF-16表示下是U+FFFD;后者在UTF-8下的表示是3F,是单字节)。而后面这个3f,为单字节码,则可以正常显示,所以这个问号没带方块。
  • 浣犲ソ鍛�“即为中文"你好呀"用UTF-8编码,GBK解码后的文字。再以GBK编码回去,可知最后一个字符在GBK中无法表示,故只能以3F代替,所以总共可以得到9个字节,前6个字节能正常显示,即为"你好”,最后3个字节是e5, 91, 3f。可知e5, 91解码失败,用�表示,3f用问号表示。
    补充一个例子:刚开始学计算机的时候,大家总会遇到”锟斤拷"这类的乱码吧,由上面的分析内容进行猜测,这可能是UTF-8编码,然后用GBK解码造成的现象(因为这三个字能显示出来,而且没有问号,只是连起来感觉怪怪的样子)。查阅GBK编码表得,锟:EF, BF;斤:BD, EF;拷:BF, BD。故组合起来即为EF, BF, BD, EF, BF, BD。大家发现没有,EF, BF, BD这3个字节好似很熟悉,在哪里见过的样子,这不就是�这个字符在UTF-8下的编码吗?
    • 分析:原文是��,经过UTF-8编码后,变成了EF, BF, BD, EF, BF, BD;再经过GBK解码后,变成了"锟斤拷"。故原文在UTF-8编码前就已经是乱码了,因为�是UTF-8解码失败才会产生的字符(UTF-8不存在编码失败的情况,因为在UTF-8下,全世界的字符几乎都能表示,故只可能说会解码失败)

结论

  1. 对于GBK无法编码的字符,GBK会以3F来代替。对于GBK无法解码的字符,也会用3F来代替。
  2. 对于UTF-8无法解码的字符,会以�(U+FFFD)来代替。对于UTF-8无法编码的字符,暂时来说好似没有,因为UTF-8可以编码太多的字符。故当显示的中文中出现问号的时候,表示原来的字节码肯定是修改过了,肯定不能百分百还原回去的。

更详细的参考文章:

https://blog.csdn.net/csdn_ds/article/details/79077483

3. 解决IDEA中tomcat启动时出现的(淇℃伅)的解决办法

  • 个人认为出现该现象的可能原因:IDEA内部是用UTF编码的,但是控制台显式的时候是用GBK解码的,这时我们再用GBK编码回去,用UTF-8解码就能正常了。即在下面第二条中修改编码为GBK。
    • :如果乱码中没有问号?之类的符号,一般情况下原来的编码都没有破坏,只不过是一样的编码,用不同的方式解码造成的结果。
    • 暂时还留有的疑问:如果IDEA中所有的编码都设置为了UTF(这里指UTF-8),为啥在控制台显式的时候会以GBK来解码,难道会转化为操作系统的默认语言的?以GBK编码回去,它能显示正常,说明最终控制台还是以UTF-8解码的。以下为个人猜想,还未经过验证,如果有知道的大佬,希望能指点一下这个地方
      • 修改配置前:IDEA(UTF-8编码)---->tomcat(GBK解,UTF-8编)---->IDEA控制台(UTF-8解)
      • 修改配置后:IDEA(UTF-8编码)---->tomcat(GBK解,GBK编)---->IDEA控制台(UTF-8解)
  1. 首先修改IDEA中所有的地方为UTF-8编码
    参考文章:

https://blog.csdn.net/m0_38132361/article/details/80628203

  1. 在tomcat的conf/loggin.properties文件中追加(具体可以参考网上其它教程)
    java.util.logging.ConsoleHandler.encoding = GBK

如果原来的是
java.util.logging.ConsoleHandler.encoding = UTF-8
则改为:
java.util.logging.ConsoleHandler.encoding = GBK


补充说明的参考文献

  1. UTF-8编码中,汉字到底占用几个字节。汉字占用3-4个字节,通常情况下可认为是3个字节

    https://blog.css8.cn/post/11228387.html

大神级别的乱码理解 强烈推荐!!!

https://blog.csdn.net/u011511756/article/details/107147491

注解区


  1. 严格来说,中文的UTF-8编码是占个字节。但是四个字节的不常见,主要以,平时一般都用不到。事实上,四个字节的汉字总数比三个字节的汉字总数还多,U+20000 - U+2FA1D : 0xF0 0xA0 0×80 0×80 - 0xF0 0xAF 0xA8 0x9D,共 64029 个为不常见汉字,这里暂时不考虑。 ↩︎

  • 27
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值