最近给JDK修了一个存在3年的bug。故事的起因是这样的,我们的一个测试在Windows下会莫名其妙地卡住,但是如果稍微把项目文件夹改个名字,增加或者减少一个字符(比如说从gradle
改成gradle1
或者gradl
),这个测试就如丝般顺滑。
俗话说,能重现的bug都是好bug。一番调试之后,发现问题似乎出在JDK本身。
故事要从Argument File说起,这玩意的中文名是啥我也不知道,就不翻译了。大家都知道,Java世界里的一切最终都会变成java
命令行的调用。在Windows上,大概长这样:
java -Xmx2g -cp "C:\\1.jar;C:\\2.jar" Main doSomething
如果你有一个很大的应用,引用了成百上千个jar包,那么你的classpath可能会特别长,长到几十K都装不下。然而,Windows万分悲痛地告诉你,不好意思,我的命令行长度是有限制的,不能超过8191个字符。
你:……
以前,大家对此的解决方案一般是,创建一个空jar包,然后把这个巨型的classpath写到manifest文件里,然后引用这个空jar包即可:https://docs.oracle.com/javase/tutorial/deployment/jar/downman.html 。Bazel就是这么干的。
JDK 9之后,官方给了一个解决方案:Argument Files。
简而言之,你不是有个巨型无敌长的classpath么?不用直接传到命令行里了,先把这个巨型的参数-cp "C:\\1.jar;C:\\2.jar;...;C:\\10000.jar"
写到一个文件里,比如说叫classpath.txt,然后直接用java @classpath.txt ...
就行了,java
命令会自动帮你展开。要注意的是,不仅是classpath,任何被java
接受的参数都可以用这种方式传递。
坏就坏在Argument File的处理上。我一步一步研究上面遇到的那个诡异bug,最终找到了一个可以复现的样例。在特定情况下,Argument File的读取会出现问题。比如说,如果你有一个Argument File,内容是:
-cp ".;C:\\app.jar" Main
我们假定Main类存在于app.jar中,因此一切正常。接下来,我们逐渐向双引号中增加.;
:
-cp ".;.;.;.;.;.;.;.;.;.;.;.;.;.;.;.;.;.;.;.;C:\\app.jar" Main
你会发现,当这个文件增长到一定长度(大约4100字节)时候,突然,java告诉你:
NoClassDefFoundError
怀着困惑的心情,我提交了JDK-8210810。随后,我下载了OpenJDK的源代码,连上宇宙第一IDE,开始了debug之路。
在JDK的实现中,Argument File的处理是一个状态机:
/*
[\n\r] +------------+ +------------+ [\n\r]
+---------+ IN_COMMENT +<------+ | IN_ESCAPE +---------+
| +------------+ | +------------+ |
| [#] ^ |[#] ^ | |
| +----------+ | [\\]| |[^\n\r] |
v | | | v |
+------------+ [^ \t\n\r\f] +------------+['"]> +------------+ |
| FIND_NEXT +-------------->+ IN_TOKEN +-----------+ IN_QUOTE + |
+------------+ +------------+ <[quote]+------------+ |
| ^ | | ^ ^ |
| | [ \t\n\r\f]| [\n\r]| | |[^ \t\n\r\f]v
| +--------------------------+-----------------------+ | +--------------+
| ['"] | | SKIP_LEAD_WS |
+---------------------------------------------------------+ +--------------+
*/
JDK每次从Argument File中读取4096字节的数据到缓冲区中,这个状态机就随着文件中Token的变化进行状态转换。然鹅,在某种特定情况下,这个状态机的某个状态转换会出现问题,这种特定情况是什么呢?
答案是,两个连续的转义字符\\
被缓冲区分割开,即:
找到了原因,修复其实就是一行代码的事:
--- a/src/java.base/share/native/libjli/args.c Fri Sep 28 13:01:28 2018 -0700
+++ b/src/java.base/share/native/libjli/args.c Fri Sep 28 13:15:01 2018 -0700
@@ -263,6 +263,8 @@
}
JLI_List_addSubstring(pctx->parts, anchor, nextc - anchor);
pctx->state = IN_ESCAPE;
+ // anchor after backslash character
+ anchor = nextc + 1;
break;
case '\'':
case '"':
翻了下Mecurial日志,这个问题从2015年起就存在了,因为修bug的时候JDK11刚刚发布,所以按照Oracle的尿性,这个fix不会被backport到10上。Workaround很简单,把所有的反斜杠\
换成/
就行了。
这个故事告诉我们,别迷信JDK,其实他们的开发者和我们一样,也在天天写bug啊。