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

时间:2011-08-02 08:38:25

标签: java jar classpath classloader

我还是比较新的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()打印“这是版本2”,反映新版本的Library3已加载,但它仍会打印“这是版本1”。

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

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

7 个答案:

答案 0 :(得分:7)

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

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

  

ClassLoader类使用委托模型来搜索类   和资源。 ClassLoader的每个实例都有一个关联的父级   类加载器。当要求查找课程或资源时,   ClassLoader实例将委派对类的搜索   在尝试查找之前,将资源添加到其父类加载器   类或资源本身。虚拟机的内置类加载器,   称为&#34; bootstrap类加载器&#34;,本身不具有父级但是   可以作为ClassLoader实例的父级。

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

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

答案 1 :(得分:2)

您需要在单独的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();

答案 2 :(得分: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)

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

答案 3 :(得分:0)

classloader在概念上很简单,但实际上非常复杂

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

您有一些部分开源解决方案,例如DCEVM

但也有非常好的商业产品,例如JRebel

答案 4 :(得分:0)

使用jar class loader可以在运行时从jar文件加载类。

答案 5 :(得分:0)

您可以使用ParentLastClassloader来解决Jar Hell。请检查this blog post

答案 6 :(得分:0)

我建议使用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