Jar hell:如何使用类加载器在运行时将一个 jar 库版本替换为另一个版本
我对 Java 还比较陌生,所以请耐心等待。
我的问题是我的 Java 应用程序依赖于两个库。我们将它们称为库 1 和库 2。这两个库共享对库 3 的相互依赖性。但是:
- 库 1 完全需要库 3 的版本 1。
- 库 2 完全需要库 3 的版本 2。
这正是 < 的定义a href="http://en.wikipedia.org/wiki/Java_Classloader#JAR_hell" rel="noreferrer">JAR 地狱(或至少一种其变体)。 如链接中所述,我无法在同一个类加载器中加载第三个库的两个版本。因此,我一直试图弄清楚是否可以在应用程序中创建一个新的类加载器来解决这个问题。我一直在研究 URLClassLoader,但是我一直无法弄清楚。
下面是一个演示该问题的示例应用程序结构。应用程序的主类 (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版本?
对于这个问题的任何帮助,我将不胜感激。
I'm still relatively new to Java, so please bear with me.
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:
- Library 1 requires exactly version 1 of Library 3.
- Library 2 requires exactly version 2 of Library 3.
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.
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 (original version, before any attempt at a solution):
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.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.
}
}
And then, of course, there are multiple versions of Library3. All they do is print their version numbers:
Version 1 of Library3 (required by Library1):
public class Library3 {
public void printVersion() {
System.out.println("This is version 1.");
}
}
Version 2 of Library3 (required by 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.
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 (new version, including my attempt at a solution):
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.
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."
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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我不敢相信四年多了没有人正确回答这个问题。
https://docs.oracle.com/javase/8 /docs/api/java/lang/ClassLoader.html
Sergei,你的例子的问题是 Library 1,2 & 3 位于默认类路径上,因此作为 URLClassloder 父级的应用程序类加载器能够从库 1,2 和 3 加载类。 3.
如果从类路径中删除库,应用程序类加载器将无法解析其中的类,因此它将把解析委托给其子级 - URLClassLoader。这就是你需要做的。
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
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.
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 中加载 Library1 和 Library2。
(在您当前的代码中,Library2 被加载到一个 URLClassloader 中,该 URLClassloader 的父级是主类加载器 - 它已经加载了 Library1。)
将您的示例更改为如下所示:
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.)
Change your example to something like this:
尝试摆脱
classpath lib2
并通过反射调用bar()
方法:给出以下输出:
这意味着您实际上正在加载
Library2
> 来自classpath
使用默认类加载器,而不是您的自定义URLClassLoader
。Trying to get rid of
classpath lib2
and invoke thebar()
method by reflection:gives following output:
This means you're in fact loading
Library2
fromclasspath
using default classloader, not your customURLClassLoader
.类加载器概念上很简单,但实际上相当复杂,
我建议您不要使用自定义解决方案
,您有一些部分开源解决方案,例如 DCEVM
但也有非常好的商业产品,例如 JRebel
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
but there are also very good commercial product, such as JRebel
我建议使用 JBoss-Modules 的解决方案。
您只需为Library1 创建一个模块:
以类似的方式您可以为Library2 创建一个模块。
然后您可以根据这两个为 Main 创建一个模块:
最后您可以加载 Main 类并通过反射运行它:
您可以下载完整的工作示例 此处
I'd suggest a solution using
JBoss-Modules
.You only need to create a module for Library1:
In a similar way you can create a module for Library2.
And then you can create a module for Main depending in these two:
Finally you can load the Main class and run it through reflection:
You can download the full working example here