从Jar包冲突搞到类加载机制,就是这么霸气

简介: 从Jar包冲突搞到类加载机制,就是这么霸气

接手了一套比较有年代感的系统,计划把重构及遇到的问题写成系列文章,老树发新枝,重温一些实战技术,分享给大家。【重构01篇】,给大家讲讲Jar包冲突及原理。


背景

目前市面上项目管理要么是基于Maven,要么是基于Gradle,最近接手了一套纯手动添加jar包的项目。


对于纯手动添加jar包的项目已经是多年前的方式了,现在工作三五年的技术人员可能都没有经历过。就是把项目中所需的jar包挨个找出来,添加到一个lib目录中,在IDE中再将jar包依赖手动添加上。


这种方式来添加jar包依赖,不仅费事,而且很容易出现jar包冲突,同时分析冲突手段,只能凭借经验。


最近就遇到这样一种情况:一个项目在开发者A的环境中可以正常启动,在B那里就无法启动,而异常信息是找不到什么什么类。


稍微有一些开发经验的人,马上就可以断定是jar包冲突导致。下面就看看如何解决及引申出来的知识点。


临时解决方案

由于暂时无法对项目进行大范围重构,也不敢轻易将Jar包进行替换升级。只能采用临时的手段来进行解决。


这里总结几个步骤以备不时之需,通常也是解决Jar依赖问题的小技巧。


第一:在IDE中查找异常中找不到的类。比如IDEA MAC操作系统,我用的快捷键是command + shift + n。


image.png以Assert类为例,可以看到有很多包都包含了Assert,但启动程序却报找不到该类的某个方法,问题基本上就出在Jar包冲突上了。


第二,定位到Jar包冲突之后,找到系统本应该使用的Jar包。


比如这里需要使用的spring-core中的类,而不spring.jar中的类。那么,就可以利用JVM的类加载顺序机制,让JVM先加载spring-core的jar包。


知识点:在同一目录下的jar包,JVM是按照jar包的先后顺序进行加载,一旦一个全路径名相同的类被加载之后,后面再有相同的类便不会进行加载了。


因此,临时解决方案就是调整JVM编译(加载)Jar包的顺序。这个在Eclipse和Idea中都有支持,可以手动进行调整。


Eclipse中调整方式:


image.pngIdea中调整方式:

image.png把需要优先加载的jar包往上调整,这样就可以优先加载它,总算是临时解决了jar包冲突的问题。


类加载机制的延伸

上面只是受限于项目现状的临时解决方案,最终肯定是要进行改造升级的,基于Maven或Gradle进行Jar包管理,同时解决掉Jar包冲突的问题的。


在这个临时解决方案,涉及到一个JVM的关键知识点:JVM的类加载器的隔离问题及双亲委派机制。如果没有JVM类加载机制的相关知识,可能连上面的临时方案都无法想到。


类加载器的隔离问题

每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name) 进行搜索来检测这个类是否已经被加载了。


JVM 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个类不是由一个 ClassLoader 加载,是无法将一个类的实例强转为另外一个类的,这就是 ClassLoader 隔离性。


为了解决类加载器的隔离问题,JVM引入了双亲委派机制。


双亲委派机制

双亲委派机制的核心有两点:第一,自底向上检查类是否已加载;其二,自顶向下尝试加载类。


image.png类加载器通常有四类:启动类加载器、拓展类加载器、应用程序类加载器和自定义类加载器。


暂且不考虑自定义类加载器,JDK自带类加载器具体执行过程如下:


第一:当AppClassLoader加载一个class时,会把类加载请求委派给父类加载器ExtClassLoader去完成;


第二:当ExtClassLoader加载一个class时,会把类加载请求委派给BootStrapClassLoader去完成;


第三:如果BootStrapClassLoader加载失败(例如在%JAVA_HOME%/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;


第四:如果ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。


ClassLoader的双亲委派实现

ClassLoader通过loadClass()方法实现了双亲委托机制,用于类的动态加载。


该方法的源码如下:


protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        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;
        }
    }

loadClass方法本身是一个递归向上调用的过程,上述代码中从parent.loadClass的调用就可以看出。


在执行其他操作之前,首先通过findLoadedClass方法从最底端的类加载器开始检查是否已经加载指定的类。如果已经加载,则根据resolve参数决定是否要执行连接过程,并返回Class对象。


而Jar包冲突往往发生在这里,当第一个同名的类被加载之后,在这一步检查时就会直接返回,不会再加载真正需要的类。那么,程序用到该类时就会抛出找不到类,或找不到类方法的异常。


Jar包的加载顺序

上面已经看到一旦一个类被加载之后,全局限定名相同的类可能就无法被加载了。而Jar包被加载的顺序直接决定了类加载的顺序。


决定Jar包加载顺序通常有以下因素:


