当类出现冲突
特别说明:封面图片来源 https://www.zcool.com.cn/work/ZMjEwNTQ3MTY=.html PS:未授权…若作者看到后不愿意授权可联系下架
做Java开发的同学应该都遇到过类冲突,一般都是各个依赖相互引用导致同一个类存在多个版本,我们一般采用依赖排除、依赖管理等常规方式来尝试解决该问题。那么类冲突的根本原因是什么呢?
在知道根本原因之前,我想还有两个有意思的问题,一是:怎么判断一个类是否存在;二是:同一类会加载多次吗?
怎么判断一个类是否存在?
先解决这个问题,怎么判断一个类是否存在?第一反应是搜索,在JVM当中怎么搜索一个类呢,第一反应是加载该类。在JVM中,加载一个类大致经过这几个过程:加载->验证->解析->初始化->使用->卸载。加载就是通过类的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成Java.lang.Class对象。通过Java代码有两种方式去加载一个类,一是:Class.forName(String className) ,二是:ClassLoader.loadClass(String name);
两者之间的共同点是都是检查异常,即需要我们处理 ClassNotFoundException ,如果抛出此异常表明当前JVM中无此类。至于JVM底层是怎么实现的,这不是我们的关注点,因为两个方法是 native 修饰,细知更底层就需要…
Class.forName() 有两个重载,两者之前的区别在于是否初始化该类
1 | public static Class<?> forName(String className) |
Class.forName(className)方法,内部实际调用的方法是 Class.forName(className, true, classloader); boolean initialize 参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static 块代码执行,static参数也会被再次初始化。
ClassLoader.loadClass(name) 该方法内部会调用 protected Class<?> loadClass(String name, boolean resolve) 实际 resolve 为false,表示不进行连接(类加载步骤中的验证、解析)。
如果我们只是判断一个类是否存在,而不需要初始化改类,用 ClassLoader.loadClass 是比较好的方式,当然也可以使用 Class.forName。
同一个类会加载多次吗?
换个问题,你可能听过多次,自定义一个String类,全限定名与JDK的 java.lang.String 一样,自定义的String可以执行吗?答案是否定的,为什么?因为Java的双亲委派机制,当一个类被加载时,首先会让其父类加载器去加载,父类加载器无法加载时子类加载器才会执行,JVM中定义类加载器主要包括,自定义类加载器 -> 应用程序类加载器 -> 扩展类加载器 -> 启动类加载器。很显然,String会优先被启动类加载器加载,为什么需要双亲委派机制,原因有以下几点,一是规范,上面做了事情,下面就不要重复做了,这样可以避免重复加载,父类已经加载了,子类就不需要再次加载;二是安全,解决了各个类加载器的基础类的统一问题,只要父类加载器加载了,子类加载器就不能再加载,否者用户随意定义类加载器来加载核心api,会带来相关隐患。
再回到这个问题,同一个类会加载多次吗?从上文的分析可知,一个类(全限定名一样)只能被JVM加载一次。其实通过ClassLoader.loadClass() 方法也知道(具体实现的方法 loadClass(String name, boolean resolve)),在加载时首先会去查询类是否已经加载,若已加载则会直接返回,未加载才会进一步去加载。
1 | protected Class<?> loadClass(String name, boolean resolve) |
类冲突的表现
类冲突的表现主要体现在一下几种情况。运行时错误和编译时异常。
- java.lang.ClassNotFoundException ,类找不到,这个问题一般是高版本或低版本缺失某个类,比如低版本有Test.class 这个类,而在高版本的依赖中没有了这个类,类加载器在加载时,加载了低版本的类,那么这个时候会报 ClassNotFoundException 异常,一般会在编译时抛出异常。根本原因是类加载器在当前classpath目录中没有找到这个字节码文件,而发生异常。
- java.lang.NoSuchMethodError,没有特定的方法,这个问题和上述类似,同样的类中,不同版本存在不同的方法,比如高版本新增了某几个方法,在类加载时加载了低版本的类,我们在使用高版本类的方法时则会报这个错,编译时发生错误。
- java.lang.NoClassDefFoundError,没有被定义的类,这类错误都是运行时错误,JVM在编译时能找到合适的类,但在运行期间找不到合适的类。
比如下面的例子,在运行时我们想调用某个类的方法或者访问这个类的静态成员的时候,发现这个类不可用,此时Java虚拟机就会抛出NoClassDefFoundError错误。
1 | package com.wy.demo.test; |
除了上面的例子外,出现 NoClassDefFoundError 错误也可能是jar冲突,此类问题遇到过一次,但是在测试环境没复现出来。当时的情况是A业务系统中的fastjson版本为1.2.49,我提供的二方库中使用的fastjson版本为1.2.62,业务系统在使用了二方库中某个类的toString方法时出现了 NoClassDefFoundError 错误,当时的解决方案是升级业务系统的fastjson版本。
有效避免
在发生问题之前,我们需要做的是有效防守,即在冲突发生之前能有效地规避。项目构建大多是使用maven,我们可以很好的利用的依赖管理,除此之外我们还可以使用一些插件去帮我们检查。在遇到一些依赖问题时,比如某个jar包是谁引入的?我们也可以使用命令去发现问题,mvn dependency:tree 。