jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)

冲突提示信息

「java.lang.ClassNotFoundException」:类型转换错误,本应该引入的是 logback 包的类,但是实际引入的是 slf4j 下的同名类,导致类型转换错误。

「java.lang.NoSuchMethodError」:找不到特定方法,如果有两个同名的包但是不同版本,例如 xxx-1.1和 xxx-1.2包同时存在,先加载了 1.1 版本的类,但是 1.2 版本中才提供了新方法,导致提示找不到特定方法

「java.lang.NoClassDefFoundError,java.lang.LinkageError」

解决方式:

查看catalina.sh堆栈信息,找到有问题的类
通过IDEA,在打包的POM文件中,使用Maven Helper插件找出冲突的依赖,确定项目需要的jar包,Exclude掉不需要的依赖

使用工具检查依赖冲突,冲突插件:maven-enforcer-plugin
引用第三方依赖(工具包或者框架包),通过Maven插件检查一下conflict依赖,提前进行Exclude

统一服务器版本
在测试阶段,准备好和生产环境一样的服务器,提前进行测试,避免依赖冲突的 WAR 包上传到生产环境,例如我们有一台 UAT 服务器,与生产环境一样配置,提前测试,暴露风险和解决问题。

==============================================

实际场景开发中遇得到的报错

如下两个报错原因:

「Class path contains multiple SLF4J binding」
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第1张图片

「org.slf4j.impl.Log4jLoggerFactory cannot be cast to ch.qos.logback.classic.LoggerContext」
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第2张图片

查看报错代码

通过 StaticLoggerBinder.getSingleton().getLoggerFactory() 获取 logger 上下文这段代码报错了,通过仔细定位,发现了有两个 StaticLoggerBinder 类

public static void initLogging(String location) throws FileNotFoundException, JoranException {
   String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location);
   URL url = ResourceUtils.getURL(resolvedLocation);
   LoggerContext loggerContext = (LoggerContext)StaticLoggerBinder.getSingleton().getLoggerFactory();//报错代码,两个StaticLoggerBinder
   loggerContext.reset();
   new ContextInitializer(loggerContext).configureByResource(url);
}

虽然不是同一个 jar 包,但是包路径和名称都一模一样!!!
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第3张图片

由于我们需要的是 logback 包,而不是 slf4j-log4j12 包,所以需要排除掉 slf4j-log4j12 依赖
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第4张图片

解决方法

① 通过 POM 文件排查包冲突

② 安装 IDEA 的插件 Maven Helper

③ 定位到编译 WAR 包的 POM 文件(我们框架定义的在 Deploy 模块中)

jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第5张图片
④ 在搜索框中,输入搜索内容,点击右键可以看到选项框

Jump To Source(跳转到源文件处)
Exclude(排除掉)
例如我点击了 Exclude ,就能看到 pom 文件中,这个依赖就被排除掉了

<dependency>
    <groupId>cn.com.xxxgroupId>
    <artifactId>framework-conf-clientartifactId>
    <version>${xqy.framework.version}version>
    <exclusions>
        <exclusion>
            <artifactId>slf4j-log4j12artifactId>
            <groupId>org.slf4jgroupId>
        exclusion>
    exclusions>
dependency>

排除依赖后,提交代码,重新打包,部署一条龙,顺利启动

=========================================================

类加载机制

在本地开发、测试环境都没有出现的问题,却在预发环境出现了,所以排除了业务逻辑代码的原因,简单考虑了几个因素和原因:

  • jdk 版本
  • tomcat 版本
  • 类加载机制
  • 第三方 jar 互相依赖

由于jdk/tomcat两者容易排查,未发现问题,所以再去排查类的加载机制

类加载机制
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第6张图片
我们写的 Java 应用代码,一般是通过 App ClassLoader 应用加载器进行加载,它不会自己先去加载它,而是通过 Extension ClassLoader 扩展类加载器进行加载(其中扩展类加载器又会去找 Bootstrap ClassLoader 启动类加载器进行加载),只有父加载器无法加载情况下,才会让下级加载器进行加载。

Java 使用的是双亲委派加载机制,通过查看 ClassLoader 类。
类被成功加载后,将被放入到内存中,内存中存放 Class 实例对象。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 首先,检查 class 是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 如果没有被加载
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 寻找 parent 加载器
                    c = parent.loadClass(name, false);
                } else {
                    // 如果父加载器不存在,则委托给启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                // 如果仍然无法加载,才会尝试自身加载
                long t1 = System.nanoTime();
                c = findClass(name);
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

类加载顺序

从代码中了解到,如果某个名字的类被加载后,类加载器是不会再重新加载,所以我们的问题根本原因可以是出现在:

「先加载了 org.slf4j 包的 org.slf4j.impl.StaticLoggerBinder,同名的 ch.qos.logback 包下的 StaticLoggerBinder 类没有被加载」
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第7张图片

在 jvm 启动脚本中,添加 -verbose 参数或者 -XX:+TraceClassLoading
查看加载顺序
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第8张图片
之前在本地开发中,IDEA 优化先加载了 ch.qos.logback 的 StaticLoggerBinder 类,然后后面的 org.slf4j 包下的同名类就没有被加载。

但这样也有个不明白,按理说加载顺序按照「字母顺序」加载,预发环境还是能够跟本地开发一样,加载到我们需要的类。实际上,加载器加载到的是另一个类,导致应用无法启动。

jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第9张图片
验证 inode 是否是问题的原因:

本地 Tomcat8 测试(正常启动)
将之前在 uat 环境有问题的代码版本重新打包,不使用 idea 工具,直接用 tomcat8 启动,并且在 catalina.sh 脚本中加入类加载打印参数 -XX:+TraceClassLoading
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第10张图片

查看 catalina.out 输入日志,发现先加载的是 logback 包中 StaticLoggerBinder
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第11张图片

本地 Tomcat8 测试(删包,先添加 slf4j,后添加 logback)

  • 清理掉 catalina.out
  • 重新上传包
  • 比较 inode 大小
  • 重新启动,查看类加载日志

「比较 inode 大小(发现 slf4j < logback)」
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第12张图片

在 uat 环境服务器测试

在 WEB-INF/lib 路径下,先将这两个包删掉,尝试有不同的上传顺序,模拟 tomcat 解压 war 包
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第13张图片
分别测试了两种场景,发现只要这两个包都存在的情况下,无论 inode 两者的大小,都是先加载了 slf4j 包的类,导致启动报错
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第14张图片

通过多种测试场景,发现本地开发、测试环境都无法复现的问题,在 uat 环境下,只要这两个包同时存在,都会启动报错,系统版本是这个:
jar包依赖冲突排查思路和解决方法,以及类加载机制排查(系统原因也导致预发布环境和本地环境的差异)_第15张图片
大意为:同一个目录下,jvm加载jar包顺序是无法保证的,每个系统的都不一样,甚至同一个系统不同的时刻加载都不一样。

于是乎,我也不纠结某台服务器上的类加载顺序,在开发阶段就先将这个包冲突的情况,给提前解决掉~

你可能感兴趣的:(jar,java,包依赖冲突)