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

Ser*_*y K 31 java jar classpath classloader

我还是比较新的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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Library1和Library2都共享对Library3的相互依赖,但是Library1需要版本1,而Library2只需要版本2.在这个例子中,这两个库只打印他们看到的Library3版本:

Library1.java:

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

Library2.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.
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,当然,Library3有多个版本.他们所做的只是打印他们的版本号:

Library3的第1版(Library1要求):

public class Library3 {
  public void printVersion() {
    System.out.println("This is version 1.");
  }
}
Run Code Online (Sandbox Code Playgroud)

Library3的第2版(Library2要求):

public class Library3 {
  public void printVersion() {
    System.out.println("This is version 2.");
  }
}
Run Code Online (Sandbox Code Playgroud)

当我启动应用程序时,类路径包含Library1(lib1.jar),Library2(lib2.jar)和Library 3的版本1(lib3-v1/lib3.jar).这适用于Library1,但它不适用于Library2.

我在某种程度上需要做的是在实例化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());
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

当我运行它时,lib1.foo()导致"这是版本1".打印.因为这是应用程序启动时类路径上的Library3的版本,所以这是预期的.

但是,我原本lib2.bar()打算打印"这是版本2",反映新版本的Library3已经加载,但它仍然打印出"这是版本1".

为什么使用加载了正确jar版本的新类加载器仍会导致使用旧的jar版本?难道我做错了什么?或者我不理解类加载器背后的概念?如何在运行时正确切换jar3的jar版本?

我很感激这个问题的任何帮助.

mdz*_*dzh 8

我无法相信,超过4年没有人正确回答这个问题.

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

ClassLoader类使用委派模型来搜索类和资源.ClassLoader的每个实例都有一个关联的父类加载器.当请求查找类或资源时,ClassLoader实例会在尝试查找类或资源本身之前,将对类或资源的搜索委托给其父类加载器.虚拟机的内置类加载器(称为"引导类加载器")本身不具有父级,但可以作为ClassLoader实例的父级.

谢尔盖,你的例子的问题是库1,2和3是在默认的类路径上,因此作为URLClassloder的父类的Application类加载器能够从库1,2和3加载类.

如果从类路径中删除库,则Application类加载器将无法从中解析类,因此它会将重新解析委托给其子级 - URLClassLoader.这就是你需要做的.


Vla*_*lad 1

尝试通过反射摆脱classpath lib2并调用该bar()方法:

try {
    cls.getMethod("bar").invoke(cls.newInstance());
} catch (Exception e) {
    e.printStackTrace();
}
Run Code Online (Sandbox Code Playgroud)

给出以下输出:

Exception in thread "main" java.lang.ClassNotFoundException: Library2
    at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:247)
    at Main.main(Main.java:36)
Run Code Online (Sandbox Code Playgroud)

这意味着您实际上是使用默认类加载器加载Library2的,而不是自定义的。classpathURLClassLoader