【JAVA基础】不同的jar拥有相同全限定类名和不同的方法Method时NoSuchMethodError,同名类加载问题 / 双亲委派

前言

最近工作中遇到了一个项目工程问题,在启动jvm的classpath有两个不同版本的jwt的jar包,在调用处报java.lang.NoSuchMethodError: 其Classpath有两个不同版本的jar包,里面都有这个类,高版本的jar里面没有这个Method,低版本这个Method。最终没有加载这个高版本的Method
猜测此问题就是 “全限定类名” 完全一样,Cloassloader只加载了高版本的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类

example.java

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包里有同名同路径的类,就会被忽略掉,不会被加载;

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在Spring Boot中,如果两个不同jar包中包含相同的包名和类名,那么在这两个jar可能会出现冲突。 当我们启动Spring Boot应用,会依次classpath下的所有jar包。在过程中,如果发现两个jar包中存在相同的包名和类名,Spring Boot将无法区分它们,从而导致冲突。 为了解决这个问题,可以使用以下方式之一: 1. 排除冲突类:在pom.xml(maven项目)或build.gradle(gradle项目)中,对引入的冲突jar包进行排除的操作。可以将其中一个jar包排除掉,从而避免冲突。 2. 修改包名:如果可以更改jar包中的代码,可以尝试修改其中一个jar包中的包名或类名,使其与另一个jar包中的包名或类名不再冲突。然后重新打包并引入修改后的jar包。 3. 使用ClassLoader隔离:可以自定义一个ClassLoader来其中一个冲突的jar包。通过使用不同的ClassLoader,可以实现对每个jar包的独立,从而避免冲突。 需要注意的是,在解决这个问题,我们应该谨慎确保冲突的jar包不会影响应用程序的运行,同应该尽量避免引入具有相同包名和类名的jar包。如果无法避免这种情况,我们可以使用上述方法中的一种来解决冲突问题。 ### 回答2: 当一个应用程序中存在两个包含相同的包名和类名的jar,Spring Boot会根据默认的类机制来类。默认情况下,Spring Boot使用的是Java的标准类器来类,而标准类器遵循委派模型。 根据委派模型,当需要一个类,标准类器会首先检查自身是否已经了这个类。如果已经,则直接返回该类的实例;如果没有,则会将这个类的请求委派给父类器进行处理。 在这种情况下,当存在两个包含相同包名和类名的jar,标准类器会根据类路径的顺序逐个这些jar包,直到找到所需的类。如果两个jar包中的类都符合要求,那么标准类器会选择路径上先出现的那个jar包中的类作为被的类。 如果开发者想要显式地选择使用其中一个jar包中的类,可以通过修改类路径的顺序来实现。可以在Spring Boot的配置文件(application.properties或application.yml)中将依赖的jar包添到`spring.autoconfigure.exclude`属性中,使得在跳过不需要的jar包。 总结起来,当存在两个包含相同包名和类名的jar,Spring Boot会根据标准类器的委派机制来类。可以通过配置类路径的顺序来明确选择所需的jar包中的类。 ### 回答3: 在Spring Boot中存在两个包含完相同包名和类名的jar包,这会导致问题。当程序运行,ClassLoader会按照特定的顺序搜索类文件并到内存中。由于这两个jar包中的类名和包名完相同,ClassLoader会优先位于Classpath中的第一个jar包中的类。 如果这两个jar包中的类内容相同,不会出现任何问题,因为ClassLoader只会其中一个类。但是,如果这两个类内容不同,将会导致错误,程序可能无法正常运行。 为了解决这个问题,我们可以采取以下几种方法: 1. 删除重复的jar包:如果这两个jar包是由于误操作或其他原因导致了重复的,我们可以通过删除其中一个来解决问题。 2. 修改类的包名:如果两个类内容不同但包名相同,我们可以通过修改其中一个类的包名来避免冲突。 3. 使用不同的ClassLoader:我们可以通过自定义ClassLoader其中一个jar包,从而避免冲突。可以通过在Spring Boot的启动类中指定ClassLoader实现这一点。 总之,解决Spring Boot中两个包含完相同包名和类名的jar问题方法是删除重复的jar包、修改包名或使用不同的ClassLoader。通过这些方式,我们可以避免错误并确保程序的正常运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑起来贼好看

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值