antlr java.lang.NoSuchFieldError 错误分析报告

一个平时在Clojure REPL工作的非常正常的函数, 今天突然无法正常工作了, 抛出如下异常.

clojure repl nosuchfielderror exception

但是我没有更改任何设置, 也没有修改任何代码, 连jar都没有更新, 同一个jar文件, 之前工作正常, 突然就抛出异常. 完全想不到原因. 而且这个问题时有时无, 上午没问题, 下午重启又有了, 而且同一套系统在Surface上工作正常, 拷贝到PC上之后又出问题.

第一个想法是先Google一下, 事后证明这是错误的做法, 且不说Google的结果很少, 只有两三页, 更在于这种问题, 只有知道原因, Google的搜索结果才有意义, 但是我之所以要Google, 原本就是因为不知道原因. 事后来看, 实际上首先应该定位到出问题的代码所在的地方, 即StackTrace的栈底.

Google没有找到答案, 我又注意到, Surface里面用的是JAVA 1.8, PC上的是JAVA 1.7. 难到是JDK版本的问题. 于是着手在PC上安装JDK 1.8. 结果竟然装不了, 这下更怀疑是系统的哪个地方出了问题. 最后不得已, 只好重装系统. 重装一次系统然后再次恢复到顺畅可用的状态是很费劲的事情.

装完系统之后赶紧安装JDK 1.8, 之后果然正常了. 但重启之后问题再次出现, 然后卸载JDK再安装一遍, 问题依旧. 说明和JDK完全没关系, 和操作系统没有关系, 原本可以不用重装系统的. 并且这个时候, Surface上也观察到了同样的现象. 而Surface的操作系统很正常.

像这种问题, 最头疼是没有思路, 不知道问题的原因. 类似这样的问题也经常出现在Servlet的开发中, 例如毫无原因的返回500错误或者404错误. 这种情况下往往会陷入无头苍蝇般的状态中, 折腾的自己精疲力竭还是解决不了问题, 总之是令人沮丧.

最后只能直接检查代码了, 还好系统上有项目的源码, 定位到CSSLexer.java 289行, EOF_TOKEN, 按F3, 显示这是antlr-runtime-3.1.jar中的org.antlr.runtime.Token类提供的. 而且在Eclipse的控制台中执行是正常的. 在命令行用java执行也没问题, 唯一有问题的是在Clojure REPL所在的JVM中执行.

通过加上-verbose:class的参数, 可以查到在Eclipse和命令行中执行的时候class文件时从哪个jar加载的, 结果的确是加载了antlr-runtime-3.1.jar.

 
java -verbose:class -cp ../nc/*;../nc/jarlib/*; test.DraftingTest
 

可以看到加载了antlr-runtime-3.1.jar中的class

 
[Loaded org.antlr.runtime.IntStream from file:/C:/workspace/nc/jarlib/antlr-runtime-3.1.jar]
[Loaded org.antlr.runtime.TokenStream from file:/C:/workspace/nc/jarlib/antlr-runtime-3.1.jar]
[Loaded org.antlr.runtime.tree.TreeNodeStream from file:/C:/workspace/nc/jarlib/antlr-runtime-3.1.jar]
[Loaded org.antlr.runtime.CharStream from file:/C:/workspace/nc/jarlib/antlr-runtime-3.1.jar]
[Loaded org.antlr.runtime.TokenSource from file:/C:/workspace/nc/jarlib/antlr-runtime-3.1.jar]
 

现在的问题是, Clojure REPL所在的JVM加载的是哪一个JAR? 如果不是antlr-runtime-3.1.jar, 会是什么? 因为查遍了所有可能的classpath, 都只存在一个antlr-runtime-3.1.jar.

同样的, 给REPL的启动脚本加上verbose参数

 
java -server -verbose:class -cp .;./*;%CLOJURE_JAR% clojure.main -i %bindir%\init.clj -r
 

到REPL中执行有问题的部分代码, 显示

 
user=> (test.DraftingTest/getList "h1.entry-title a {}"  "http://hackingthevalley.com/")
...
...
[Loaded org.antlr.runtime.IntStream from file:/D:/bin/antlr-3.5-complete-no-st3.jar]
[Loaded org.antlr.runtime.TokenStream from file:/D:/bin/antlr-3.5-complete-no-st3.jar]
[Loaded org.antlr.runtime.tree.TreeNodeStream from file:/D:/bin/antlr-3.5-complete-no-st3.jar]
[Loaded org.antlr.runtime.CharStream from file:/D:/bin/antlr-3.5-complete-no-st3.jar]
[Loaded org.antlr.runtime.TokenSource from file:/D:/bin/antlr-3.5-complete-no-st3.jar]
...
...
 

原来实际使用的jar是D:/bin/antlr-3.5-complete-no-st3.jar. 虽然这个目录根本没有在classpath中, 但是classpath的第一个路径是当前目录

 
.;
 

因此如果我们启动JVM的时候, 当前目录是D:/bin的话, 相当于把当前目录也加入到classpath里面了. 而这个目录恰好有一个antlr 3.5, 而且靠前, 同样包含org.antlr.runtime这个package.

这种情况下当JVM的classloader碰到

 
import org.antlr.runtime.*;
 

的时候, 就会判断出需要的jar是antlr-3.5-complete-no-st3.jar. 而同时antlr3.5有一个改动, 就是EOF_TOKEN这个常量被去掉了.

 antlr 3.5 removed EOF_TOKEN

这就是最根本的原因了, antlr library的高版本jar文件被classloader提前加载了, 低版本的被覆盖了, 而我们的代码又引用了一个只有低版本中存在的field.

总结

如果出现NoSuchFieldError这样的错误, 绝大部分情况都会是JVM加载了错误的JAR, 即某个引用的field在加载的jar中不存在, 此时首先应该定位到所引用的这个field是哪个jar的哪个class提供的, 在这个例子中, 引用的field是antlr-runtime-3.1.jar的org.antlr.runtime.Token class的成员EOF_TOKEN, 然后分析这个class为什么没有加载. 最好的办法是打开java的-verbose:class.

如果是Eclipse, 找到 Preferences -> Java -> Installed JRE -> Edit -> Default VM Arguments 加上

 
-verbose:class
 

正如Windows系统上有dll hell, JAVA平台同样有jar hell的问题, 安装新版本的组件导致旧有的系统无法工作的情况比比皆是. 这是二进制软件分发系统的通病.

另外, 类似于这样的问题, StackTrace提供的信息实在太少了, 例如本文中的例子, 只是说EOF_TOKEN这个field没有找到, 而通常不会告诉你是哪个jar出了问题. 这就需要对JVM的classloader有相当程度的了解.