java类加载中的版本冲突是一个头疼的问题,这里列举几种排查问题以及处理的常用办法
有时候在测试环境下正常的,线上部分机器也是正常,但是有一些机器就会异常,这是啥原因?
因为jar包的加载顺序在不同的机器上面是不同的,一台机器多次重启,基本的顺序不会变,但是新的jar包就会有问题
在不同的机器上,对jar包中类的加载顺序有时候不是完全一致的,例如,在/home/admin/.default/lib 目录下的有个A.jar和b.jar,里面都有个叫SayHello的class,但A的SayHello里的功能(例如他打印的是“去你的”)和b的SayHello功能(例如他打印的是“你好”)不一样,
由于不同机器加载顺序可能不一致,那么就有可能A机器上先加载了A.jar的SayHello类(会打印出“去你的”),而B机器上加载了b.jar的SayHello类(会打印出“你好”)。
这个问题在特定的机器上,其加载顺序基本不变的,因此,一旦测试的时候没问题,那他就很难再重现出此问题,只有部署到线上多台机器上时,问题才可能会暴露出来。
(1)通过maven自带的几个工具来进行分析
1、mvn dependency:list能够列举出项目中的依赖情况
2、mvn dependency:tree打印出依赖出(\-代表行尾) 这个在排除依赖的时候非常有用,通过树状结构来查看jar包的加载情况
3、mvn dependency:analyzer 这个能够分析项目中的依赖情况。
Used undeclared dependencies 是指那些在项目中直接使用到的,但没有在POM中配置的依赖。要注意一点,这个分析是编译主代码和测试代码需要的依赖,运行时需要的没有打印出来。例如该例中可能项目中的一些类有关于spring-context的Java import声明,但spring-context这个依赖实际是通过传递性依赖进入classpath的,这就意味者潜在的风险。一般来说我们对直接依赖的版本变化会比较清楚,因为那是我们自己直接配置的,但对于传递性依赖的版本变化,就会比较模糊,当这种变化造成构建失败的时候,就很难找到原因。因此我们应当增加这些 Used undeclared dependencies 。
依赖分析还提供了 Unused declared dependencies 供我们参考,这表示那些我们配置了,但并未直接使用的依赖。需要注意的时,对于这些依赖,我们不该直接简单地删除。由于dependency:analyze只分析编译主代码和测试代码使用的依赖,一些执行测试和运行时的依赖它发现不了,因此还需要人工分析。通常情况,Unused declared dependencies 还是能帮助我们发现一些无用的依赖配置。
补充一个maven传递依赖的两个原则
(2)在应用启动的时候通过-verbose来进行分析
这个能够辅助进行一些分析,不过jvm启动参数上面加入-verbose可能会导致jboss挂起,可以使用,但是不要作为常态化来使用
1、在JVM的启动参数里面加入“-verbose”,这样会在日志里面显示类加载的信息;
2、输出JVM启动的时候的信息到特定的文件(可选)-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=<path>
通过脚本抓取日志里面从WEB-INF中加载的jar包
grep '^\[Loaded.*/WEB-INF/lib.*' jboss_stdout.log | awk '{print $4 }' | cut -d'/' -f10 | cut -d']' -f1 |sort -u
(3)在编译阶段就预警出问题来,
在编译后,可以通过两种方式来进行
1、在WEB-INF目录下的lib中,解压jar包,然后递归比较类文件名称,如果有一个类名称相同的文件出现在两个jar包中,则有可能会发生冲突,提示出来即可
2、遍历maven插件,在编译的时候进行校验。
大体过程可以分为两步,第一步通过maven自带的API获取所有的jar包文件
第二步,遍历文件,将所有的类信息放到map或者set中,然后进行比较,如果文件曾经出现过,则提示有可能冲突。
private MavenProject project; private boolean failOnWarning; @SuppressWarnings("unchecked") public void execute() throws MojoExecutionException, MojoFailureException { Set<Artifact> artifacts = project.getArtifacts(); HashMap<String, Artifact> classAndArtifactMap = new HashMap<String, Artifact>(); boolean hasWarnings = false; for (Artifact artifact : artifacts) { ZipInputStream zis = null; try { zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(artifact.getFile()))); ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.isDirectory() || !entry.getName().endsWith(".class")) { continue; } if (classAndArtifactMap.containsKey(entry.getName())) { hasWarnings = true; getLog().warn("DUPLICATED CLASS FOUND! " + entry.getName() + "\r\n\t" + artifact + "\r\n\t" + classAndArtifactMap.get(entry.getName())); break; } classAndArtifactMap.put(entry.getName(), artifact); } } catch (Exception e) { throw new MojoExecutionException("Unknown errors...", e); } finally { IOUtils.closeQuietly(zis); } } if (failOnWarning && hasWarnings) { throw new MojoFailureException("PLEASE CHECK ABOVE WARNINGS!!! There is duplicated classes found in your dependencies."); } }
第一种方式在排查问题的时候非常有用,第二种方式不推荐,极力推荐在编译阶段就把有可能存在类冲突的类查出来