【问题标题】:Jar hell: how to use a classloader to replace one jar library version with another at runtimeJar hell:如何在运行时使用类加载器将一个 jar 库版本替换为另一个版本
【发布时间】:2011-10-18 01:37:48
【问题描述】:

我对 Java 还是比较陌生,所以请多多包涵。

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

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

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

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

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

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

Library3 的版本 1(Library1 需要):

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

Library3 的第 2 版(Library2 需要):

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)。这适用于 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());
    }
  }
}

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

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

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

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

【问题讨论】:

标签: java jar classpath classloader


【解决方案1】:

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

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

给出以下输出:

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)

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

【讨论】:

    【解决方案2】:

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

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

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

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

    【讨论】:

      【解决方案3】:

      您需要在单独的 URLClassloader 中加载 Library1 和 Library2。 (在您当前的代码中,Library2 被加载到 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();
      

      【讨论】:

      • 它不起作用。一旦您尝试运行程序,就会出现 java.lang.NoClassDefFoundError 错误。我认为正如@janos 所提到的,Library3 实例化是使用默认类加载器执行的。
      【解决方案4】:

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

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

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

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

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

      【讨论】:

        【解决方案5】:

        我建议使用JBoss-Modules 的解决方案。

        你只需要为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());
        

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

        然后你可以根据这两个为 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());
        

        最后你可以加载 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]);
        

        您可以下载完整的工作示例here

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-11-01
          • 1970-01-01
          • 2023-02-22
          • 2018-03-14
          • 1970-01-01
          • 1970-01-01
          • 2017-06-24
          • 2019-07-20
          相关资源
          最近更新 更多