Jar hell:如何使用类加载器在运行时用另一个jar库版本替换另一个版本

2022-09-01 11:06:07

我对Java还比较陌生,所以请耐心等待。

我的问题是我的Java应用程序依赖于两个库。让我们称它们为库 1 和库 2。这两个库都相互依赖库 3。然而:

  • 库 1 正好需要库 3 的版本 1。
  • 库 2 正好需要库 3 的版本 2。

这正是JAR地狱的定义(或者至少是它的一个变体)。如链接中所述,我无法在同一类加载器中加载第三个库的两个版本。因此,我一直在试图弄清楚我是否可以在应用程序中创建一个新的类加载器来解决这个问题。我一直在研究URLClassLoader,但我无法弄清楚。

下面是演示问题的示例应用程序结构。应用程序的 Main 类 (Main.java) 尝试实例化 Library1 和 Library2,并运行这些库中定义的一些方法:

Main.java(原始版本,在任何解决方案尝试之前):

public class Main {
    public static void main(String[] args) {
        Library1 lib1 = new Library1();
        lib1.foo();

        Library2 lib2 = new Library2();
        lib2.bar();
    }
}

库 1 和库 2 都共享对库 3 的相互依赖关系,但库 1 正好需要版本 1,而库 2 恰好需要版本 2。在此示例中,这两个库都只打印它们看到的 Library3 版本:

库1.java:

public class Library1 {
  public void foo() {
    Library3 lib3 = new Library3();
    lib3.printVersion();    // Should print "This is version 1."
  }
}

库2.java:

public class Library2 {
  public void foo() {
    Library3 lib3 = new Library3();
    lib3.printVersion();    // Should print "This is version 2." if the correct version of Library3 is loaded.
  }
}

当然,还有多个版本的Library3。他们所做的就是打印他们的版本号:

库 3 的版本 1(库 1 要求):

public class Library3 {
  public void printVersion() {
    System.out.println("This is version 1.");
  }
}

库 3 的版本 2(库 2 要求):

public class Library3 {
  public void printVersion() {
    System.out.println("This is version 2.");
  }
}

当我启动应用程序时,类路径包含 Library1 (lib1.jar)、Library2 (lib2.jar) 和 Library 3 的版本 1 (lib3-v1/lib3.jar)。这对库 1 来说很好,但对库 2 不起作用。

我需要做的是在实例化 Library2 之前替换出现在类路径上的 Library3 版本。我的印象是URLClassLoader可以用于此目的,所以这是我尝试过的:

Main.java(新版本,包括我对解决方案的尝试):

import java.net.*;
import java.io.*;

public class Main {
  public static void main(String[] args)
    throws MalformedURLException, ClassNotFoundException,
          IllegalAccessException, InstantiationException,
          FileNotFoundException
  {
    Library1 lib1 = new Library1();
    lib1.foo();     // This causes "This is version 1." to print.

    // Original code:
    // Library2 lib2 = new Library2();
    // lib2.bar();

    // However, we need to replace Library 3 version 1, which is
    // on the classpath, with Library 3 version 2 before attempting
    // to instantiate Library2.

    // Create a new classloader that has the version 2 jar
    // of Library 3 in its list of jars.
    URL lib2_url = new URL("file:lib2/lib2.jar");        verifyValidPath(lib2_url);
    URL lib3_v2_url = new URL("file:lib3-v2/lib3.jar");  verifyValidPath(lib3_v2_url);
    URL[] urls = new URL[] {lib2_url, lib3_v2_url};
    URLClassLoader c = new URLClassLoader(urls);

    // Try to instantiate Library2 with the new classloader    
    Class<?> cls = Class.forName("Library2", true, c);
    Library2 lib2 = (Library2) cls.newInstance();

    // If it worked, this should print "This is version 2."
    // However, it still prints that it's version 1. Why?
    lib2.bar();
  }

  public static void verifyValidPath(URL url) throws FileNotFoundException {
    File filePath = new File(url.getFile());
    if (!filePath.exists()) {
      throw new FileNotFoundException(filePath.getPath());
    }
  }
}

当我运行此命令时,会导致打印“这是版本 1”。由于这是应用程序启动时类路径上的 Library3 版本,因此这是预料之中的。lib1.foo()

但是,我本来以为打印“这是版本 2”,反映出 Library3 的新版本已加载,但它仍然打印“这是版本 1”。lib2.bar()

为什么使用加载了正确jar版本的新类加载器仍然会导致使用旧的jar版本?我做错了什么吗?还是我不理解类加载器背后的概念?如何在运行时正确切换库 3 的 jar 版本?

我将不胜感激有关此问题的任何帮助。


答案 1

我不敢相信,超过4年的时间里,没有人正确回答这个问题。

https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html

类使用委派模型来搜索类和资源。类装入器的每个实例都有一个关联的父类装入器。当请求查找类或资源时,ClassLoader 实例会先将类或资源的搜索委托给其父类装入器,然后再尝试查找类或资源本身。虚拟机的内置类装入器(称为“引导类装入器”)本身没有父级,但可以用作类装入器实例的父级。

Sergei,你的例子的问题在于库1,2和3在默认的类路径上,所以作为URLClassloder父级的应用程序类加载器能够从库1,2和3加载类。

如果从类路径中删除库,则应用程序类装入器将无法从中解析类,因此它将把解析委托给其子级 - URLClassLoader。所以这就是你需要做的。


答案 2

您需要在单独的 URLClassloaders 中加载 Library1 和 Library2。(在你当前的代码中,Library2 被加载到一个 URLClassloader 中,该 URLClassloader 的父级是主类加载器 - 它已经加载了 Library1。

将示例更改为如下所示:

URL lib1_url = new URL("file:lib1/lib1.jar");        verifyValidPath(lib1_url);
URL lib3_v1_url = new URL("file:lib3-v1/lib3.jar");  verifyValidPath(lib3_v1_url);
URL[] urls1 = new URL[] {lib1_url, lib3_v21_url};
URLClassLoader c1 = new URLClassLoader(urls1);

Class<?> cls1 = Class.forName("Library1", true, c);
Library1 lib1 = (Library1) cls1.newInstance();    


URL lib2_url = new URL("file:lib2/lib2.jar");        verifyValidPath(lib2_url);
URL lib3_v2_url = new URL("file:lib3-v2/lib3.jar");  verifyValidPath(lib3_v2_url);
URL[] urls2 = new URL[] {lib2_url, lib3_v2_url};
URLClassLoader c2 = new URLClassLoader(url2s);


Class<?> cls2 = Class.forName("Library2", true, c);
Library2 lib2 = (Library2) cls2.newInstance();

推荐