Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GBK<->UTF8 互转问题: Maven checkstyle输出乱码 #26

Closed
nobodxbodon opened this issue Aug 24, 2017 · 23 comments
Closed

GBK<->UTF8 互转问题: Maven checkstyle输出乱码 #26

nobodxbodon opened this issue Aug 24, 2017 · 23 comments
Labels

Comments

@nobodxbodon
Copy link
Member

nobodxbodon commented Aug 24, 2017

下面摘自 @azigeprogram-in-chinese/junit4_in_chinese#11 (comment)

在IDE和命令行(都是GBK)里都是之前那样,然后在UTF-8的命令行里就变成这样了……

[INFO] --- maven-checkstyle-plugin:2.17:check (validate) @ junit4_in_chinese ---
[INFO] ▒?始检查▒?▒▒??
▒?查完成▒??

目测是编码转了两次的结果,java在我的系统里应该默认用GBK来编码字符的,但是只有这个插件输出的字符是用UTF-8编码的

感觉值得探究一下. 这种编码格式不匹配的问题可能会比想象中的更普遍, 如果中文windows的默认格式是GBK的话.

@nobodxbodon
Copy link
Member Author

@azige 有个不明白的是为啥utf-8的命令行会输出带乱码的中文. 请问能把mvn -v的输出贴一下吗?

@azige
Copy link

azige commented Aug 27, 2017

Apache Maven 3.0.5 (r01de14724cdef164cd33c7c8c2fe155faf9602da; 2013-02-19 21:51:28+0800)
Maven home: D:\Program Files\NetBeans 8.2\java\maven
Java version: 1.8.0_102, vendor: Oracle Corporation
Java home: D:\Java\jdk1.8.0_102\jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 10", version: "10.0", arch: "amd64", family: "dos"

只是把命令行改成了UTF-8而已,系统本身的环境还是GBK的

@nobodxbodon
Copy link
Member Author

发现不少网上分析GBK<->UTF8互转出现乱码的文章里都提到偶数OK,奇数有问题的说法. 其实不尽然.

比如此文提到的"姓名"的三个双字节段恰好都是合法的GBK字符[e5 a7] [93 e5] [90 8d]因此没有被破坏.

只要UTF8的字节码中有GBK中不合法的字符, 就会有问题. 比如我们碰到的"开始". 初步分析已经整理在program-in-chinese/junit4_in_chinese#12 (comment) 还有一些细节要探究, 然后是针对这个插件问题的对策.

@nobodxbodon nobodxbodon changed the title Maven checkstyle输出乱码问题 GBK<->UTF8 互转问题: Maven checkstyle输出乱码 Oct 3, 2017
@nobodxbodon
Copy link
Member Author

看起来checkstyle的Logger里是按照UTF8编码输出信息的:

 final Writer infoStreamWriter = new OutputStreamWriter(infoStream, StandardCharsets.UTF_8);
...
            final Writer errorStreamWriter = new OutputStreamWriter(errorStream,
StandardCharsets.UTF_8);

试了把StandardCharsets.UTF_8改成Charset.defaultCharset()之后, 本地测试就显示正常了(ubuntu, mvn -v: Default locale:zh_CN, plaform encoding: GBK)

@nobodxbodon
Copy link
Member Author

找到一个久远的中文部分乱码问题, 感觉应该也是类似的转码过程由于编码之间不兼容导致的, 就是不知道具体转变过程. 后来看到JDBC连接MySQL抛出异常信息乱码, 才试出了这个可能的过程, 输出同样的乱码中�?��?�??�??个传说:

    String 初始字符串 = "中国从前有个传说";
    
    String 编码1 = "utf-8";
    String 编码2 = "windows-1252";
    String 编码3 = "iso-8859-1";
    String 编码4 = "utf-8";
    
    byte[] 字节 = 初始字符串.getBytes(编码1);
    String 转编码后 = new String(字节, 编码2);
    byte[] 转码字节 = 转编码后.getBytes(编码3);
    String 二转编码后 = new String(转码字节, 编码4);
    System.out.println(二转编码后);

