在项目中,同时引用不同版本同一个jar包

1,290 阅读5分钟

在面试过程中,面试官问了一个问题“在一个项目中,同时引用不同版本同一个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.jardemo-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插件的主要配置:

  1. **<relocations>** 这个元素用于指定类的重新定位规则,它的子元素 <relocation> 可以有多个。每个 <relocation> 都包含了两个子元素:<pattern><shadedPattern><pattern> 指定了原始的包名,<shadedPattern> 指定了新的包名。当 Shade 插件运行时,它会将所有符合 <pattern> 的类都移动到 <shadedPattern> 指定的包下。
  2. **<filters>** 这个元素用于指定包过滤规则,其子元素 <filter> 可以有多个。每个 <filter> 都包含了 <artifact><includes><excludes> 两个子元素。<artifact> 指定了要过滤的依赖项,<includes><excludes> 指定了要包含或排除的类或资源。
  3. **<transformers>** 这个元素用于指定类转换器,其子元素 <transformer> 可以有多个。每个 <transformer> 都包含一个 <implementation> 子元素,它指定了转换器的实现类。一些常见的转换器包括 org.apache.maven.plugins.shade.resource.ManifestResourceTransformer(用于修改 MANIFEST.MF 文件)和 org.apache.maven.plugins.shade.resource.ServicesResourceTransformer(用于合并 META-INF/services 下的文件)。
  4. **<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包的目的。