运行时加载jar文件导致NoClassDefFoundError/ClassNotFoundException错误

6
摘要:从正在运行的Java程序加载jar文件会导致NoClassDefFoundError错误,这是由于类之间的依赖关系(例如import语句)导致的ClassNotFoundException。我该怎么解决它? 更详细的问题描述: 我正试图通过自己的Java程序(称为“ServerAPI”)以编程方式将一个名为“Server”的jar文件加载到Java虚拟机中,并使用扩展和其他技巧修改并与Server交互。ServerAPI依赖于Server,但如果Server不存在,ServerAPI仍然必须能够运行并从网站下载Server。
为了避免ServerAPI在没有满足其对Server的依赖关系的情况下加载而导致的错误,我创建了一个启动器(称为“Launcher”),旨在下载Server并根据需要设置ServerAPI,然后加载Server和ServerAPI,最后运行ServerAPI。
然而,当我尝试从Launcher加载jar文件时,我会遇到错误,因为ClassLoader无法解析它正在加载的类所依赖的文件中的其他类。简而言之,如果我尝试加载类A,则如果A导入B,它将引发错误,因为我还没有加载B。然而,如果B也导入A,我就卡住了,因为我无法弄清如何同时加载两个类或如何加载一个类而不运行其验证。 为什么所有限制都导致了这个问题: 我试图修改和添加Server的行为,但由于复杂的法律原因,我不能直接修改该程序,因此我创建了依赖于Server并可以从外部调整Server行为的ServerAPI。
但是,由于更复杂的法律原因,Server和ServerAPI不能简单地一起下载。必须下载Launcher(见上文)与ServerAPI一起下载,然后Launcher需要下载Server。最后,ServerAPI可以使用Server作为依赖项运行。这就是为什么这个问题如此复杂。
这个问题也将适用于项目的后续部分,该部分涉及基于插件的API接口,需要能够在运行时从jar文件中加载和卸载插件。 我已经对这个问题进行了研究: 我已经阅读并未得到帮助:
  • 这个问题只解决了单个方法的问题,没有解决类之间依赖错误的问题;
  • 这个问题无法解决我的问题,因为我不能每次加载或卸载jar时都关闭并重新启动程序(主要是对插件部分做了简短的提及);
  • 这个问题只适用于程序启动时存在依赖关系的情况;
  • 这个问题和第2个问题一样;
  • 这个问题和第3个问题一样;
  • 这篇文章让我了解了隐藏的loadClass(String, boolean)方法,但使用truefalse值尝试后并没有帮助;
  • 这个问题和第1个问题一样;

还有更多的尝试,但都没有成功。

//编辑: 我已经尝试过以下方法:

我尝试使用URLClassLoader来加载jar文件,使用JarFile中的JarEntries,类似于这个问题。我尝试了使用URLClassLoaderloadClass(String)方法和创建一个扩展了URLClassLoader的类,以便我可以利用loadClass(String, boolean resolve)来强制ClassLoader解析它加载的所有类。无论哪种方式,我都得到了同样的错误:

I couldn't find the class in the JarEntry!
entry name="org/apache/logging/log4j/core/appender/db/jpa/converter/ContextMapAttributeConverter.class"
class name="org.apache.logging.log4j.core.appender.db.jpa.converter.ContextMapAttributeConverter"
java.lang.NoClassDefFoundError: javax/persistence/AttributeConverter
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at Corundum.launcher.CorundumClassLoader.load(CorundumClassLoader.java:52)
    at Corundum.launcher.CorundumLauncher.main(CorundumLauncher.java:47)
Caused by: java.lang.ClassNotFoundException: javax.persistence.AttributeConverter
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 12 more

//结束编辑

//编辑2:

