引子
在启动 Java 项时,可能会出现这种诡异的现象:
- 在本地可以启动,但部署到服务器上却不能启动;
- 或者,在服务器可以启动,但本地不可以启动;
此时如果去查看错误日志,往往可以看到诸如 ClassNotFoundException、NoClassDefFoundError 等异常信息,接着很容易就会想到很可能是发生了类冲突。 然后你会结合异常日志即依赖变更情况,定位到冲突的类及其所在的 jar 包,然后用 dependecy-exclude 等手段将冲突解决掉,最终使得服务可以正常启动。一般我们处理问题就到此为止了。 本文试图更近一步,深入分析本地与远程服务器行为不一致的原因,并简单介绍业界解决依赖冲突的一些方法,和排查类冲突问题的小技巧。
背景知识
Java Classpath:
本地启动的 Classpath 顺序
本地一般都是在 IDEA 中启动;IDEA 启动服务时,会在 Console 打印启动参数:
那么 IDEA 是以怎样的顺序将 jar 包添加到 classpath 中呢?
答案:是按 Maven 依赖解析的方式进行加载的。通过执行 mvn dependency:list
可以看到 Maven 依赖解析的结果,这个顺序,就是 classpath 中 jar 的添加顺序:
通过对比可以发现,两者顺序一致。
远程启动的 Classpath 顺序
登录服务器,使用 ps aux | grep java
查看启动参数,会发现,远程服务器没有加 -classpath
参数!!!相关的依赖在哪里???
实际上,在构建项目时,Maven 会将所有的依赖都添加到 xx-server.jar 中的 BOOT-INF/lib 目录下。
JVM 在启动时,会根据 Jar 包中的另一个元数据文件–META-INF/MANIFEST.MF
,来决定类的加载顺序。
META-INF/MANIFEST.MF
默认情况下,JVM 在加载类的顺序依赖 OS 时,对于 Linux 来说,最底层是 opendir 函数,这个函数返回的顺序,又与文件系统有关。『对于 CentOS 6,它使用的是Ext4,文件顺序与目录文件的大小是否超过一个磁盘块和文件系统计算的Hash值有关。』
简单说,先加载完全是哪个看运气!远程服务器的版本不同,加载的顺序就可能不一样。这就是文章开头诡异问题的根源。
看运气怎么行?!
spring-boot-maven-plugin
这个插件使用了 The Executable Jar Format,支持一种称之为 Classpath Index 的索引文件,负责指定 jar 被添加到 classpath 中的顺序。很明显,通过这个插件可以保证 jar 的扫描顺序在不同的环境下是一致的。完美解决上面的问题。
小节
- 类的加载顺序取决于 classpath;
- 嵌套的 JARS 中的加载顺序,在默认情况取决于 OS,对于 Linux 来说取决于文件系统;
- Spring Boot 提供了一个插件,利用 The Executable Jar Format 完美解决了加载顺序错乱问题;
其它
业界的一些其他解决方案
Spring Boot 的方案可以解决拥有 main 方法的服务的冲突问题,解决方法依赖一个 Maven 差距。
对于开发或扩展类库(比如 guava/hadoop),或者 Java Agent 的开发时,有时候面临的问题更复杂,甚至要解决 classloader 冲突的问题。
maven-plugin-shade
自定义类加载器
思路:『使用独立的类加载器去加载 Java Agent 依赖的类,该独立的类加载器的 parent 指向 Bootstrap ClassLoader,且将 Java Agent 依赖的类的默认后缀 .class 进行调整,以避免系统类加载器加载到这些类,以实现类的隔离。』
类冲突问题排查小技巧
加启动参数 -verbose:classpath
加上这个参数, JVM 会将『哪个类是从哪个 jar中被加载』的信息输出到 console 中。
这个方法需要你能控制启动参数,适合在本地,不确定哪个类冲突的时候使用。
使用 arthas 的 Jad 功能
arthas 输出的信息更全面,出了现实加载的位置,还会告诉你 ClassLoader 是哪一个,并且自动反编译(jad 命令本来就是干这个的!)
这个方法需要你安装 arthas,适合本地和 dev、test 环境,在你有了明确的怀疑对象时,优先使用这个命令。
如何安装 arthas
|
|