当前位置: 首页 > 科技观察

从Jar包冲突,到类加载机制,霸气十足

时间:2023-03-20 11:25:00 科技观察

背景介绍目前市面上的项目管理要么基于Maven,要么基于Gradle。最近接手了一组手动添加jar包的项目。对于纯手工添加jar包的项目,很多年前就是这种方式,工作三五年的技术人员可能没有体会过。就是把项目中需要的jar包一个一个找出来,添加到一个lib目录下,然后在IDE中手动添加jar包依赖。这种方式添加jar包依赖不仅费时,而且容易出现jar包冲突。同时,分析冲突的手段只能靠经验。最近遇到这样一种情况:一个项目在开发者A的环境中可以正常启动,但是在B中无法启动,异常信息是找不到类。稍有开发经验的人马上就能断定是jar包冲突导致的。下面我们就来看看它是如何解决的,以及由此衍生出的知识点。临时解决方案由于项目暂时无法进行大规模重构,Jar包的替换和升级并不容易。只能通过临时手段解决。这里总结几个步骤以备不时之需,通常是解决Jar依赖问题的小窍门。第一:在IDE中查找异常中没有找到的类。比如IDEAMAC操作系统,我用的快捷键是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中调整方法:Eclipse调整顺序Idea中调整方法:Idea调整顺序调整需要先加载的jar包,使其先加载,最终暂时解决jar包冲突问题。上面对类加载机制的扩展只是受限于项目现状的临时解决方案。最终还是要改造升级,基于Maven或者Gradle进行Jar包管理,同时解决Jar包冲突的问题。在这个临时方案中,涉及到JVM的一个关键知识点:JVM类加载器的隔离问题和双亲委派机制。如果没有JVM类加载机制的相关知识,可能连上面的临时解决方案都想不出来。类加载器的隔离每个类加载器都有自己的命名空间,用于存储加载的类。当类加载器加载一个类时,它会搜索存储在命名空间中的全限定类名(FullyQualifiedClassName)来检测该类是否已经被加载。JVM对一个类的唯一标识是ClassLoaderid+PackageName+ClassName,所以在一个运行的程序中可能会出现两个包名和类名完全相同的类。而如果这两个类不是由一个ClassLoader加载的,就不可能将一个类的实例强行给另一个类,这就是ClassLoader的隔离性。为了解决类加载器隔离的问题,JVM引入了双亲委派机制。双亲委托机制双亲委托机制的核心有两点:第一,检查类是否已经自底向上加载;其次,尝试从上到下加载类。类加载器类加载器一般有四种类型:启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器。抛开自定义类加载器不谈,JDK自带类加载器的具体执行过程如下:第一:AppClassLoader加载一个类时,会将类加载请求委托给父类加载器ExtClassLoader来完成;第二种:当ExtClassLoader加载类时,会将类加载请求委托给BootStrapClassLoader来完成;第三:如果BootStrapClassLoader加载失败(比如%JAVA_HOME%/jre/lib中没有找到类),会使用ExtClassLoader尝试加载;four:如果ExtClassLoader也加载失败,则使用AppClassLoader加载,如果AppClassLoader也加载失败,则报异常ClassNotFoundException。ClassLoader的双亲委托实现ClassLoader通过loadClass()方法实现双亲委托机制,实现类的动态加载。该方法源码如下:protectedClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){//首先检查类是否已经被加载类c=findLoadedClass(name);if(c==null){longt0=System.nanoTime();try{if(parent!=null){c=parent.loadClass(name,false);}else{c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){//ClassNotFoundExceptionthrownifclassnotfound//fromthenon-nullparentclassloader}if(c==null){//如果仍然没有找到,则调用findClassinorder//tofindtheclass.longt1=System.nanoTime();c=findClass(name);//thisisthedefiningclassloader;addTime(t1-t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if(resolve){resolveClass(c);}returnc;}}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核心接口;$java_home/lib/ext目录下的java扩展jar包;java-classpath/-Djava.class.path指向的目录下的class和jar包;$CATALINA_HOME/common目录按照文件夹的顺序从上到下加载;$CATALINA_HOME/server目录按照文件夹顺序从上到下加载;$CATALINA_BASE/shared按照文件夹的顺序从上到下加载;项目路径/WEB-INF/classes下的class文件;项目路径/WEB-INF/lib下的jar文件;上述目录中,从上到下依次加载同文件夹下的Jar包。如果一个类文件已经被加载到JVM中,则以后不会再加载同一个类文件。总结Jar包冲突是我们日常开发中非常常见的问题。如果我们能够很好地理解冲突的原因和潜在机制,我们就可以大大提高我们解决问题的能力和团队影响力。所以,很多面试都会提到这样的问题。本文重点介绍手动添加依赖情况下Jar包冲突的原因及解决方法。在解决这个问题的时候,Maven往往会设计一些Jar包冲突管理的策略,比如依赖传递原则、最短路径优先原则、先声明原则等,我们会在下一篇详细讲到.