在面试过程中,面试官问了一个问题“在一个项目中,同时引用不同版本同一个jar包”,当时只想到了和类加载过程,双亲委派机制有关,但是具体怎么实现并没有答出来。回来了之后搜索了相关资料,发现有2种解决方案,分别是使用不同的 ClassLoader和Maven或Gradle中的 Shade 插件对类进行重新定位。
准备工作
举个例子,类com.ldbmcs.Printer在1.0版本和2.0版本的实现不一样。
// demo-1.0.jar
public class Printer {
public static void print() {
System.out.println("Hello world! Main1");
}
}
// demo-2.0.jar
public class Printer {
public static void print() {
System.out.println("Hello world! Main2");
}
}
现在我们需要在项目中同时引用demo-1.0.jar和demo-2.0.jar。
ClassLoader
如果你想要使用不同的 ClassLoader 来加载这两个包含 com.ldbmcs 包的 JAR 文件,你可以创建两个 URLClassLoader 的实例,每个实例加载一个 JAR 文件。以下是一个示例:
public class Main {
public static void main(String[] args) {
String projectPath = System.getProperty("user.dir");
try {
invokePrintMethod(projectPath + "/libs/demo-1.0.jar", "com.ldbmcs.Printer");
invokePrintMethod(projectPath + "/libs/demo-2.0.jar", "com.ldbmcs.Printer");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void invokePrintMethod(String jarPath, String className) throws Exception {
URL[] jarUrls = {new File(jarPath).toURI().toURL()};
try (URLClassLoader loader = new URLClassLoader(jarUrls)) {
Class<?> clazz = loader.loadClass(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("print");
method.invoke(instance);
}
}
}
这段代码的关键在于使用了URLClassLoader来加载不同版本的相同JAR包。每个URLClassLoader实例都有自己的命名空间,它们之间是相互隔离的。这意味着每个URLClassLoader都可以加载自己的类版本,而不会受到其他URLClassLoader加载的类的影响。
在这个示例中,我们创建了两个不同的URLClassLoader实例,每个实例分别加载一个不同版本的JAR文件。由于它们是独立的,所以它们可以各自加载自己的类,而不会冲突。
然后,我们用反射来调用这些类的方法。由于反射是在运行时动态调用方法,所以它可以处理在编译时无法解决的版本冲突问题。
总的来说,通过使用不同的URLClassLoader实例和反射,我们可以在同一个项目中使用不同版本的相同JAR包,而不会出现类冲突的问题。
Shade插件
Maven的Shade 插件
Maven Shade 插件是一个强大的工具,可以将你的项目构建产物和其依赖打包到一个 uber-jar 中,并且可以改变包的命名空间,以避免类名冲突。它还包含了其他一些高级功能,如包过滤、类转换等。
Uber-JAR,也被称为fat JAR或者shadow JAR,是一种将应用程序的所有依赖项打包到一个单一的、可执行的 JAR 文件的方式。这种 JAR 文件包含了应用程序代码以及所有的依赖库,所以只需要这一个 JAR 文件就可以运行整个应用程序。
下面是一些Maven Shade插件的主要配置:
**<relocations>**这个元素用于指定类的重新定位规则,它的子元素<relocation>可以有多个。每个<relocation>都包含了两个子元素:<pattern>和<shadedPattern>。<pattern>指定了原始的包名,<shadedPattern>指定了新的包名。当 Shade 插件运行时,它会将所有符合<pattern>的类都移动到<shadedPattern>指定的包下。**<filters>**这个元素用于指定包过滤规则,其子元素<filter>可以有多个。每个<filter>都包含了<artifact>和<includes>或<excludes>两个子元素。<artifact>指定了要过滤的依赖项,<includes>和<excludes>指定了要包含或排除的类或资源。**<transformers>**这个元素用于指定类转换器,其子元素<transformer>可以有多个。每个<transformer>都包含一个<implementation>子元素,它指定了转换器的实现类。一些常见的转换器包括org.apache.maven.plugins.shade.resource.ManifestResourceTransformer(用于修改 MANIFEST.MF 文件)和org.apache.maven.plugins.shade.resource.ServicesResourceTransformer(用于合并META-INF/services下的文件)。**<artifactSet>**这个元素用于指定要包含在 uber-jar 中的依赖项,其子元素<includes>和<excludes>可以用来指定要包含或排除的依赖项。
以上是一些常见的配置,但是 Shade 插件还有更多的高级功能和配置。更详细的信息可以参考 Maven Shade 插件的官方文档 ↗。
所以,我们就可以通过**relocation**配置项来重新定位相同的类到不同的包下。
以下是如何在 Maven 的 pom.xml 文件中配置 Shade 插件的示例:
在此之前,我已经将jar包通过
install命名部署到了本地仓库。
<dependencies>
<dependency>
<groupId>com.ldbmcs</groupId>
<artifactId>demo</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.ldbmcs</pattern>
<shadedPattern>com.empty</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
在这个配置中,我们在一个新项目中添加了demo的1.0版本的依赖,然后在build配置中,我们定义了一个 <relocation> 元素。<pattern> 元素定义了原始的包名com.ldbmcs,<shadedPattern> 元素定义了新的包名com.empty。当 Shade 插件运行时,它会将包com.ldbmcs 下的所有类移动到 com.empty中。
然后,当你运行 mvn package 命令时,Maven 将会创建一个包含了你的项目和所有依赖的 JAR。这个 JAR 被称为 uber JAR,你可以像运行普通的 JAR 一样运行它。这个 uber JAR 中的类将被重新定位到新的包下,这样就避免了类名冲突。
Gradle的Shade插件
在Gradle中,如果你需要将两个包含相同包路径的JAR文件合并到一个Uber JAR文件中,并解决类名冲突的问题,你可以使用Shadow插件的类重定位(Relocation)功能。
配置如下:
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
repositories {
mavenLocal()
}
dependencies {
implementation("com.ldbmcs:demo:1.0")
}
shadowJar {
relocate 'com.ldbmcs', 'com.gradle'
}
jar {
from shadowJar.archiveFileName
}
在这个例子中,Shadow插件将把 com.ldbmcs 包的所有类都重定位到 com.gradle 包下,这样就可以避免类名冲突的问题。
然后,你可以通过以下命令来创建Uber JAR:
./gradlew shadowJar
这将生成一个包含所有依赖项的Uber JAR文件,就达到了在一个项目中同时使用不同版本的相同jar包的目的。