创建一个ClassLoader以从字节数组加载JAR文件

Bra*_*ell 10 java arrays jar classloader

我正在寻找一个自定义类加载器,它将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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这可能适用于小JAR文件,但我尝试加载一个2.7MB几乎2000类的jar文件,它只160 ms需要遍历所有条目,更不用说加载它找到的类了.

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

Nic*_*tto 7

你能做的最好

首先你没有必要使用,JarInputStream因为它只是将清单的支持添加到ZipInputStream我们并不真正关心的类中.您不能将您的条目放入缓存中(除非您直接存储每个条目的内容,这些内容在内存消耗方面会很糟糕),因为a 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

理想的解决方案

使用zip访问权限JarInputStream显然不是这样做的方法,因为您需要迭代条目才能找到它,这不是一种可扩展的方法,因为性能将取决于jar文件中的条目总数.

为了获得最佳性能,您需要使用a 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;
    }
}
Run Code Online (Sandbox Code Playgroud)


Pet*_*rey 3

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