前言
最近工作中遇到了一个项目工程问题,在启动jvm的classpath有两个不同版本的jwt的jar包,在调用处报java.lang.NoSuchMethodError: 其Classpath有两个不同版本的jar包,里面都有这个类,高版本的jar里面没有这个Method,低版本有这个Method。最终没有加载这个高版本的Method
猜测此问题就是 “全限定类名” 完全一样,Cloassloader只加载了高版本的jar包。
在Java应用程序中,如果两个jar包中包含了完全相同的包名和类名,那么在加载这类时就会出问题。本文将细讲解这个问题,并提供两个示例说明。
问题描述
当两个jar包中包含了完全相的包名和名时,Java虚拟机(JVM)将无法区分这些类,因为它们的全限定名是相同的这将导以下问题:
- 如果在代码中使用了这些类,那么JVM将法确定要加载哪个类,从而导致编译错误或运行时错误。
- 如果在两个jar包中的类具有不同的实现,那么在运行时将无法确定使用哪个实现。
验证jvm是如何加载classpath中同类同全限定类名的过程
准备工程
准备三个不同的jar,里面都有同样一个类 Car
,如下:
- demo-audi-1.0.jar
- demo-audi-2.0.jar
- demo-mercedes-1.0.jar
这三个工程拥有同样一个class: com.example.demo.Car
demo-audi-1.0.jar内容
public class Car { private static final String version = "A4L"; public String getVersion() { return version; } public String getName() { return "audi"; } public Integer limitSpeed() { return 100; } public String seatPerson(Integer a) { return "可以坐 :" + a; } }
demo-audi-2.0.jar内容
public class Car { private static final String version = "A6L"; public String getVersion() { return version; } public String getName() { return "audi"; } public Integer limitSpeed() { return 140; } public String seatPerson() { return "可以坐 :5"; } }
demo-mercedes-1.0.jar内容
public class Car { private static final String version = "E300L"; public String getVersion() { return version; } public String getName() { return "mercedes"; } public Integer limitSpeed() { return 135; } public String hasPerson() { return "能舒服的坐 :4"; } }
开始演示功能
准备Example类
public class Example { public static void main(String[] args) { Car car = new Car(); System.out.println("当前车辆版本:" + car.getVersion()); System.out.println("当前 jar 包路径 : "); System.out.println(car.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()); Method[] declaredMethods = car.getClass().getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { System.out.println("------------------"); System.out.println("method name: " + declaredMethod.getName()); List<String> collect = Arrays.stream(declaredMethod.getParameterTypes()).map(Class::getName).collect(Collectors.toList()); if(!collect.isEmpty()) { System.out.println("parameter type : " + collect); } System.out.println("------------------"); } } }
准备Classpath
test 目录
demo-audi-1.0.jar demo-audi-2.0.jar demo-example-1.0.jar demo-mercedes-1.0.jar
演示功能
- 自然顺序,*来代替所有jar
java -classpath "/test/*" com.example.demo.Example 当前车辆版本:A4L 当前 jar 包路径 : /test/demo-audi-1.0.jar ------------------ method name: getName ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: seatPerson parameter type : [java.lang.Integer] ------------------ ———————————————
运行时,JVM加载类Car,根据【操作系统】的选择,本次加载了demo-audi-1.0.jar的com.example.demo.Car
类的class文件
- 手动改变jar名称,改变顺序
当把 demo-audi-1.0.jar 重命名为 zemo-audi-1.0.jar ,“d” 改成 “z”
➜ test mv demo-audi-1.0.jar zemo-audi-1.0.jar ➜ test java -classpath "/test/*" com.example.demo.Example 当前车辆版本:A6L 当前 jar 包路径 : /test/demo-audi-2.0.jar ------------------ method name: getName ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: seatPerson ------------------
运行时,JVM加载类Car,根据【操作系统】的选择,本次加载了demo-audi-2.0.jar的com.example.demo.Car
类的class文件
- 手动改变jar名称及版本,改变顺序
当把 demo-audi-2.0.jar 重命名为 demo-mercedes-2.0.jar ,“audi” 改成 “mercedes”
[/test]# mv demo-audi-2.0.jar zemo-mercedes-2.0.jar [/test]# ls demo-example-1.0.jar demo-mercedes-1.0.jar demo-mercedes-2.0.jar zemo-audi-1.0.jar [/test]# java -classpath "/test/*" com.example.demo.Example 当前车辆版本:A6L 当前 jar 包路径 : /test/demo-mercedes-2.0.jar ------------------ method name: getName ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: seatPerson ------------------
运行时,JVM加载类Car,根据【操作系统】的选择,本次加载了demo-mercedes-2.0.jar的com.example.demo.Car
类的class文件
- 只保留一个jar
如果把audi的两个jar移动出去,classpath里面只剩下 demo-mercedes-1.0.jar 时
当前车辆版本:E300L 当前 jar 包路径 : /test/demo-mercedes-1.0.jar ------------------ method name: getName ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: hasPerson ------------------
运行时,JVM加载类Car,根据【操作系统】的选择,本次加载了demo-mercedes-1.0.jar的com.example.demo.Car
类的class文件
手动指定Classpath的包先后顺序( 重点)
# 当 demo-audi-1.0.jar在第一个时 java -classpath /test/demo-audi-1.0.jar:/test/demo-audi-2.0.jar:/test/demo-mercedes-1.0.jar:/test/demo-example-1.0.jar com.example.demo.Example 当前车辆版本:A4L 当前 jar 包路径 : /test/demo-audi-1.0.jar ------------------ method name: getName ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: seatPerson parameter type : [java.lang.Integer] # 当 demo-audi-2.0.jar在第一个时 java -classpath /test/demo-audi-2.0.jar:/test/demo-audi-1.0.jar:/test/demo-mercedes-1.0.jar:/test/demo-example-1.0.jar com.example.demo.Example 当前车辆版本:A6L 当前 jar 包路径 : /test/demo-audi-2.0.jar ------------------ method name: getName ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: seatPerson ------------------ # 当 demo-mercedes-1.0.jar在第一个时 java -classpath /test/demo-mercedes-1.0.jar:/test/demo-audi-2.0.jar:/test/demo-audi-1.0.jar:/test/demo-example-1.0.jar com.example.demo.Example 当前车辆版本:E300L 当前 jar 包路径 : /test/demo-mercedes-1.0.jar ------------------ method name: getName ------------------ ------------------ method name: limitSpeed ------------------ ------------------ method name: getVersion ------------------ ------------------ method name: hasPerson ------------------
结论
根据JVM的双亲委派模型,默认情况下相同全限定类名的类只会加载一次,因此JVM加载Car类时只会从demo-audi-1.0.jar或demo-audi-2.0.jar以及demo-mercedes-1.0.jar选一个;
同名的两个Car类来自不同的三个Jar包,他们是平级的,根据JVM的类加载机制——双亲委派模型,相同全限定类名的类默认只会加载一次(除非手动破坏双亲委派模型);
Jar包中的类是使用AppClassLoader加载的,而类加载器中有一个命名空间的概念,同一个类加载器下,相同包名和类名的class只会被加载一次,如果已经加载过了,直接使用加载过的;
如果依赖中有多个全限定类名相同的类,那JVM会加载哪一个类呢?
比较靠谱的说法是,操作系统本身,控制了Jar包的默认加载顺序;也就是说,对于我们来说是不明确不确定的!
而Jar包的加载顺序,是跟classpath这个参数有关,当使用idea启动springboot的服务时,可以看到classpath参数的;包路径越靠前,越先被加载;
换句话说,如果靠前的Jar包里的类被加载了,后面Jar包里有同名同路径的类,就会被忽略掉,不会被加载;
解决方案
为了解决个问题,我们可以使用以下方法:
- 使用不同的包名
将两个jar包中的类放在不同的包中,以避免包名和类名的冲突。例如,如果两个jar包中都包含了名为com.example.MyClass
的类,那么我们可以将其中一个类放在com.example2.MyClass
包中。
- 使用类加载器
使用不同的类加载器来加载这些类,以避免冲突。例如,我们可以使用自定义的类加载器来加载其中一个jar包中的类,从而避免与另一个jar包中的类发生冲突。
示例说明
以下是两个示例说明,演示了两个jar包中包含相同的包和类名时可能出现的问题。
- 示例1:编译错误
假设我们有两个jar包:lib1.jar
和lib2.jar
,它们都包含了名为com.example.MyClass
的类。如果我们在代码中使用了这个类,那么编译器将无法确定要使用哪个类,从而导致编译错误。
import com.example.MyClass; public class Main { public static void main(String[] args) { MyClass myClass = new MyClass(); // 编译错误:无法确定要使用哪个MyClass类 myClass.doSomething(); } }
- 示例2:运行时错误
假设我们有两个jar包:lib1.jar
和lib2.jar
,它们都包含了名为com.example.MyClass
的类。如果这些类具有不同的实现,那么在运行时将无法确定使用哪个实现。
import com.example.MyClass; public class Main { public static void main(String[]) { MyClass myClass = new MyClass(); // 运行时错误:无法确定要使用哪个MyClass类 myClass.doSomething(); } }
在这个示例中,如果lib1.jar
和lib2.jar
中的MyClass
类具有不同的实现,那么在运行时将无法确定使用哪个实现。这可能会导致意外的行为或错误。
以上就是浅谈两个jar包中包含完全相同的包名和类名的加载问题的完整攻略。在实际开发,我们需要注意避免个问题,并根据实际情况选择合适的解决方案。