笔者在个人电脑上使用VSCode开发Java的时间并不多,当时脑子抽抽了用了VSCode加插件的方式刷力扣题。后续在使用Maven管理项目依赖时出现了编译通过但VSCode运行出现java.lang.NoClassDefFoundError的错误。故使用本文来记录该问题的现象、调查过程及其解决方案。
笔者想用通过Maven依赖的第三方库的JOL(Java Object Layout)来查看Java虚拟机里的对象内存分布。代码如下:
上述代码运行时呢则会出现NoClassDefFoundError错误。
也就是所谓的虽然编译通过了,但是在运行时Class Loader找不到指定类。这就很匪夷所思了,以前在别的IDE Maven项目可从来没出现过这种问题。
pom.xml依赖项,可以看到并没有指定scope,也就是默认的compile级别。可以排除是pom的问题。
既然pom没有问题,那么运行时找不到三方库里的类就是Maven本地库的jar包或者运行时classpath出了问题导致找不到。首先我们检查Maven本地库jar包是否有问题。
我们在maven的本地库里找到了对应jar包可以排除是Maven本地库出了问题。
此时笔者只能怀疑是VSCode调用Java传参时出了问题。VSCode的调用命令如下:
PS C:\myEP\GithubRepos\Demonstration> c:; cd 'c:\myEP\GithubRepos\Demonstration'; & 'C:\Program Files\Java\jdk11.0.16_8\bin\java.exe' '@C:\Users\虎猫儿\AppData\Local\Temp\cp_564pls627a8vasz6pulnt3q6z.argfile' 'per.eicho.demo.sdk.Test'
传递给java.exe的参数呢有两个
后者是我们的Main类,也就是程序入口了。前者呢是一个临时文件路径前面加个@,这是个什么玩意儿呢?
笔者的环境是OpenJDK11,通过java --help就可以看到@argument这部分是通过文件来传递参数给jvm。
PS C:\WINDOWS\system32> java --help
...省略...
@argument 文件
一个或多个包含选项的参数文件
...省略...
jvm的调用者可以通过 ‘@filePath’ 的方式来实现通过文件给jvm传递参数。
调查到这个阶段,必须要找到临时参数文件,检查其内容来确定是否是VSCode生成参数文件时出了问题。
那么可以看到临时参数文件里呢,确实是包含了需要传递给jvm的classpath信息。这些信息也没有任何问题,因为你可以通过直接传参的方式来验证。
PS C:\WINDOWS\system32> java -cp "C:\\myEP\\GithubRepos\\Demonstration\\Demo\\test\\target\\classes;C:\\Users\\虎猫儿\\.m2\\repository\\org\\openjdk\\jol\\jol-core\\0.16\\jol-core-0.16.jar" per.eicho.demo.sdk.Test
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 0x0000000800000000 base address and 0-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
class org.openjdk.jol.vm.VM
PS C:\WINDOWS\system32>
相反如果笔者也使用文件传参会如何呢?笔者复制了临时传参文件并更名为options.txt。
PS C:\WINDOWS\system32> java '@C:\Users\虎猫儿\Desktop\options.txt' per.eicho.demo.sdk.Test
Exception in thread "main" java.lang.NoClassDefFoundError: org/openjdk/jol/vm/VM
at per.eicho.demo.sdk.Test.main(Test.java:7)
Caused by: java.lang.ClassNotFoundException: org.openjdk.jol.vm.VM
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
... 1 more
PS C:\WINDOWS\system32>
结果已经很明朗了,文件传参出了问题,JVM不能很好地识别文本文件的内容。
java里,可以通过下列代码来获取并打印jvm的classpath值来检查。
System.out.println(System.getProperty("java.class.path"));
然后我们得到了如下结果。
C:\myEP\GithubRepos\Demonstration\Demo\test\target\classes;C:\Users\铏庣尗鍎縗.m2\repository\org\openjdk\jol\jol-core\0.16\jol-core-0.16.jar
好家伙,用户名直接乱码了,这就是导致class loader不能正确加载三方库类的原因。笔者也测试过直传参数的方式,其可以正确传递中文路径给jvm。
根据上面的调查呢,我们可以确定有两个原因导致VSCode运行Maven项目时出现java.lang.NoClassDefFoundError错误。
第一个是根本原因,是在于 HotSpot JVM 11(注意供应商和版本) 不能正确从UTF-8文本文件里读取带中文路径所导致的,能支持中文路径(直传参〇)但又不完全支持(文件传参×),这是OpenJDK的问题。
第二个呢是间接原因,是由于VSCode的Java开发套件使用了文件传参这种方式来运行Java,并且测试用例没有做中/日文所导致,笔者目前使用过的其他IDE则没有遇到过这种问题。
问题原因找到了,那么如何解决呢?其实也很简单,既然是对包含中文字符的文件的支持出了问题,那么很容易想到两个临时性的解决方案。
临时性解决方案在难以改变外部环境时,如让VSCode的Java开发套件改为直传参的方式,或者让OpenJDK修复此bug再更换为新的修复过bug的JDK版本。这些都是成本极高或者难以实现的,因此我们可以妥协。
更改本地库的路径使其避免出现中文对于笔者来说是成本最低的临时性解决方案,所以笔者目前是通过显式设置Maven的Setting.xml文件里的localRepository使其变更到一个没有中文的路径去。甚至如果不想重新下载,你可以直接拷贝.m2下的repository文件夹到你设置的新路径去,这实在是太廉价了。
临时性解决方案只能治标不能治本。一旦Maven之外,比如项目路径中不小心出现中文也会导致相同的问题。要从根本上解决这个问题,只有寄希望于两点
第二点让笔者想起一件事,在开源的GlassFish服务器里,有个BUG直到笔者2021年调查之时一直没修。而基于GlassFish源代码开发的商用版服务器早在2016年就修复了这个BUG的。GlassFish的定位并不是一个商用Java服务器,其定位就是 为商业版的开发提供参考。OpenJDK也类似,也许在商业版本的JDK里这个问题已经得到了解决。不过限于个人资源问题,笔者这里仅提供思路,验证就交给各位读者自己了。
VSCode+插件开发Java的方式毕竟不是主流,没有良好社区支持难免在一些细枝末节上会有纰漏,甚至遇到这个问题并询问的人都很少,这也侧面给了笔者一个教训,选用开源产品时,一定要选社区活跃的产品…
希望本文能帮助读者们理清为什么VSCode运行Maven项目时为什么会出现这个错误,以及如何解决这个问题。
如果未来这个问题依然得不到解决,笔者也只能考虑放弃使用VSCode。