Java 运行时尝试从外部JAR加载类时发生ClassNotFoundException

Java 运行时尝试从外部JAR加载类时发生ClassNotFoundException,java,class,classloader,dynamic-class-loaders,Java,Class,Classloader,Dynamic Class Loaders,我试图在运行时从表示为byte[]数组的JAR加载一个类。 关于要加载的类,我知道两件事: 1.它实现了“RequiredInterface” 2.我知道它是限定名:“sample.TestJarLoadingClass” 我找到了我必须扩展类加载器的,但它抛出: Exception in thread "main" java.lang.ClassNotFoundException: sample.TestJarLoadingClass at java.lang.ClassLoader.

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

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

我找到了我必须扩展类加载器的,但它抛出:

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;
        }
}
JAR文件中的类:

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

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

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

但这可能还不够,因为您的
字节类加载器不知道您的
requireInterface
,所以您可以读取类本身,但类的定义包含它实现的
requireInterface
信息,这对于类加载器来说是个问题。这个问题很容易解决,您只需将常规类加载器作为构造函数参数传递给您的类加载器,然后使用
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);
    }
}

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

我使用了您在我的机器上发布的代码,所有这些代码都工作正常。你能把你得到的确切的例外情况公布出来吗?另外,请检查您使用的路径是否真的指向包含要加载的类的jar?好的,我发布了整个异常消息。我检查了所有可能的“愚蠢”错误,一切都应该正常。请检查您的类加载器范围内是否没有需要加载的类。是的,您是对的,我错误地将依赖项添加到带有类的jar,而不是带有接口的jar。对不起,为什么不直接使用URLClassLoader,而不是从头开始重新实现一个呢?为什么不首先把这个jar放在类路径中呢?因为它需要我将jar存储在本地。在实际情况中,我的输入是来自应用程序前端的一个byte[]数组,我不希望在本地构建JAR文件,而只基于byte[]生成解决方案。我正在尝试运行您的解决方案,但每次我收到名为“TestJarLoadingClass”的异常:线程“main”中的异常java.lang.NoClassDefFoundError:TestJarLoadingClass(错误名称:sample/TestJarLoadingClass)@adsqew可能是因为我在
TestJarLoadingClass
上测试了它,所以我的示例没有考虑它。我已经更新了这个示例,现在它考虑了不同的包(看看new
EntryNameequalClassName
)。还要尝试将断点放在
findClass
方法的开头,并查看JarEntry实例在每次迭代中的外观。它可能会让您更多地了解类是如何在Jar文件中内部存储的。很好,现在它可以按预期工作了。最后我想问一下,您是否知道为什么nextJarEntry.getSize()每次都给出结果-1(它是默认值)?我必须手动传递.class size。这很难说,因为在我的例子中,非类的大小为0,类的大小为正(在我的例子中TestJarLoadingClass的大小为445)。显然,您的环境与我的环境略有不同-我现在唯一能做的就是引用javadoc():
返回输入数据的未压缩大小,或者-1(如果未知)。
但我认为这不会很有帮助:)我所要做的就是捕获变量的读取字节数:int read=jis.read(nextClass,0,maxFileSize);然后按如下方式使用它:defineClass(className,nextClass,0,read,null)
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());