Jar 地狱:如何使用类加载器在运行时将一个 jar 库版本替换为另一个 [英] Jar hell: how to use a classloader to replace one jar library version with another at runtime

查看:23
本文介绍了Jar 地狱:如何使用类加载器在运行时将一个 jar 库版本替换为另一个的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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:

  • 库 1 需要库 3 的版本 1.
  • 库 2 需要库 3 的第 2 版.

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

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.

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

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.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 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.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 有多个版本.他们所做的只是打印他们的版本号:

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

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.

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.

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

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 URLClassLoader could be used for this, so here is what I tried:

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 版本,这是意料之中的.

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.

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

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

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

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?

如果您能对此问题提供任何帮助,我将不胜感激.

I would appreciate any help on this problem.

推荐答案

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

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

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

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

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.

谢尔盖,你的例子的问题是图书馆 1,2 &3 位于默认类路径上,因此作为 URLClassloder 父级的应用程序类加载器能够从库 1,2 & 加载类.3.

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.

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

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.

这篇关于Jar 地狱:如何使用类加载器在运行时将一个 jar 库版本替换为另一个的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