以后遇到这种部分乱码的问题, 考虑用个宽度搜索在所有编码里找可能的转变路径, 才能生成同样的乱码 hehe

@nobodxbodon
Copy link
Member Author

阶段小结已发在都市传说: "部分"中文出现乱码.
为了尝试解决, 基于#26 (comment), 向checkstyle作者提问为何用UTF8编码日志信息.

@nobodxbodon
Copy link
Member Author

@program-in-chinese/all 召唤集体智慧应对~~. 插件作者回复说原因是错误信息的保存文件格式是UTF8. 感觉有两个问题:

  • 文件格式是UTF8, 但读取到Java字符串时用的编码格式是什么? 换句话说, final Writer infoStreamWriter = new OutputStreamWriter(infoStream, "UTF-8");这里用的编码不是已经和文件编码格式无关了吗?
  • 还有除了GBK<->UTF8 互转问题: Maven checkstyle输出乱码 #26 (comment) 之外的解决方案吗?

@azige
Copy link

azige commented Oct 12, 2017

说一下本人知道的事

  • Unicode 解码器在遇到无法处理的字节序列时会将其转换为 '\uFFFD',此字符在 UTF-8 编码下成为 0xEFBFBD,两个连在一起就会被 GBK 解码器转换成著名的“锟(0xEFBF)斤(0xBDEF)拷(0xBFBD)”
  • GBK 解码器只会对奇数字节序列的最后那个字节造成破坏,除此之外的字节转换成字符后,都能原封不动的被 GBK 编码器转换为原本的字节(大概,但是中间的某些特定字节应该也会被破坏)
  • iso-8859-1 编解码器怎么都不会对字节序列造成破坏

windows-1252 没有深入了解过,尝试了一下看来它会对无法处理的字节序列造成某种程度的破坏。不过"中国从前有个传说"那个问题看起来还是因为 iso-8859-1 和 windows-1252 的一些码位不同导致转换出错而破坏了原本的字节序列


那个插件的问题果然跟本人想的一样原作者是直接用 UTF-8 来写字节的啊,虽然意图没搞懂,但是这样在非 UTF-8 的控制台里自然是会乱码的

@nobodxbodon
Copy link
Member Author

nobodxbodon commented Oct 12, 2017

GBK 解码器只会对奇数字节序列的最后那个字节造成破坏,除此之外的字节转换成字符后,都能原封不动的被 GBK 编码器转换为原本的字节

我也看到不少文章提到奇数有问题, 偶数没问题. 其实偶数个字符也可能有问题. 原因详见program-in-chinese/junit4_in_chinese#12 (comment). 刚发现更好的例子是"开始": https://gist.github.com/nobodxbodon/f01a6853ff236703ed2751bd2cf20848
不仅'开'被破坏了, 而且'始'也被破坏了. 因为'开'的第三个字节被替换成了3f, 从而在GBK编码时, '始'的前两位被合成了一个GBK字符, 而第三个又落单被替换成了3f

编码 原字1 原字2
原字
UTF8表示 e5 bc 80 e5 a7 8b
转为GBK后 e5 bc 3f e5 a7 3f
GBK字符 寮� 濮�
转回UTF8字符 �? �?

iso-8859-1没有8_,9_等等, 而windows-1252没有8d. 这些都被替换成了3f(两次转换都有被破坏的). 演示例程在这里


我也还不明白为何用UTF8编码. 如果系统不是UTF8编码, 那么输出的时候似乎肯定会有问题. 听作者回复的意思好像是为了确保读取UTF8格式的资源文件(messages_x.properties)的时候不会有问题. 但是我的感觉是已经从文件中读出来了(Java的String)之后就应该依赖系统编码.

@azige
Copy link