第一,Jar包所处的加载路径。也就是加载该Jar包的类加载器在JVM类加载器树结构中所处层级。上面讲到的四类类加载器加载的Jar包的路径是有不同的优先级的。

第二,文件系统的文件加载顺序。因Tomcat、Resin等容器的ClassLoader获取加载路径下的文件列表时是不排序的,这就依赖于底层文件系统返回的顺序,当不同环境之间的文件系统不一致时,就会出现有的环境没问题,有的环境出现冲突。

本人遇到的问题属于第二种因素中的一个分支情况,即同一目录下不同Jar包的加载顺序不同。因此,通过调整Jar包的加载顺序就暂时解决了问题。


Jar包冲突的通常表现

Jar包冲突往往是很诡异的事情,也很难排查,但也会有一些共性的表现。


抛出java.lang.ClassNotFoundException:典型异常,主要是依赖中没有该类。导致原因有两方面:第一,的确没有引入该类;第二,由于Jar包冲突,Maven仲裁机制选择了错误的版本,导致加载的Jar包中没有该类。

抛出java.lang.NoSuchMethodError:找不到特定的方法。Jar包冲突,导致选择了错误的依赖版本,该依赖版本中的类对不存在该方法,或该方法已经被升级。

抛出java.lang.NoClassDefFoundError,java.lang.LinkageError等,原因同上。

没有异常但预期结果不同:加载了错误的版本,不同的版本底层实现不同,导致预期结果不一致。

Tomcat启动时Jar包和类的加载顺序

最后,梳理一下Tomcat启动时,对Jar包和类的加载顺序,其中包含上面提到的不同种类的类加载器默认加载的目录:


$java_home/lib 目录下的java核心api;

$java_home/lib/ext 目录下的java扩展jar包;

java -classpath/-Djava.class.path所指的目录下的类与jar包;

$CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载;

$CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载;

$CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载;

项目路径/WEB-INF/classes下的class文件;

项目路径/WEB-INF/lib下的jar文件;

上述目录中,同一文件夹下的Jar包,按照顺序从上到下一次加载。如果一个class文件已经被加载到JVM中,后面相同的class文件就不会被加载了。


小结

Jar包冲突在我们的日常开发中是非常常见的问题,如果能够很好理解冲突的原因及底层机制,可以极大的提高解决问题的能力和团队影响力。因此,在不少面试中都会被提及此类问题。


这篇文章我们重点讲了手动添加依赖情况下导致Jar包冲突的原因及解决方案。在解决该问题时往往还会设计到Maven对Jar包冲突管理的一些策略,比如依赖传递原则、最短路径优先原则、最先声明原则等,我们下篇文章再来详细聊聊。


目录
相关文章
|
6月前
|
数据可视化 Java 分布式数据库
分享111个Java源码,总有一款适合您
分享111个Java源码,总有一款适合您
189 0
|
6月前
|
小程序 Java 分布式数据库
分享66个Java源码,总有一款适合您
分享66个Java源码,总有一款适合您
171 0
分享66个Java源码,总有一款适合您
|
算法 数据可视化 Java
深入理解JVM系列教程(完) - 终章总结
深入理解JVM系列教程(完) - 终章总结
63 0
|
5月前
|
算法 Java 程序员
【程序员必看!】掌握这招if-else,你的Java代码将瞬间高大上!
【6月更文挑战第14天】本文介绍了if-else语句在程序设计中的重要性,通过示例展示其基本语法和复杂用法,如嵌套和else-if链。强调了避免过度嵌套、逻辑运算符的使用、保持一致性及添加注释等提升代码质量的技巧。学习并实践这些技巧,将使你的Java代码更优雅、高效,助你成为编程艺术家。
57 0
|
6月前
|
监控 Java 测试技术
面试准备不充分,被Java守护线程干懵了,面试官主打一个东西没用但你得会
面试准备不充分,被Java守护线程干懵了,面试官主打一个东西没用但你得会
61 1
|
6月前
|
SQL 新零售 移动开发
分享77个Java源码,总有一款适合您
分享77个Java源码,总有一款适合您
335 0
|
6月前
|
数据采集 供应链 Oracle
分享52个Java源码,总有一款适合您
分享52个Java源码,总有一款适合您
168 0
|
Java Maven
不会还在为jar包冲突发愁吧
在我们平时的开发过程中,常常会遇到引入各种不同的 jar 包,然后引发的 Maven 依赖冲突,今天我们来学习下如何使用 Maven 命令检测 pom.xml 中的重复依赖项。
84 0
|
数据采集 安全 Java
【java筑基】面试、工作不可不知的六个Java工具类
【java筑基】面试、工作不可不知的六个Java工具类
|
存储 算法 安全
《我想进大厂》之JVM夺命连环10问
这是面试专题系列第五篇JVM篇。这一篇可能稍微比较长,没有耐心的同学建议直接拖到最后。
《我想进大厂》之JVM夺命连环10问