类加载分析方案以及预防办法

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-contextJava import声明,但spring-context这个依赖实际是通过传递性依赖进入classpath的,这就意味者潜在的风险。一般来说我们对直接依赖的版本变化会比较清楚,因为那是我们自己直接配置的,但对于传递性依赖的版本变化,就会比较模糊,当这种变化造成构建失败的时候,就很难找到原因。因此我们应当增加这些 Used undeclared dependencies

依赖分析还提供了 Unused declared dependencies 供我们参考,这表示那些我们配置了,但并未直接使用的依赖。需要注意的时,对于这些依赖,我们不该直接简单地删除。由于dependency:analyze只分析编译主代码和测试代码使用的依赖,一些执行测试和运行时的依赖它发现不了,因此还需要人工分析。通常情况,Unused declared dependencies 还是能帮助我们发现一些无用的依赖配置。

 

补充一个maven传递依赖的两个原则

 

maven的依赖调节机制有两个原则,一个是路径最近者优先,一个是第一声明者优先。
(1)最短路径优先
例如项目A中有这样的依赖关系,A->B->C->X(1.0) A->D->X(2.0) 分别传递依赖了X包的两个版本,
此时X(1.0)的路径为3,X(2.0)的路径为2,maven优先使用2.0)这个包
(2)第一声明者优先
上面这个原则不能解决所有问题,例如X(1.0)和X(2.0)的路径相同,这时候会使用那个包?
在依赖路径长度相同的情况下,POM依赖声明的顺序决定了谁会被解析使用,顺序靠前的那个依赖优先使用。
例如:
A->B->Y(1.0) A->C->Y(2.0)
如果B的依赖声明在C之前,则Y(1.0)版本会被解析使用。
 

 

(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.");
        }
    }
 

 

 

 

第一种方式在排查问题的时候非常有用,第二种方式不推荐,极力推荐在编译阶段就把有可能存在类冲突的类查出来

 

你可能感兴趣的:(类加载)