如何为嵌套的 JAR 文件创建自定义 ClassLoader

How to create a custom ClassLoader for nested JAR files

我正在使用一个 Java 库,它在 lib 包中有一些嵌套的 JAR 文件。

我有 2 个问题:

  1. 我在 IDE 中看不到引用类型(我使用的是 JetBrains IntelliJ)
  2. 当然我得到 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 在这里:

https://github.com/nsoft/jesterj/blob/jdk11/code/ingest/src/main/java/org/jesterj/ingest/utils/JesterJLoader.java

虽然如果你愿意简单地委托并依赖主 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 中的特色库)并欢迎任何错误报告或评论甚至投稿!