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

我还是比较新的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(); } } 

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版本?

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

我无法相信,超过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。 这就是你需要做的。

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

试图摆脱classpath lib2并通过reflection调用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

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

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

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

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

使用jar类加载器 ,可用于在运行时从jar文件加载类。

您可以使用ParentLastClassloader来解决Jar Hell。 请查看此博客文章

我建议使用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类并通过reflection运行它:

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

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