如何为嵌套的 JAR 文件创建自定义 ClassLoader
How to create a custom ClassLoader for nested JAR files
我正在使用一个 Java 库,它在 lib
包中有一些嵌套的 JAR 文件。
我有 2 个问题:
- 我在 IDE 中看不到引用类型(我使用的是 JetBrains IntelliJ)
- 当然我得到 class not defined at runtime
我知道我必须创建和使用自定义 ClassLoader,它会解决这两个问题吗?
这是实现此结果的推荐方法吗?
JAR 文件是意大利政府提供的库,我无法修改它,因为它会随着法规的变化而定期更新。
我不知道你加载 jar 后想做什么。
在我的例子中,对 Servlet 示例使用 jar 动态加载。
try{
final URLClassLoader loader = (URLClassLoader)ClassLoader.getSystemClassLoader();
final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
new File(dir).listFiles(new FileFilter() {
@Override
public boolean accept(File jar) {
// load file if it is 'jar' type
if( jar.toString().toLowerCase().contains(".jar") ){
try {
method.invoke(loader, new Object[]{jar.toURI().toURL()});
XMLog.info_arr(logger, jar, " is loaded.");
JarInputStream jarFile = new JarInputStream(new FileInputStream(jar));
JarEntry jarEntry;
while (true) {
// load jar file
jarEntry = jarFile.getNextJarEntry();
if (jarEntry == null) {
break;
}
// load .class file in loaded jar file
if (jarEntry.getName().endsWith(".class")) {
Class loadedClass = Class.forName(jarEntry.getName().replaceAll("/", "\.").replace(".class",""));
/*
* In my case, I load jar file for Servlet.
* If you want to use it for other case, then change below codes
*/
WebServlet annotaions = (WebServlet) loadedClass.getAnnotation(WebServlet.class);
// load annotation and mapping if it is Servlet
if (annotaions.urlPatterns().length > 0) {
ServletRegistration.Dynamic registration = servletContextEvent.getServletContext().addServlet(annotaions.urlPatterns().toString(), loadedClass);
registration.addMapping(annotaions.urlPatterns());
}
}
}
} catch (Exception e) {
System.err.println("Can't load classes in jar");
}
}
return false;
}
});
} catch(Exception e) {
throw new RuntimeException(e);
}
是的,据我所知,标准的 ClassLoader 不支持嵌套的 JAR。这很可悲,因为这本来是一个非常好的主意,但 Oracle 根本不在乎它。这是一张18岁的票:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4735639
如果您从其他人那里获得这些 JAR,最好的办法是联系供应商并要求他们以兼容标准的格式交付。从你的回答中我意识到这可能很难实现,但我仍然会尝试与他们交谈,因为这是正确的做法。我很确定与您处境相同的其他人也有同样的问题。根据行业标准,这种情况通常会暗示您的供应商将 Maven 存储库用于他们的可交付成果。
如果与您的供应商交谈失败,您可以在获得 JAR 时重新打包。我建议为此编写一个自动化脚本,并确保它在每次交付时得到 运行。您可以将所有 .class 文件放入一个 uber-JAR 中,或者将嵌套的 JAR 移到封闭的 JAR 之外。警告 1:可以有多个 class 具有相同的名称,因此您需要确保选择正确的名称。警告 2:如果 JAR 已签名,您将丢失签名(除非您使用自己的签名)。
选项 3:您始终可以实现自己的类加载器以从任何地方加载 classes,甚至从厨房水槽。
这个人就是这样做的:https://www.ibm.com/developerworks/library/j-onejar/index.html
简短的总结是这样的类加载器必须执行递归解压缩,这有点让人头疼,因为存档基本上是为流访问而不是随机访问而创建的,但除此之外完全可行。
您可以将他的解决方案用作 "wrapper loader",它将取代您的主要 class。
就 IntelliJ IDEA 而言,我认为它不支持开箱即用的功能。最好的办法是如上所述重新打包 JAR 并将它们添加为单独的 class 路径条目,或者搜索是否有人为嵌套 JAR 支持编写了插件。
有趣的是,我刚刚为 JesterJ 解决了这个问题的一个版本,尽管我还有为 jar 文件中的代码加载依赖项的额外要求。 JesterJ(截至今晚的提交!)运行s 来自一个 fat jar,并接受一个参数,该参数表示第二个 fat jar,其中包含 classes、文档摄取计划的依赖项和配置(用户的代码我需要 运行).
我的解决方案的工作方式是我从 Uno-Jar(生成 fat jar 的库)借用了如何在 jar 中加载 jar 的知识,并将我自己的 classloader 放在上面控制 class 加载程序的评估顺序。
https://github.com/nsoft/jesterj/blob/jdk11/code/ingest/src/main/java/org/jesterj/ingest/Main.java 中的关键位如下所示:
JesterJLoader jesterJLoader;
File jarfile = new File(javaConfig);
URL planConfigJarURL;
try {
planConfigJarURL = jarfile.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e); // boom
}
jesterJLoader = (JesterJLoader) ClassLoader.getSystemClassLoader();
ClassLoader loader;
if (isUnoJar) {
JarClassLoader jarClassLoader = new JarClassLoader(jesterJLoader, planConfigJarURL.toString());
jarClassLoader.load(null);
loader = jarClassLoader;
} else {
loader = new URLClassLoader(new URL[]{planConfigJarURL}, jesterJLoader);
}
jesterJLoader.addExtLoader(loader);
我的 JesterJLoader 在这里:
虽然如果你愿意简单地委托并依赖主 class 路径上的现有 classes(而不是像我一样从 sub-fat-jar 加载额外的依赖项做)你的可能要简单得多。我付出了很多努力让它首先检查子 jar 而不是立即委托给父 jar,然后必须跟踪发送到子 jar 的内容以避免循环和随后的 WhosebugError ...
另请注意,我获取系统 class 加载程序的行不会是您想要的,我还在使用系统加载程序来解决我的一些依赖项的不礼貌问题使用 class 加载。
如果您决定尝试查看 Uno-Jar,请注意,此嵌套场景的资源加载可能还不稳定,并且在 https://github.com/nsoft/uno-jar/commit/cf5af42c447c22edb9bbc6bd08293f0c23db86c2
之前肯定无法正常工作
另外:最近提交了经过精简测试的代码警告:)
披露:我同时维护 JesterJ 和 Uno-Jar(One-JAR 的一个分支,jurez 提供的 link 中的特色库)并欢迎任何错误报告或评论甚至投稿!
我正在使用一个 Java 库,它在 lib
包中有一些嵌套的 JAR 文件。
我有 2 个问题:
- 我在 IDE 中看不到引用类型(我使用的是 JetBrains IntelliJ)
- 当然我得到 class not defined at runtime
我知道我必须创建和使用自定义 ClassLoader,它会解决这两个问题吗?
这是实现此结果的推荐方法吗?
JAR 文件是意大利政府提供的库,我无法修改它,因为它会随着法规的变化而定期更新。
我不知道你加载 jar 后想做什么。 在我的例子中,对 Servlet 示例使用 jar 动态加载。
try{
final URLClassLoader loader = (URLClassLoader)ClassLoader.getSystemClassLoader();
final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
new File(dir).listFiles(new FileFilter() {
@Override
public boolean accept(File jar) {
// load file if it is 'jar' type
if( jar.toString().toLowerCase().contains(".jar") ){
try {
method.invoke(loader, new Object[]{jar.toURI().toURL()});
XMLog.info_arr(logger, jar, " is loaded.");
JarInputStream jarFile = new JarInputStream(new FileInputStream(jar));
JarEntry jarEntry;
while (true) {
// load jar file
jarEntry = jarFile.getNextJarEntry();
if (jarEntry == null) {
break;
}
// load .class file in loaded jar file
if (jarEntry.getName().endsWith(".class")) {
Class loadedClass = Class.forName(jarEntry.getName().replaceAll("/", "\.").replace(".class",""));
/*
* In my case, I load jar file for Servlet.
* If you want to use it for other case, then change below codes
*/
WebServlet annotaions = (WebServlet) loadedClass.getAnnotation(WebServlet.class);
// load annotation and mapping if it is Servlet
if (annotaions.urlPatterns().length > 0) {
ServletRegistration.Dynamic registration = servletContextEvent.getServletContext().addServlet(annotaions.urlPatterns().toString(), loadedClass);
registration.addMapping(annotaions.urlPatterns());
}
}
}
} catch (Exception e) {
System.err.println("Can't load classes in jar");
}
}
return false;
}
});
} catch(Exception e) {
throw new RuntimeException(e);
}
是的,据我所知,标准的 ClassLoader 不支持嵌套的 JAR。这很可悲,因为这本来是一个非常好的主意,但 Oracle 根本不在乎它。这是一张18岁的票:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4735639
如果您从其他人那里获得这些 JAR,最好的办法是联系供应商并要求他们以兼容标准的格式交付。从你的回答中我意识到这可能很难实现,但我仍然会尝试与他们交谈,因为这是正确的做法。我很确定与您处境相同的其他人也有同样的问题。根据行业标准,这种情况通常会暗示您的供应商将 Maven 存储库用于他们的可交付成果。
如果与您的供应商交谈失败,您可以在获得 JAR 时重新打包。我建议为此编写一个自动化脚本,并确保它在每次交付时得到 运行。您可以将所有 .class 文件放入一个 uber-JAR 中,或者将嵌套的 JAR 移到封闭的 JAR 之外。警告 1:可以有多个 class 具有相同的名称,因此您需要确保选择正确的名称。警告 2:如果 JAR 已签名,您将丢失签名(除非您使用自己的签名)。
选项 3:您始终可以实现自己的类加载器以从任何地方加载 classes,甚至从厨房水槽。
这个人就是这样做的:https://www.ibm.com/developerworks/library/j-onejar/index.html
简短的总结是这样的类加载器必须执行递归解压缩,这有点让人头疼,因为存档基本上是为流访问而不是随机访问而创建的,但除此之外完全可行。
您可以将他的解决方案用作 "wrapper loader",它将取代您的主要 class。
就 IntelliJ IDEA 而言,我认为它不支持开箱即用的功能。最好的办法是如上所述重新打包 JAR 并将它们添加为单独的 class 路径条目,或者搜索是否有人为嵌套 JAR 支持编写了插件。
有趣的是,我刚刚为 JesterJ 解决了这个问题的一个版本,尽管我还有为 jar 文件中的代码加载依赖项的额外要求。 JesterJ(截至今晚的提交!)运行s 来自一个 fat jar,并接受一个参数,该参数表示第二个 fat jar,其中包含 classes、文档摄取计划的依赖项和配置(用户的代码我需要 运行).
我的解决方案的工作方式是我从 Uno-Jar(生成 fat jar 的库)借用了如何在 jar 中加载 jar 的知识,并将我自己的 classloader 放在上面控制 class 加载程序的评估顺序。
https://github.com/nsoft/jesterj/blob/jdk11/code/ingest/src/main/java/org/jesterj/ingest/Main.java 中的关键位如下所示:
JesterJLoader jesterJLoader;
File jarfile = new File(javaConfig);
URL planConfigJarURL;
try {
planConfigJarURL = jarfile.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e); // boom
}
jesterJLoader = (JesterJLoader) ClassLoader.getSystemClassLoader();
ClassLoader loader;
if (isUnoJar) {
JarClassLoader jarClassLoader = new JarClassLoader(jesterJLoader, planConfigJarURL.toString());
jarClassLoader.load(null);
loader = jarClassLoader;
} else {
loader = new URLClassLoader(new URL[]{planConfigJarURL}, jesterJLoader);
}
jesterJLoader.addExtLoader(loader);
我的 JesterJLoader 在这里:
虽然如果你愿意简单地委托并依赖主 class 路径上的现有 classes(而不是像我一样从 sub-fat-jar 加载额外的依赖项做)你的可能要简单得多。我付出了很多努力让它首先检查子 jar 而不是立即委托给父 jar,然后必须跟踪发送到子 jar 的内容以避免循环和随后的 WhosebugError ...
另请注意,我获取系统 class 加载程序的行不会是您想要的,我还在使用系统加载程序来解决我的一些依赖项的不礼貌问题使用 class 加载。
如果您决定尝试查看 Uno-Jar,请注意,此嵌套场景的资源加载可能还不稳定,并且在 https://github.com/nsoft/uno-jar/commit/cf5af42c447c22edb9bbc6bd08293f0c23db86c2
之前肯定无法正常工作另外:最近提交了经过精简测试的代码警告:)
披露:我同时维护 JesterJ 和 Uno-Jar(One-JAR 的一个分支,jurez 提供的 link 中的特色库)并欢迎任何错误报告或评论甚至投稿!