这是我用来在尝试解析类时加载类的代码示例。 这是我创建的一个扩展URLClassLoader的类中的代码。 在以Class<?> clazz = loadClass( 开始的行上,我尝试使用true和false作为布尔参数; 两种尝试都导致了上述相同的错误。

public boolean load(ClassLoadAction class_action, FinishLoadAction end_action) {
    // establish the jar associated with this ClassLoader as a JarFile
    JarFile jar;
    try {
        jar = new JarFile(jar_path);
    } catch (IOException exception) {
        System.out.println("There was a problem loading the " + jar_path + "!");
        exception.printStackTrace();
        return false;
    }

    // load each class in the JarFile through its JarEntries
    Enumeration<JarEntry> entries = jar.entries();

    if (entries.hasMoreElements())
        for (JarEntry entry = entries.nextElement(); entries.hasMoreElements(); entry = entries.nextElement())
            if (!entry.isDirectory() && entry.getName().endsWith(".class"))
                try {
                    /* this "true" in the line below is the whole reason this class is necessary; it makes the URLClassLoader this class extends "resolve" the class,
                     * meaning it also loads all the classes this class refers to */
                    Class<?> clazz = loadClass(entry.getName().substring(0, entry.getName().length() - 6).replaceAll("/", "."), true);
                    class_action.onClassLoad(this, jar, clazz, end_action);
                } catch (ClassNotFoundException | NoClassDefFoundError exception) {
                    try {
                        close();
                    } catch (IOException exception2) {
                        System.out.println("There was a problem closing the URLClassLoader after the following " + exception2.getClass().getSimpleName() + "!");
                        exception.printStackTrace();
                    }
                    try {
                        jar.close();
                    } catch (IOException exception2) {
                        System.out.println("There was a problem closing the JarFile after the following ClassNotFoundException!");
                        exception.printStackTrace();
                    }
                    System.out.println("I couldn't find the class in the JarEntry!\nentry name=\"" + entry.getName() + "\"\nclass name=\""
                            + entry.getName().substring(0, entry.getName().length() - 6).replaceAll("/", ".") + "\"");
                    exception.printStackTrace();
                    return false;
                }

    // once all the classes are loaded, close the ClassLoader and run the plugin's main class(es) load() method(s)
    try {
        jar.close();
    } catch (IOException exception) {
        System.out.println("I couldn't close the URLClassLoader used to load this jar file!\njar file=\"" + jar.getName() + "\"");
        exception.printStackTrace();
        return false;
    }

    end_action.onFinishLoad(this, null, class_action);
    System.out.println("loaded " + jar_path);
    // TODO TEST
    try {
        close();
    } catch (IOException exception) {
        System.out.println("I couldn't close the URLClassLoader used to load this jar file!\njar file=\"" + jar_path + "\"");
        exception.printStackTrace();
        return false;
    }
    return true;
}

//结束编辑 2

我意识到这个问题肯定有一个简单的解决方案,但是我却找不到。任何帮助都将让我感激不尽。谢谢。


你确认了堆栈跟踪,确实是导入本身引起了问题,而不是(比如)从构造函数或静态变量中无意中访问其他类吗? (如果真的是导入问题,如果删除导入并完全限定对该类的所有对象引用,会发生什么?)最后,请问您是如何加载jar包的? - Scott Dudley
在这种情况下,可以使用两个不执行任何操作的简单类模拟示例,并确定它们在加载时是否存在相同的问题。如果没有,就继续增加模型的复杂性,直到它反映了应用程序中使用的模式。例如,我已经注意到(在OSGi环境中)类加载器喜欢解析直接加载的类引用的所有类,但它不会尝试解析在一步移除的类中引用的类,直到它们被调用。 - Scott Dudley
@ScottDudley 我已经从堆栈跟踪中确认错误发生在导入时。我无法尝试完全限定而不是使用导入,因为我无法修改Server,这也是我无法尝试您的“shim class”想法的原因。我尝试过使用URLClassLoaders加载jar文件,既通过单独使用它们,又通过创建一个子类来利用loadClass(String, boolean);但由于同样的原因,两者都没有起作用。我将在问题中添加有关我的加载尝试的更多信息,包括堆栈跟踪。 - Variadicism
@REALDrummer,你不能修改Server,但是你可以控制ServerAPI,对吗?你能不能先加载使用一个shimmed类与Server通信的ServerAPI,然后再加载Server呢?(你尝试过通过向URLClassLoader构造函数传递两个URL来同时访问两个JAR文件吗?) - Scott Dudley
@REALDrummer,我不太清楚您为什么要直接解析JAR文件并尝试单独加载每个类。您有这样做的某些功能需求吗?您觉得这个链接怎么样:https://bitbucket.org/sdudley/jartest/src - Scott Dudley
显示剩余12条评论
1个回答

2
令人尴尬的是,我发现错误信息所说的确实是真的。javax.persistence.AttributeConverter这个类,加载器声称它不存在,而实际上并未在jar包中。
我通过仅加载主类和ClassLoader所有引用类来解决此问题,基本上加载程序中使用的jar包中的所有类,这是我需要的所有内容。
现在,我记得以前曾检查过这个问题,并找到了该类;我想我当时一定是在检查Apache开放源代码库中的类,而不是在实际服务器上检查。我记不清了。
无论如何,缺少AttributeConverter。我不知道他们如何或为什么设法编译出带有丢失依赖项的jar包,但我猜想他们的主要进程从来没有使用过该部分代码,因此从未抛出错误。
很抱歉浪费了大家的时间...包括我的时间。我一直被这个问题困扰了一段时间。
这个故事的寓意是:
如果你正在尝试加载一个可执行的jar包,除非你必须这样做,否则不要费力地加载jar包中的所有类。只需加载主类即可,这将会加载程序运行所需的所有内容。
//编辑:
现在我遇到了同样的错误,但这种错误直到我尝试从已加载的类中调用方法时才出现。问题显然仍然存在。请给这个答案投反对票并忽略它。

你能修好这个问题吗? - Maria Ines Parnisari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接