Создание ClassLoader для загрузки файла JAR из байтового массива

Я ищу написать собственный загрузчик классов, который будет загружать JAR файл из другой сети. В конце концов, все, с чем мне нужно работать, это байтовый массив JAR файл.

Я не могу записать массив байтов в файловую систему и использовать URLClassLoader,
Моим первым планом было создать JarFile объект из потока или байтового массива, но он поддерживает только File объект.

Я уже написал что-то, что использует JarInputStream:

public class RemoteClassLoader extends ClassLoader {

    private final byte[] jarBytes;

    public RemoteClassLoader(byte[] jarBytes) {
        this.jarBytes = jarBytes;
    }

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(name);
        if (clazz == null) {
            try {
                InputStream in = getResourceAsStream(name.replace('.', '/') + ".class");
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                StreamUtils.writeTo(in, out);
                byte[] bytes = out.toByteArray();
                clazz = defineClass(name, bytes, 0, bytes.length);
                if (resolve) {
                    resolveClass(clazz);
                }
            } catch (Exception e) {
                clazz = super.loadClass(name, resolve);
            }
        }
        return clazz;
    }

    @Override
    public URL getResource(String name) {
        return null;
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
            JarEntry entry;
            while ((entry = jis.getNextJarEntry()) != null) {
                if (entry.getName().equals(name)) {
                    return jis;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Это может хорошо работать для маленьких JAR файлы, но я попытался загрузить 2.7MB фляга с почти 2000 классы и это занимало 160 ms просто для перебора всех записей, не говоря уже о загрузке найденного им класса.

Если кто-нибудь знает решение, которое быстрее, чем итерация JarInputStreamзаписи каждый раз, когда класс загружается, пожалуйста, поделитесь!

2 ответа

Решение

Я бы перебрал класс один раз и кэшировал записи. Я также посмотрел бы на исходный код URLClassLoader, чтобы увидеть, как он это делает. Если это не удается, запишите данные во временный файл и загрузите его через обычный загрузчик классов.

Лучшее, что вы можете сделать

Во-первых, вам не нужно использовать JarInputStream поскольку это только добавляет поддержку манифеста классу ZipInputStream что нам здесь все равно. Вы не можете поместить свои записи в кеш (если вы не храните непосредственно содержимое каждой записи, что было бы ужасно с точки зрения потребления памяти), потому что ZipInputStream не предназначен для совместного использования, поэтому он не может быть прочитан одновременно. Лучшее, что вы можете сделать, это сохранить имена записей в кеше, чтобы перебирать записи только тогда, когда мы знаем, что запись существует.

Код может быть примерно таким:

public class RemoteClassLoader extends ClassLoader {

    private final byte[] jarBytes;
    private final Set<String> names;

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

    /**
     * This will put all the entries into a thread-safe Set
     */
    private static 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 name) {
        // Check first if the entry name is known
        if (!names.contains(name)) {
            return null;
        }
        // I moved the JarInputStream declaration outside the
        // try-with-resources statement as it must not be closed otherwise
        // the returned InputStream won't be readable as already closed
        boolean found = false;
        ZipInputStream jis = null;
        try {
            jis = new ZipInputStream(new ByteArrayInputStream(jarBytes));
            ZipEntry entry;
            while ((entry = jis.getNextEntry()) != null) {
                if (entry.getName().equals(name)) {
                    found = true;
                    return jis;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // Only close the stream if the entry could not be found
            if (jis != null && !found) {
                try {
                    jis.close();
                } catch (IOException e) {
                    // ignore me
                }
            }
        }
        return null;
    }
}

Идеальное решение

Доступ к записи zip с помощью JarInputStream это явно не способ сделать это, так как вам нужно будет перебирать записи, чтобы найти его, что не является масштабируемым подходом, потому что производительность будет зависеть от общего количества записей в вашем jar-файле.

Чтобы получить наилучшие результаты, вам нужно использовать ZipFileчтобы получить прямой доступ к записи благодаря методу getEntry(name) независимо от размера вашего архива. К сожалению класс ZipFile не предоставляет конструкторов, которые принимают содержимое вашего архива как byte массив (это не очень хорошая практика, так как вы можете столкнуться с OOME, если файл слишком большой), но только как File, поэтому вам нужно будет изменить логику вашего класса, чтобы сохранить содержимое вашего zip-файла во временный файл, а затем предоставить этот временный файл вашему ZipFile чтобы иметь доступ к записи напрямую.

Код может быть примерно таким:

public class RemoteClassLoader extends ClassLoader {

    private final ZipFile zipFile;

    public RemoteClassLoader(byte[] jarBytes) throws IOException {
        this.zipFile = RemoteClassLoader.load(jarBytes);
    }

    private static ZipFile load(byte[] jarBytes) throws IOException {
        // Create my temporary file
        Path path = Files.createTempFile("RemoteClassLoader", "jar");
        // Delete the file on exit
        path.toFile().deleteOnExit();
        // Copy the content of my jar into the temporary file
        try (InputStream is = new ByteArrayInputStream(jarBytes)) {
            Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
        }
        return new ZipFile(path.toFile());
    }

    ...

    @Override
    public InputStream getResourceAsStream(String name) {
        // Get the entry by its name
        ZipEntry entry = zipFile.getEntry(name);
        if (entry != null) {
            // The entry could be found
            try {
                // Gives the content of the entry as InputStream
                return zipFile.getInputStream(entry);
            } catch (IOException e) {
                // Could not get the content of the entry
                // you could log the error if needed
                return null;
            }
        }
        // The entry could not be found
        return null;
    }
}
Другие вопросы по тегам