创建一个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 jar文件,包含近2000类,并且需要大约160 ms来迭代所有条目,更不用说加载它找到的类了。

如果有人知道的解决方案比每次加载类时迭代JarInputStream的条目更快,请分享!

我将遍历该类一次并缓存条目。 我还会看一下URLClassLoader的源代码,看看它是如何做到的。 如果失败,请将数据写入临时文件并通过普通的类加载器加载。

你能做的最好

首先,您不需要使用JarInputStream ,因为它只将清单的支持添加到类ZipInputStream ,我们在这里并不关心。 您不能将您的条目放入缓存中(除非您直接存储每个条目的内容,这在内存消耗方面会很糟糕),因为ZipInputStream不是要共享的,因此无法同时读取它。 您可以做的最好的事情是将条目的名称存储到缓存中,以便在我们知道条目存在时仅迭代条目。

代码可能是这样的:

 public class RemoteClassLoader extends ClassLoader { private final byte[] jarBytes; private final Set 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 loadNames(byte[] jarBytes) throws IOException { Set 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; } } 

理想的解决方案

使用JarInputStream访问zip条目显然不是这样做的方法,因为您需要迭代条目才能找到它,这不是一种可扩展的方法,因为性能将取决于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; } }