azige commented Oct 12, 2017

看来 Java 的 GBK 和 windows-1252 解码不存在的码位的时候在Java内部就转换成 \uFFFD 了,而 \uFFFD 再编码就会变成 0x3f(问号?还真是简单易懂)

可以试试这个例子就会发现惊喜了

public static void main(String[] args) throws Exception{
    二重转换("开始", "UTF-8", "GBK");
    二重转换("开始", "UTF-8", "iso-8859-1");
    二重转换("开始", "UTF-8", "windows-1252");
    System.out.println("=======================");
    二重转换("中国从前有个传说", "UTF-8", "GBK");
    二重转换("中国从前有个传说", "UTF-8", "iso-8859-1");
    二重转换("中国从前有个传说", "UTF-8", "windows-1252");
    System.out.println("=======================");
    二重转换("中国开始有个传说", "UTF-8", "GBK");
    二重转换("中国开始有个传说", "UTF-8", "iso-8859-1");
    二重转换("中国开始有个传说", "UTF-8", "windows-1252");
}

public static void 二重转换(String 源, String 初次编码, String 二次编码) throws UnsupportedEncodingException{
    System.out.println(初次编码 + " -> " + 二次编码);
    System.out.println("源字符串: " + 源);
    byte[] bytes = 源.getBytes(初次编码);
    System.out.print("源字节串: ");
    for (byte b : bytes){
        System.out.printf("%x ", b);
    }
    System.out.println();
    String 中间形态 = new String(bytes, 二次编码);
    System.out.println("中间形态字符串: " + 中间形态);
    System.out.print("内部 UTF-16 字节串: ");
    for (char c : 中间形态.toCharArray()){
        System.out.printf("%x ", (int)c);
    }
    System.out.println();
    bytes = 中间形态.getBytes(二次编码);
    System.out.print("转换后字节串: ");
    for (byte b : bytes){
        System.out.printf("%x ", b);
    }
    System.out.println();
    System.out.println("转换后字符串: " + new String(bytes, 初次编码));
    System.out.println();
}

@nobodxbodon
Copy link
Member Author

@azige 结论是...? 既然Java的String用的是UTF16编码, 输出时的编码和资源文件格式应该完全无关了吧? 你觉得#26 (comment) 之外还有什么解决方案吗?

@azige
Copy link

azige commented Oct 14, 2017

嗯之前的 #26 (comment) 本人只是讨论一下错误的反复转码导致源字节流被破坏的成因。关于 checkstyle 插件,本人还没深入去研读过源代码,但是 maven 的日志记得只能输出字符串,如果 checkstyle 自己的日志模块输出 UTF-8 字节流的话,必然要在输出到 maven 日志的时候又要转换回字符。那部分不知道使用了何种编码来转换,而源字节流被破坏的问题可能就是来自这个地方。

@nobodxbodon
Copy link
Member Author

@azige 不好意思忘说了, 我之前好像找到了它按照UTF8读取properties文件的地方, 详见checkstyle/checkstyle@702a1a9#commitcomment-24945064

如果 checkstyle 自己的日志模块输出 UTF-8 字节流的话,必然要在输出到 maven 日志的时候又要转换回字符

之前没有想到这个问题...但是它最后输出的时候应该用的是默认系统编码吧, 详见checkstyle/checkstyle#3569 (comment):

When printing the message here, the strings are encoded using platform's default character encoding again. Thus it seems reasonable to use default charset in the beginning?

@azige
Copy link

azige commented Oct 16, 2017

不好意思忘说了, 我之前好像找到了它按照UTF8读取properties文件的地方, 详见checkstyle/checkstyle@702a1a9#commitcomment-24945064

我之前就是看到他这个 Logger 然后确认他是输出了 UTF-8 字节流的。

你能取到 ▒?始检查▒?▒▒?? ▒?查完成▒?? 的原文吗?进行一下 二重转换("<原文>", "UTF-8", "GBK"); 看看能不能得到相同的结果大概就能验证本人的猜想了

