你这是踩到矿坑了。

人生中,第一次,YDJSIR遇到了跨平台开发的困难。

众所周知,JAVA是一个跨平台的语言。各式各样的JVM,让JAVA顺利运行在10亿设备上。然而,其“跨平台性”真的是无可挑剔的吗?

由于本人才疏学浅,下面只能对现象加以描述,而无法分析具体原因。如果有人能够帮YDJSIR解释其原因,YDJSIR将万分感激。

下面举了一个例子。

相关链接:https://github.com/XZ-X/2020SE1-FAQ/issues/13

本问题的高级形态: https://zhuanlan.zhihu.com/p/46294360

问题描述

预期结果

当然是AC啦!YDJSIR都整了两天了!

实际结果

远端OJ永远返回运行超时。分数为0分

代码分析

让YDJSIR们先来看下这段代码。

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//判断所给字符串是否是Linux下的合法目录相对路径:是否是由/连接起来的的只由数字、字母、下划线组成的字符串的集合(这里当然是理想化的情况,有一些额外限定,下同)
String patternDir = "^(\\w+\\/?)+$";

//判断所给字符串是否是Linux下合法的zip/ZIP/jar/JAR文件相对路径
String patternArchive = "^(\\w+\\/?)+(.)+((ZIP)|(zip)|(JAR)|(jar))+$";

//是否是含有通配符的目录相对路径
String patternWildEntry = "^(\\w+\\/?)+\\*";


//多么优美的正则表达式啊!那就把他们投入使用吧!
//Composite是以上三种路径连起来的集合,以PATH_SEPARATOR分割
boolean isMatchComposite = false;
String[] composite = classpath.split(PATH_SEPARATOR);
if(composite.length > 1) {
for (String item : composite
) {
if (Pattern.matches(patternArchive, classpath) || Pattern.matches(patternWildEntry, classpath) ||
Pattern.matches(patternDir, item)) {
continue;
}
isMatchComposite = true;
}
}
if(isMatchComposite){
return new CompositeEntry(classpath);
}
else if(Pattern.matches(patternArchive, classpath)){
return new ArchivedEntry(classpath);
}
else if(Pattern.matches(patternWildEntry, classpath)){
return new WildEntry(classpath);
}
else if(Pattern.matches(patternDir, classpath)){
return new DirEntry(classpath);
}
return null;

似乎没什么问题。写到这里,YDJSIR还狠狠地嘲讽了面向用例编程的YDJSIR的舍友(别打YDJSIR)

测试结果

如果你觉得以下具体内容没有意思,可以直接到最后看表格。

本地测试结果-Windows 10 x64

YDJSIR愉快地在本地跑了一下测试。

Windows10版本

image-20200521090551533

本地JDK版本
1
2
3
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_242-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.242-b08, mixed mode)
IDEA测试结果

这是IDEA运行测试(Junit4)的结果。很好,94ms,全绿。

mvn test运行结果
1
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.133 sec

本处省略大量e.printbacktrace()的内容。

1
2
3
4
5
6
7
8
9
10
11
Results :

Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13.387 s
[INFO] Finished at: 2020-05-17T13:12:00+08:00
[INFO] ------------------------------------------------------------------------

YDJSIR还是不放心,找了另外一个大佬来测试(感谢czgdl)

Linux AdoptOpenJDK 1.8 测试结果

image-20200522112750868

118ms,很好。YDJSIR大喜。

此实验在64位的Arch下进行。

Oracle JDK8测试结果

JDK版本
1
jdk1.8.0_251
IDEA测试结果

image-20200522112324100

这个时间差距……

?好像有什么不对?

好像有点大。不管了。

mvn test测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running edu.nju.ClassFileReaderTest
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 81.642 sec

Results :

Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:23 min
[INFO] Finished at: 2020-05-22T11:23:02+08:00
[INFO] ------------------------------------------------------------------------

很好。YDJSIR决定把YDJSIR的答案Push到OJ上。

于是,YDJSIR收获了这个。

服务器-CentOS x64测试结果

CentOS

image-20200521161941359

JDK版本
1
jdk-8u161-linux-x64
OJ-mvn test 运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=utf-8
Picked up _JAVA_OPTIONS: -Djdk.net.URLClassPath.disableClassPathURLCheck=true
Running edu.nju.ClassFileReaderTest
Cancelling nested steps due to timeout
Sending interrupt signal to process
Terminated

Results :

Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

script returned exit code 143
Timeout has been exceeded

根据助教的解释,服务器上面给每一条OJ题的时间是给足1分钟。YDJSIR本地i5-9300H他再强,也和服务器的性能间差不到一个数量级的。94ms和1min,这明显不合理。问题是,这不是编译的错误,这就是这个问题的关键所在。如果是变异错误,那YDJSIR改就是了,问题是它不是!

