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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6909306/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-30 17:52:14  来源:igfitidea点击:

Jar hell: how to use a classloader to replace one jar library version with another at runtime

javajarclasspathclassloader

提问by Sergey K

I'm still relatively new to Java, so please bear with me.

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

My issue is that my Java application depends on two libraries. Let's call them Library 1 and Library 2. Both of these libraries share a mutual dependency on Library 3. However:

我的问题是我的 Java 应用程序依赖于两个库。我们称它们为库 1 和库 2。这两个库共享对库 3 的相互依赖。但是:

  • Library 1 requires exactly version 1 of Library 3.
  • Library 2 requires exactly version 2 of Library 3.
  • 库 1 正好需要库 3 的版本 1。
  • 库 2 正好需要库 3 的版本 2。

This is exactly the definition of JAR hell(or at least one its variations). As stated in the link, I can't load both versions of the third library in the same classloader. Thus, I've been trying to figure out if I could create a new classloader within the application to solve this problem. I've been looking into URLClassLoader, but I've not been able to figure it out.

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

Here's an example application structure that demonstrates the problem. The Main class (Main.java) of the application tries to instantiate both Library1 and Library2 and run some method defined in those libraries:

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

Main.java (original version, before any attempt at a solution):

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

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

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

Library1 and Library2 both share a mutual dependency on Library3, but Library1 requires exactly version 1, and Library2 requires exactly version 2. In the example, both of these libraries just print the version of Library3 that they see:

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

Library1.java:

库1.java:

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

Library2.java:

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.
  }
}

And then, of course, there are multiple versions of Library3. All they do is print their version numbers:

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

Version 1 of Library3 (required by Library1):

Library3 的版本 1(Library1 需要):

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

Version 2 of Library3 (required by Library2):

Library3 的第 2 版(Library2 需要):

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

When I launch the application, the classpath contains Library1 (lib1.jar), Library2 (lib2.jar), and version 1 of Library 3 (lib3-v1/lib3.jar). This works out fine for Library1, but it won't work for Library2.

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

What I somehow need to do is replace the version of Library3 that appears on the classpath before instantiating Library2. I was under the impression that URLClassLoadercould be used for this, so here is what I tried:

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

Main.java (new version, including my attempt at a solution):

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());
    }
  }
}

When I run this, lib1.foo()causes "This is version 1." to be printed. Since that's the version of Library3 that's on the classpath when the application starts, this is expected.

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

However, I was expecting lib2.bar()to print "This is version 2.", reflecting that the new version of Library3 got loaded, but it still prints "This is version 1."

然而,我期待lib2.bar()打印“This is version 2.”,反映Library3的新版本已加载,但它仍然打印“This is version 1”。

Why is it that using the new classloader with the right jar version loaded still results in the old jar version being used? Am I doing something wrong? Or am I not understanding the concept behind classloaders? How can I switch jar versions of Library3 correctly at runtime?

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

I would appreciate any help on this problem.

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

回答by mdzh

I can't believe that for more than 4 years no one has answered this question correctly.

我不敢相信 4 年多来没有人正确回答这个问题。

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

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

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.

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

Sergei, the problem with your example was that Library 1,2 & 3 were on the default class path, so the Application classloader which was the parent of your URLClassloder was able to load the classes from Library 1,2 & 3.

Sergei,您的示例的问题在于库 1,2 和 3 位于默认类路径上,因此作为 URLClassloder 父级的应用程序类加载器能够加载库 1,2 和 3 中的类。

If youremove the libraries from the classpath, the Application classloader won't be able to resolve classes from them so it will delegate resolvation to its child - the URLClassLoader. So that is what you need to do.

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

回答by Dave DiFranco

You need to load both Library1 and Library2 in separate URLClassloaders. (In your current code, Library2 is loaded in a URLClassloader whose parent is the main classloader - which has already loaded Library1.)

您需要在单独的 URLClassloader 中加载 Library1 和 Library2。(在您当前的代码中,Library2 加载到 URLClassloader 中,其父类是主类加载器 - 已经加载了 Library1。)

Change your example to something like this:

将您的示例更改为如下所示:

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();

回答by Vlad

Trying to get rid of classpath lib2and invoke the bar()method by reflection:

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

try {
    cls.getMethod("bar").invoke(cls.newInstance());
} catch (Exception e) {
    e.printStackTrace();
}

gives following output:

给出以下输出:

Exception in thread "main" java.lang.ClassNotFoundException: Library2
    at java.net.URLClassLoader.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)

This means you're in fact loading Library2from classpathusing default classloader, not your custom URLClassLoader.

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

回答by wikier

classloader are something simple in concept, but actually quite complex

类加载器在概念上很简单,但实际上相当复杂

I recommend you not to use a custom solution

我建议您不要使用自定义解决方案

you have some partial open source solutions, such as DCEVM

你有一些部分开源的解决方案,比如DCEVM

but there are also very good commercial product, such as JRebel

但是也有很好的商业产品,比如JRebel

回答by fonkap

I'd suggest a solution using JBoss-Modules.

我建议使用JBoss-Modules.

You only need to create a module for Library1:

你只需要为 Library1 创建一个模块:

    final ModuleIdentifier module1Id = ModuleIdentifier.fromString("library1");
    ModuleSpec.Builder moduleBuilder = ModuleSpec.build(module1Id);
    JarFile jarFile = new JarFile("lib/lib3-v1/lib3.jar", true);
    ResourceLoader rl1 = ResourceLoaders.createJarResourceLoader("lib3-v1", jarFile);
    moduleBuilder.addResourceRoot(ResourceLoaderSpec.createResourceLoaderSpec(
            rl1
            ));
    moduleBuilder.addResourceRoot(ResourceLoaderSpec.createResourceLoaderSpec(
            TestResourceLoader.build()
            .addClass(Library1.class)
            .create()
            ));
    moduleBuilder.addDependency(DependencySpec.createLocalDependencySpec());
    moduleLoader.addModuleSpec(moduleBuilder.create());

In a similar way you can create a module for Library2.

以类似的方式,您可以为 Library2 创建一个模块。

And then you can create a module for Main depending in these two:

然后你可以根据这两个为 Main 创建一个模块:

    //Building main module
    final ModuleIdentifier moduleMainId = ModuleIdentifier.fromString("main");
    moduleBuilder = ModuleSpec.build(moduleMainId);
    moduleBuilder.addResourceRoot(ResourceLoaderSpec.createResourceLoaderSpec(
            TestResourceLoader.build()
            .addClass(Main.class)
            .create()
            ));
    //note the dependencies
    moduleBuilder.addDependency(DependencySpec.createModuleDependencySpec(module1Id, true, false));
    moduleBuilder.addDependency(DependencySpec.createModuleDependencySpec(module2Id, true, false));
    moduleBuilder.addDependency(DependencySpec.createLocalDependencySpec());
    moduleLoader.addModuleSpec(moduleBuilder.create());

Finally you can load the Main class and run it through reflection:

最后,您可以加载 Main 类并通过反射运行它:

    Module moduleMain = moduleLoader.loadModule(moduleMainId);
    Class<?> m = moduleMain.getClassLoader().loadClass("tmp.Main");
    Method method = m.getMethod("main", String[].class);
    method.invoke(null, (Object) new String[0]);

You can download the full working example here

您可以在此处下载完整的工作示例