Java重复类问题探究

集成开发环境(IDE)是一把双刃剑,为我们提供莫大便利的同时也隐藏了很多的问题。一旦出现问题,如果不了解内部的一些机制会让我们手足无措。本文抛开IDE,用最原始的方式还原重复类引发的一个问题,希望能给大家一点启发。

重复类的定义

重复类是指程序中存在两个或者多个包名以及类名都一致的类。如果只是类名一致,而包名不一致,这本身就是不同的类,不在本文的讨论之列。而包名和类名完全一致但是加载它们的类加载器(ClassLoader)不一样,则其实在JVM的层面,它们也属于不同的类,也不在我们本文讨论的重复类范围。

本文要讨论的重复类是指类名和包名完全一样,并且不会被不同的类加载器分别加载的情况,也就是说这两个类只会有一个被加载到虚拟机中。

哪种情况下会引入重复类呢?
比如maven中引入了两个不同jar包依赖,而这两个不同的jar包均包含同一个类。
需要指出的是,如果这两个类包括内容都是完全一样的,那不会引起什么问题。因此本文讨论的重复类,是指类名一致,但是内容不一致的情况。

重复类可能引起的问题

如果说两个重复类中的内容不一致,则很有可能在系统运行的时候会报错,比如报找不到某个方法的错(java.lang.NoSuchMethodError)。
当然除了NoSuchMethodError之外还可能出现其他的各种异常,需要具体问题具体分析。

重复类举例

假设我们当前的目录结构如下所示:

abinge@abinge-ubuntu:~/workspace$ pwd
/home/abinge/workspace
abinge@abinge-ubuntu:~/workspace$ tree
.
├── classes
│   └── cn
│       └── codecrazy
│           └── App.class
├── lib
│   ├── common-1.jar
│   └── common-2.jar
└── src
    ├── cn
    │   └── codecrazy
    │       └── App.java
    └── my
        └── Hello.java
  • lib目录用来存放外部依赖的类(jar包)
  • src目录中存放我们的应用源文件
  • classes目录中存放我们的应用类编译之后的class文件

App.java的内容如下所示:

package cn.codecrazy;
import my.Hello;

public class App {
  public static void main(String[] args) {
    Hello hello = new Hello();
    System.out.println(hello.sayHello());
  }
}

App.java是我们的主类,该类中引用了my.Hello,而my.Hello的class文件已经打包到lib目录下的common-1.jarcommon-2.jar中了。这两个jar包中都包含了my.Hello.class文件,但是类的内容不一样。common-1.jar中的class文件对应的java源文件为src/my/Hello.java,内容如下:

package my;
public class Hello {
  public String sayHello() {
    return "Hello";
  }
}

该类只有一个方法,sayHello(),而common-2.jar中存在同样的my.Hello.class文件,只不过该class文件对应的源文件如下所示:

package my;
public class Hello {

}

即去掉了sayHello()方法
因此common-1.jarcommon-2.jar中包含了相同的类,但是类的内容不一样。

之后我们用javac命令编译App.java:

javac -d classes src/cn/codecrazy/App.java -classpath lib/common-1.jar:lib/common-2.jar

javac命令的-d选项后面跟着 classes 表示将最终生成的class文件输出到classes目录下面,-classpath后面指定我们javac编译文件时搜索的类路径。如果有多个路径的话中间用冒号分隔(Windows下面是用分号分隔)。

由于我们的App.java中用到了my.Hello,因此需要指定classpath,否则编译的时候会找不到my.Hello这个符号,从而编译失败。

编译完之后会在classes/cn/codecrazy下面生成一个App.class,多出来的cn/codecrazy目录是自动生成的,因为App.java中的包名是cn.codecrazy

编译成功之后,我们再来运行App.class,在workspace目录下面,执行如下命令:

java -classpath classes:lib/common-2.jar:lib/common-1.jar cn.codecrazy.App 

执行过程会报如下错误:

Exception in thread "main" java.lang.NoSuchMethodError: my.Hello.sayHello()Ljava/lang/String;
    at cn.codecrazy.App.main(App.java:7)

重复类报错原因分析

报错原因很明显,是因为运行的时候加载了那个没有sayHello方法的类导致运行时报错,那为什么会去加载那个没有sayHello方法的类呢?这是因为我们执行java命令的时候-classpath 那里将common-2.jar放在了common-1.jar前面,类加载器加载类的时候一旦在前面已经加载到它要加载的my.Hello.class的时候就不会再去加载其他的my.Hello.class类,因此到了执行的时候就会发现加载的那个my.Hello.class类没有sayHello方法,从而报错。

可能有人会说,javac编译的时候common-1.jar就是放在前面的,为什么在java命令这里故意把common-2.jar放前面从而引起报错呢?对于这个问题,大家不要忘了,实际场景中,编译和运行本来就是两个不同的过程,甚至很多是在本地编译,然后放到远程去运行的,这种时候你怎么保证远程执行的时候就会按照你编译时指定的classpath的顺序去运行java程序。

对于我们这个例子,如果在编译的时候就把common-2.jar放在前面,那编译就直接通不过了,因为编译器会发现App.java中调用了my.Hello类中的syaHello方法,但是编译器找到的那个my.Hello类中没有那个方法,因此编译直接报错。

我这个例子中的错误正好是个编译时就能发现的错误,很多错误可能是运行时才能发现的,因此那种情况下,就算是编译和运行时指定的classpath路径顺序一致的话还是会存在编译不报错,运行报错的情况。

从本质上来说,之所以会出现这些问题,就是因为classpath下面存在了相同名称但是内容不一致的类,而正好又是那个有问题的类被加载进JVM中运行。

希望本文能给大家带来一点启发。篇幅所限,有许多相关的主题没有在本文进行更发散和深入的分析,留待以后再进行分享。

您的关注是我不断创作的动力源泉!期待认识更多的朋友,一起交流Java相关技术栈,共同进步!阅读更多技术文章,可关注我的公众号:codecrazy4j
这里写图片描述

### 回答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加载。通过这些方式,我们可以避免加载错误并确保程序的正常运行。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值