于是YDJSIR提出了Issue。JVVM作业助教首先认为是Maven包的问题,让YDJSIR运行mvn test。YDJSIR一开始由于网络问题缺了包,然而把包补齐之后,提交到OJ上面一样无法通过。

而后,平台助教的反馈是因为系统没有部署/构建成功,根本无法判断是哪个测试没有通过。贴心的平台助教给了YDJSIR下图。这是YDJSIR完成正则表达式相关判定前的提交内容,并据此推测是有关CompositeWildcard 有关文件通配符时的读写问题。

与此同时,还有两句极为现实的话。

有人已经能AC了,总不能为了你一个人改动OJ吧?

实在不行,只能让助教单独来跑一次然后给分了。

于是,此时的YDJSIR的心情是这样的。

image-20200521163633986

于是YDJSIR在本地陷入无限的debug中,但是根本没问题。

image-20200521140229918

MAC-测试结果

MAC环境

img

本地JDK环境
1
jdk-8u251-macosx-x64
IDEA测试结果

image-20200521163300684

MVN测试结果

足足过了20s都没有跑出结果,测试终止。

盲猜结局和上面的1分多钟是一样的。

YDJSIR此时的内心是这样的。

image-20200521163358536

解决方案

初级版:放弃正则表达式

这段代码来自YDJSIR的舍友。

1
2
3
4
5
6
7
8
9
10
String substring = classpath.substring(classpath.length() - 4, classpath.length());
if(classpath.contains(PATH_SEPARATOR)){
return new CompositeEntry(classpath);
}
else if(classpath.charAt(classpath.length()-1)=='*')
return new WildEntry(classpath);
else if(substring.equals(".JAR")|| substring.equals(".jar")|| substring.equals(".zip")|| substring.equals(".ZIP"))
return new ArchivedEntry(classpath);
else
return new DirEntry(classpath);

你看着代码,就看下尾巴,多么不正式!要是有奇怪的字符混进来了呢?img

后来,YDJSIR一怒之下换掉了YDJSIR辛辛苦苦写的正则模块,想着赌一把,然后本地当然过了,看着39ms这个时间,YDJSIR陷入沉思。

本地IDEA运行结果

image-20200521154828526

管他呢,先提交完AC再说!分数重要!

下面是提交结果。

OJ运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
T E S T S
-------------------------------------------------------
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=utf-8
Picked up _JAVA_OPTIONS: -Djdk.net.URLClassPath.disableClassPathURLCheck=true
Running edu.nju.ClassFileReaderTest
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.215 sec - in edu.nju.ClassFileReaderTest

Results :

Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.471 s
[INFO] Finished at: 2020-05-17T07:39:24+00:00
[INFO] Final Memory: 15M/36M
[INFO] ------------------------------------------------------------------------

进阶版:修正正则表达式

AC之后,YDJSIR倒过头来反复研读南京大学软件所的那篇知乎专栏(链接放在开头),似乎发现了什么。于是,YDJSIR的正则表达式变成了下面这样。

1
2
3
String patternDir = "^(\\w+\\/?)+$";
String patternArchive = "^(\\w+\\/?)+(\\.)+((ZIP)|(zip)|(JAR)|(jar))$";
String patternWildEntry = "^(\\w+\\/?)+\\*$";
本地IDEA运行结果

好了,按照惯例,push到OJ上。

OJ运行结果

顺利AC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 T E S T S
-------------------------------------------------------
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=utf-8
Picked up _JAVA_OPTIONS: -Djdk.net.URLClassPath.disableClassPathURLCheck=true
Running edu.nju.ClassFileReaderTest
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 16.383 sec - in edu.nju.ClassFileReaderTest

Results :

Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22.397 s
[INFO] Finished at: 2020-05-21T07:41:27+00:00
[INFO] Final Memory: 14M/36M
[INFO] ------------------------------------------------------------------------

问题出在何处?

. 匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \. 。
https://www.runoob.com/regexp/regexp-syntax.html

image-20200521154503284

这个时间上的差距……好吧还是很可怕。

问题总结

不同平台结果汇总

运行环境 修改前 修改后
YDJSIR-IDEA-OpenJDK 1.8.0_242-b08 Win_x64(存疑) 94ms 58ms
YDJSIR-MVN-OpenJDK 1.8.0_242-b08 Win_x64 133ms 201ms
YDJSIR-IDEA-OracleJDK jdk-1.8.0_251 Win_x64 81sec 14sec
YDJSIR-MVN-OracleJDK jdk-1.8.0_251 Win_x64 81sec 15sec
YDJSIR -IDEA-OracleJDK jdk-14.0.1 Win_x64 115ms 106ms
YDJSIR -IDEA-OracleJDK jdk-14.0.1 Win_x64 123ms 73ms
MAC-IDEA-OracleJDK jdk-1.8.0_251 Mac_x64 ==超时== 17sec
CZG VM-MVN-OpenJDK 1.8.0_242-b08 Linux_x64(Arch) 118ms ==没有测试==
OJ ECS-MVN-OracleJDK jdk-1.8.0_251 Linux_x64(CentOS 7.7) ==超时== 16s
CX MAC-IDEA-OracleJDK jdk-11 Mac_x64 90ms 40ms
CX MAC-MVN-OracleJDK jdk-11 Mac_x64 180ms 107ms

