【问题标题】:ClassNotFoundException while trying to load class from external JAR at runtime尝试在运行时从外部 JAR 加载类时出现 ClassNotFoundException
【发布时间】:2017-07-08 14:51:13
【问题描述】:

我正在尝试从 JAR 加载一个在运行时表示为 byte[] 数组的类。
我知道关于要加载的类的两件事:

1.它实现了“RequiredInterface”
2. 我知道它的限定名称:“sample.TestJarLoadingClass”

我找到了solution,我必须在其中扩展 ClassLoader 但它会抛出:

Exception in thread "main" java.lang.ClassNotFoundException: sample.TestJarLoadingClass
    at java.lang.ClassLoader.findClass(ClassLoader.java:530)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at tasks.Main.main(Main.java:12)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

每当我想加载课程时。

这种情况可能是什么原因,我该如何摆脱这种情况?
任何帮助都非常感谢

主要方法:

public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
    Path path = Paths.get("src/main/java/tasks/sample.jar");
    RequiredInterface requiredInterface = (RequiredInterface) Class.forName("sample.TestJarLoadingClass", true, new ByteClassLoader(Files.readAllBytes(path))).newInstance();
}

自定义类加载器:

    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipInputStream;

    public class ByteClassLoader extends ClassLoader {
        private final byte[] jarBytes;
        private final Set<String> names;

        public ByteClassLoader(byte[] jarBytes) throws IOException {
            this.jarBytes = jarBytes;
            this.names = loadNames(jarBytes);
        }

        private Set<String> loadNames(byte[] jarBytes) throws IOException {
            Set<String> set = new HashSet<>();
            try (ZipInputStream jis = new ZipInputStream(new ByteArrayInputStream(jarBytes))) {
                ZipEntry entry;
                while ((entry = jis.getNextEntry()) != null) {
                    set.add(entry.getName());
                }
            }
            return Collections.unmodifiableSet(set);
        }

        @Override
        public InputStream getResourceAsStream(String resourceName) {
            if (!names.contains(resourceName)) {
                return null;
            }
            boolean found = false;
            ZipInputStream zipInputStream = null;
            try {
                zipInputStream = new ZipInputStream(new ByteArrayInputStream(jarBytes));
                ZipEntry entry;
                while ((entry = zipInputStream.getNextEntry()) != null) {
                    if (entry.getName().equals(resourceName)) {
                        found = true;
                        return zipInputStream;
                    }
                }
            } catch (IOException e) {;
                e.printStackTrace();
            } finally {
                if (zipInputStream != null && !found) {
                    try {
                        zipInputStream.close();
                    } catch (IOException e) {;
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }
}

必需接口:

public interface RequiredInterface {
    String method();
}

JAR 文件中的类:

package sample;
public class TestJarLoadingClass implements RequiredInterface {
    @Override
    public String method() {
        return "works!";
    }
}

【问题讨论】:

  • 我使用了您在我的机器上发布的代码并且一切正常。您能否发布确切的例外情况?另外请检查您使用的路径是否真的指向包含您要加载的类的jar?
  • 好的,我发布了整个异常消息。我检查了所有可能的“愚蠢”错误,一切都应该正常。请检查您的类加载器范围内是否没有需要加载的类。
  • 是的,你是对的,我错误地添加了对 jar 的依赖项,而不是使用接口的 jar。对不起
  • 为什么不直接使用 URLClassLoader,而不是从头开始重新实现呢?为什么不一开始就把那个 jar 放在类路径中呢?
  • 因为它需要我将 JAR 存储在本地。在实际情况下,我的输入是来自应用程序前端的 byte[] 数组,我不希望在本地构建 JAR 文件并仅基于 byte[] 制作解决方案。

标签: java class classloader dynamic-class-loaders


【解决方案1】:

在我看来,我们这里有两个问题:

首先你应该重写包含加载类的实际逻辑的findClass 方法。这里的主要挑战是找到包含您的类的字节数组的一部分 - 因为您将整个 jar 作为字节数组,您需要使用 JarInputStream 来扫描您的类的字节数组。

但这可能还不够,因为您的 RequiredInterface 对于您的 ByteClassLoader 是未知的 - 所以您将能够读取类本身,但类的定义包含它实现 RequiredInterface 的信息,这是一个问题你的类加载器。这个很容易修复,您只需将常规类加载器作为构造函数参数传递给您的类并使用super(parentClassLoader)

这是我的版本:

public class ByteClassLoader extends ClassLoader {
    private final byte[] jarBytes;

    public ByteClassLoader(ClassLoader parent, byte[] jarBytes) throws IOException {
        super(parent);
        this.jarBytes = jarBytes;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        // read byte array with JarInputStream
        try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
            JarEntry nextJarEntry;

            // finding JarEntry will "move" JarInputStream at the begining of entry, so no need to create new input stream
            while ((nextJarEntry = jis.getNextJarEntry()) != null) {

                if (entryNameEqualsClassName(name, nextJarEntry)) {

                    // we need to know length of class to know how many bytes we should read
                    int classSize = (int) nextJarEntry.getSize();

                    // our buffer for class bytes
                    byte[] nextClass = new byte[classSize];

                    // actual reading
                    jis.read(nextClass, 0, classSize);

                    // create class from bytes
                    return defineClass(name, nextClass, 0, classSize, null);
                }
            }
            throw new ClassNotFoundException(String.format("Cannot find %s class", name));
        } catch (IOException e) {
            throw new ClassNotFoundException("Cannot read from jar input stream", e);
        }
    }


    private boolean entryNameEqualsClassName(String name, JarEntry nextJarEntry) {

        // removing .class suffix
        String entryName = nextJarEntry.getName().split("\\.")[0];

        // "convert" fully qualified name into path
        String className = name.replace(".", "/");

        return entryName.equals(className);
    }
}

及用法

    RequiredInterface requiredInterface = (RequiredInterface)Class.forName("com.sample.TestJarLoadingClass", true, new ByteClassLoader(ByteClassLoader.class.getClassLoader(), Files.readAllBytes(path))).newInstance();
    System.out.println(requiredInterface.method());

请注意,我的实现假定文件名 = 类名,因此例如不会找到非顶级类。当然,有些细节可能会更精致(比如异常处理)。

【讨论】:

  • 我正在尝试运行您的解决方案,但每次我收到名称“TestJarLoadingClass”异常:线程“main”中的异常 java.lang.NoClassDefFoundError:TestJarLoadingClass(错误名称:sample/TestJarLoadingClass )
  • @adsqew 可能是因为我已经在TestJarLoadingClass 上测试了它,所以我把它放在了默认包中,所以我的示例没有考虑到它。我已经更新了示例,现在它考虑了不同的包(看看新的entryNameEqualsClassName)。还可以尝试在findClass 方法的开头放置断点,并查看 JarEntry 实例在每次迭代中的样子。它可能会让你更多地了解类是如何在 Jar 文件内部存储的
  • 太好了,现在它按预期工作了。最后我想问你是否知道为什么 nextJarEntry.getSize() 每次都给出结果-1(它是默认值)?我必须手动传递 .class 大小。
  • 很难说,因为在我的情况下,如果不是类,我的 size = 0,如果是类,我的 size = 0(在我的情况下,TestJarLoadingClass 的 size = 445)。显然,您的环境与我的环境将略有不同 - 我现在唯一能做的就是引用 javadoc (docs.oracle.com/javase/7/docs/api/java/util/zip/…) :Returns the uncompressed size of the entry data, or -1 if not known. 但我认为它不会很有帮助:)
  • 我所要做的就是将读取的字节数捕获到变量中: int read = jis.read(nextClass, 0, maxFileSize);然后按如下方式使用:defineClass(className, nextClass, 0, read, null)
猜你喜欢
  • 2019-03-18
  • 1970-01-01
  • 2019-09-08
  • 1970-01-01
  • 2017-05-24
  • 2014-07-21
  • 1970-01-01
  • 2013-04-26
  • 1970-01-01
相关资源
最近更新 更多