Java 类冲突问题及其解决方案

本文讨论 Java 类冲突问题的背景、解决方案,以及排查类冲突问题的小技巧。

最后修改于:

引子

在启动 Java 项时,可能会出现这种诡异的现象:

  • 在本地可以启动,但部署到服务器上却不能启动;
  • 或者,在服务器可以启动,但本地不可以启动;

此时如果去查看错误日志,往往可以看到诸如 ClassNotFoundException、NoClassDefFoundError 等异常信息,接着很容易就会想到很可能是发生了类冲突。 然后你会结合异常日志即依赖变更情况,定位到冲突的类及其所在的 jar 包,然后用 dependecy-exclude 等手段将冲突解决掉,最终使得服务可以正常启动。一般我们处理问题就到此为止了。 本文试图更近一步,深入分析本地与远程服务器行为不一致的原因,并简单介绍业界解决依赖冲突的一些方法,和排查类冲突问题的小技巧

背景知识

Java Classpath: image1

本地启动的 Classpath 顺序

本地一般都是在 IDEA 中启动;IDEA 启动服务时,会在 Console 打印启动参数: image2

那么 IDEA 是以怎样的顺序将 jar 包添加到 classpath 中呢?

答案:是按 Maven 依赖解析的方式进行加载的。通过执行 mvn dependency:list 可以看到 Maven 依赖解析的结果,这个顺序,就是 classpath 中 jar 的添加顺序: image3

通过对比可以发现,两者顺序一致。

远程启动的 Classpath 顺序

登录服务器,使用 ps aux | grep java 查看启动参数,会发现,远程服务器没有加 -classpath 参数!!!相关的依赖在哪里??? image4

实际上,在构建项目时,Maven 会将所有的依赖都添加到 xx-server.jar 中的 BOOT-INF/lib 目录下。

JVM 在启动时,会根据 Jar 包中的另一个元数据文件–META-INF/MANIFEST.MF,来决定类的加载顺序。

META-INF/MANIFEST.MF

image5

默认情况下,JVM 在加载类的顺序依赖 OS 时,对于 Linux 来说,最底层是 opendir 函数,这个函数返回的顺序,又与文件系统有关。『对于 CentOS 6,它使用的是Ext4,文件顺序与目录文件的大小是否超过一个磁盘块和文件系统计算的Hash值有关。』

简单说,先加载完全是哪个看运气!远程服务器的版本不同,加载的顺序就可能不一样。这就是文章开头诡异问题的根源。

看运气怎么行?!

spring-boot-maven-plugin

这个插件使用了 The Executable Jar Format,支持一种称之为 Classpath Index 的索引文件,负责指定 jar 被添加到 classpath 中的顺序。很明显,通过这个插件可以保证 jar 的扫描顺序在不同的环境下是一致的。完美解决上面的问题。 image6

小节

  • 类的加载顺序取决于 classpath;
  • 嵌套的 JARS 中的加载顺序,在默认情况取决于 OS,对于 Linux 来说取决于文件系统;
  • Spring Boot 提供了一个插件,利用 The Executable Jar Format 完美解决了加载顺序错乱问题;

其它

业界的一些其他解决方案

Spring Boot 的方案可以解决拥有 main 方法的服务的冲突问题,解决方法依赖一个 Maven 差距。

对于开发或扩展类库(比如 guava/hadoop),或者 Java Agent 的开发时,有时候面临的问题更复杂,甚至要解决 classloader 冲突的问题。

maven-plugin-shade

maven-plugin-shade 详解

自定义类加载器

Java Agent 的类加载隔离实现

思路:『使用独立的类加载器去加载 Java Agent 依赖的类,该独立的类加载器的 parent 指向 Bootstrap ClassLoader,且将 Java Agent 依赖的类的默认后缀 .class 进行调整,以避免系统类加载器加载到这些类,以实现类的隔离。』

类冲突问题排查小技巧

加启动参数 -verbose:classpath

image7

加上这个参数, JVM 会将『哪个类是从哪个 jar中被加载』的信息输出到 console 中。

这个方法需要你能控制启动参数,适合在本地,不确定哪个类冲突的时候使用。

使用 arthas 的 Jad 功能

image8

arthas 输出的信息更全面,出了现实加载的位置,还会告诉你 ClassLoader 是哪一个,并且自动反编译(jad 命令本来就是干这个的!)

这个方法需要你安装 arthas,适合本地和 dev、test 环境,在你有了明确的怀疑对象时,优先使用这个命令。

如何安装 arthas

1
2
3
4
# https://arthas.aliyun.com/doc/install-detail.html

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
本文总阅读量 次 本文总访客量 人 本站总访问量 次 本站总访客数
发表了20篇文章 · 总计32.36k字
本博客已稳定运行
使用 Hugo 构建
主题 StackJimmy 设计