Creating a ClassLoader to load a JAR file from an array of bytes

I am looking to write a custom class loader that downloads a JAR from a user network. In the end, all I have to work with is a byte array of the JAR .

I cannot dump an array of bytes into the file system and use URLClassLoader .
My first plan was to create a JarFile object from a stream or a byte array, but it only supports a File object.

I already wrote something that uses 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; } } 

This might work fine for small JAR files, but I tried loading the 2.7MB jar file with nearly 2000 classes, and it went around 160 ms just to iterate over all the records, not to mention loading the class that it found.

If someone knows a solution that is faster than iterating through JarInputStream entries every time a class is loaded, please share!

+6
source share
2 answers

I would iterate over the class once and cache the records. I would also look at the source code for the URLClassLoader to find out how it does it. If this fails, write the data to a temporary file and load it through a regular classloader.

+2
source

The best you can do

You don’t need to use JarInputStream , as it adds manifest support to the ZipInputStream class, which we are not interested in here. You cannot cache your records (unless you directly store the contents of each record, which would be horrible in terms of memory consumption), because ZipInputStream not intended to be shared, therefore it cannot be read at the same time. The best you can do is store the name of the records in the cache, so that iterate over only the records when we know that the record exists.

The code could be something like this:

 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; } } 

The perfect solution

Accessing a zip record using a JarInputStream clearly not the way to do this, since you will need to iterate over the records to find which one is not a scalable approach , because performance will depend on the total number of records in your jar file.

To get the best results, you need to use ZipFile to access the record directly thanks to the getEntry(name) method, regardless of the size of your archive. Unfortunately, the ZipFile class ZipFile not provide any constructors that accept the contents of your archive as a byte array (this is not good practice, as you may encounter OOME if the file is too large), but only as a File , so you will need to change the logic of your class to save the contents of the zip to a temporary file, and then provide this temporary file to your ZipFile so that you can directly access the recording.

The code could be something like this:

 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; } } 
+4
source

Source: https://habr.com/ru/post/945224/


All Articles