ClassNotFoundException при попытке загрузить класс из внешнего JAR во время выполнения

Я пытаюсь загрузить класс из JAR, представленный как массив byte[] во время выполнения.
Я знаю две вещи о классе для загрузки:

1. он реализует "RequiredInterface"
2. Я знаю, что это квалифицированное имя: "sample.TestJarLoadingClass"

Я нашел решение, в котором я должен расширить 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;
        }
}

RequiredInterface:

public interface RequiredInterface {
    String method();
}

Класс в файле JAR:

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

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());

Обратите внимание, что моя реализация предполагает, что имя файла = имя класса, поэтому, например, классы, которые не являются верхним уровнем, не будут найдены. И, конечно, некоторые детали могут быть более отточенными (например, обработка исключений).

Другие вопросы по тегам