@nobodxbodon
Copy link
Member Author

nobodxbodon commented Oct 16, 2017

@azige 不好意思我蒙圈了...你的二重转换和我之前的例程的区别就是多显示了Java的UTF16内码吧? \uFFFD 再编码就会变成 0x3f是看了你的例程才意识到的. 不过其他的UTF8->GBK->UTF8转换过程不是在program-in-chinese/junit4_in_chinese#12 (comment) 的第二张表里写过了吗?

@nobodxbodon
Copy link
Member Author

@azige

不好意思忘说了, 我之前好像找到了它按照UTF8读取properties文件的地方, 详见checkstyle/checkstyle@702a1a9#commitcomment-24945064

我之前就是看到他这个 Logger 然后确认他是输出了 UTF-8 字节流的。

我指的是这里https://github.com/checkstyle/checkstyle/blob/master/src/main/java/com/puppycrawl/tools/checkstyle/api/LocalizedMessage.java#L522

@azige
Copy link

azige commented Oct 16, 2017

呃抱歉忘记看那边了,那看起来问题已经能确认了?

  1. checkstyle 的 Logger 将"开始检查……"以 UTF-8 编码输出字节流A
  2. 为了向 maven 日志输出,把字节流A使用默认字符集(GBK)转回字符,输出字符流B
  3. maven 日志向控制台输出字符流B,使用默认字符集(GBK)编码为字节流C
  4. 一个设置为使用 UTF-8 解码的控制台解码字节流C并获得了"▒?始检查▒?▒▒??"

@azige
Copy link

azige commented Oct 16, 2017

我指的是这里https://github.com/checkstyle/checkstyle/blob/master/src/main/java/com/puppycrawl/tools/checkstyle/api/LocalizedMessage.java#L522

抱歉这部分没注意看,但是记得 properties 文件按 Java 的规范的是只使用 iso-8859-1,此字符集以外的字符都要转义。而且这里跟日志输出乱码关系应该不是很大吧

@nobodxbodon
Copy link
Member Author

@azige 嗯应该是这个1-4过程.

抱歉这部分没注意看,但是记得 properties 文件按 Java 的规范的是只使用 iso-8859-1,此字符集以外的字符都要转义。而且这里跟日志输出乱码关系应该不是很大吧

那个链接针对的是checkstyle作者的回复里提到的"all resource files (messages) are encoded in UTF8." 我也觉得文件编码和Logger里的输出编码没有关系, 那个回复只是想向作者确认一下. 现在感觉只有写个对应的单元测试才能更有说服力了.

@nobodxbodon
Copy link
Member Author

@azige checkstyle开发组已开始对这个issue进行深入: checkstyle/checkstyle#3569 (comment) 有个问题, 中文windows的系统编码默认是GBK吗? 手边没有中文版windows来确认. 看到这个Windows为什么用GBK而不是UTF-8?, 其中提到windows内码是UTF-16. 那么这个issue的根源其实是, 中文windows命令行默认使用的编码格式是GBK吗?

@nobodxbodon
Copy link
Member Author

客观条件限制, 暂不深究. 如发现此确为冰山一角, 再作投入.

@nobodxbodon nobodxbodon mentioned this issue Jan 28, 2019
61 tasks
@yusiwen
Copy link

yusiwen commented Jan 6, 2023

Update 2023:
目前还是没有很好的办法解决中文乱码的问题,Windows系统里全局unicode支持仍是beta状态,实在不想打开这个功能。

我规避这个问题的办法是设置checkstyle的默认locale到en_US,让它输出的日志都是英文。在checkstyle.xml中配置:

<module name="Checker">
    <property name="localeLanguage" value="en" />
    <property name="localeCountry" value="US" />
....

@xgongya
Copy link

xgongya commented Jan 6, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants