Создание 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;
}
}