此处的超时指的是运行时间超过1min。

  • 不同JDK对于同样的语法的实现是不一样的,因而对语言特性的应用应极为谨慎。即使都是JDK 1.8,OpenJDK的实现与Oracle JDK的实现可能是完全不一样的。从58ms到16秒,将近两个数量级的差距足以造成巨大的混乱。然而,除非生产环境和实际环境的配置相同,这样的问题极难发现;
  • 小的错误,不是Critical的错误可能会导致极大的混乱。一个没有转义的.,在OpenJDK中仅仅带来了从58ms到94ms的变化,然而在OracleJDK中,却带来了从16s到81s的巨大差异,其威力不可小觑。诚然,即使不用正则,剩下的业务逻辑也要消耗38ms,这样看来,前者耗时增加了近2.8倍,而而后者将近5倍。
  • 在开发中尽量选择更新的技术是有意义的。JAVA 11同样是LTS版本,虽然旧的版本可能资料多些,踩过坑的人多些,但是新技术在底层做出的改进是应该被认可的。

image-20200522113959911

本土来自英文维基

题外话

YDJSIR看到有些资料表示IDEA可能一开始使用了自带的JDK进行测试,而非mvn test 进行测试。YDJSIR认为这个说法是有道理的。因为事实上,YDJSIR一开始IDEA本地全过的时候,在IDEA里面调用Maven运行mvn test甚至是会提示少了资源包的,需要YDJSIR用魔法解决。在某舍友的MAC上运行时,出现了IDEA全过,mvn test提示不支持的情况。期间的共性,就是安装从IDEA开始,从未更改SDK相关设置,全部走默认。然而YDJSIR在IDEA官方文档得出的结果却是自带JDK仅仅用于IDE自身运行,并非用于开发,你需要自行设置JDK。

参考资料:https://www.jetbrains.com/help/idea/sdk.html#

1
2
3
4
Important notice
The bundled JRE is used for running the IDE itself, and it's not sufficient for developing Java applications. Before you start developing in Java, download and install a standalone JDK build.

Due to the changes in the Oracle Java License, you might not have the rights to use Oracle's Java SE for free. We recommend that you use one of the OpenJDK builds to avoid potential compliance failures.

YDJSIR不清楚该如何理解上面那段话。

这个链接里面同样提到类似的问题。

参考资料:https://intellij-support.jetbrains.com/hc/en-us/community/posts/360006922240-Do-I-have-to-install-JDK-

实验证明,IDEA会默认用自己的JDK(基于OpenJDK,叫做JBR)来编译并运行程序。YDJSIR设置了自己的JDK之后,没有找到只使用自带JDK的方法。现在YDJSIR为了减少类似情况,在大作业中选择Oracle JDK来完成YDJSIR的大作业(DDL啊啊啊啊)

1
2
JetBrains Runtime is a runtime environment for running IntelliJ Platform based products on Windows, Mac OS X, and Linux. JetBrains Runtime is based on OpenJDK project with some modifications. These modifications include: Subpixel Anti-Aliasing, enhanced font rendering on Linux, HiDPI support, ligatures, some fixes for native crashes not presented in official builds, and other small enhancements. 

这段话摘自JBR官网。

YDJSIR才疏学浅,无力解决上述问题。为了辅助参考,这里给出本地IDEA版本相关信息。

image-20200522115930944

补充资料:https://www.quora.com/As-IntelliJ-has-its-own-embedded-JDK-can-it-run-Java-programs-without-having-any-Java-installed-elsewhere-on-the-system

结语

YDJSIR在这一系列的折腾后,苦思冥想,大致地得出了以下结论:

  • 在编程中,任何键盘上能直接敲出来的字符都应该被小心对待!哪怕是小键盘,也可能有114514种奇怪的功能!
  • 正则表达式是个大坑,一不小心就会掉进去;
  • 面向用例编程有时候是刷分的好技巧。然而在可能的情况下,YDJSIR还是愿意折腾一下的;
  • 学艺不精是很危险的。比如说打漏转义字符符号的YDJSIR直接导致了服务器上运行的超时。这要是在生产环境中,可能会造成灾难性的后果;
  • YDJSIR学了这么久,感觉编程的能力没有多大提高 但是膜神卖弱吹水能力大增 ,但是Debug的经验的确变多了。对于断点的重要性,分部寻找出问题的点的能力也有所提高。YDJSIR要争取尽快摆脱全靠print这样的低级手段Debug,而是更多利用IDE的特性。