乐趣区

关于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。

以 Assert 类为例,能够看到有很多包都蕴含了 Assert,但启动程序却报找不到该类的某个办法,问题基本上就出在 Jar 包抵触上了。

第二,定位到 Jar 包抵触之后,找到零碎本应该应用的 Jar 包。

比方这里须要应用的 spring-core 中的类,而不 spring.jar 中的类。那么,就能够利用 JVM 的类加载程序机制,让 JVM 先加载 spring-core 的 jar 包。

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

因而,长期解决方案就是调整 JVM 编译(加载)Jar 包的程序。这个在 Eclipse 和 Idea 中都有反对,能够手动进行调整。

Eclipse 中调整形式:

Idea 中调整形式:

把须要优先加载的 jar 包往上调整,这样就能够优先加载它,总算是长期解决了 jar 包抵触的问题。

类加载机制的延长

下面只是受限于我的项目现状的长期解决方案,最终必定是要进行革新降级的,基于 Maven 或 Gradle 进行 Jar 包治理,同时解决掉 Jar 包抵触的问题的。

在这个长期解决方案,波及到一个 JVM 的要害知识点:JVM 的类加载器的隔离问题及双亲委派机制。如果没有 JVM 类加载机制的相干常识,可能连下面的长期计划都无奈想到。

类加载器的隔离问题

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

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

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

双亲委派机制

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

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

暂且不思考自定义类加载器,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 包抵触治理的一些策略,比方依赖传递准则、最短门路优先准则、最先申明准则等,咱们下篇文章再来具体聊聊。

博主简介:《SpringBoot 技术底细》技术图书作者,热爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢送关注~

技术交换:请分割博主微信号:zhuan2quan

退出移动版