读取jar包中嵌套的jar包内容的⽅法
背景
最近在做 javaagent 的时候,我们需要将很多依赖的包打成⼀个⼤⼤的 jar 包,这时候可以⽤maven-shade-plugin 进⾏操作,但是如果我们的代码不想默认被 AppClassloader 来加载(javaagent 的代码默认是由 AppClassloader 来进⾏加载的),⼜不想将这些包放在这个⼤ jar 包的外⾯,这个时候我们就需要吧这些代码以 jar 包的形式放在 resources ⾥⾯,最终 jar 包图可能是这样.
── org
├── Hello.class
── plugins
├── a.jar
├── b.jar
├── c.jar
.
..
复制代码
org ⽂件夹⾥⾯存放的是我们编译后的.class ⽂件, plugins 存放的是⼀些需要额外加载的 jar 包,默认情况下,⾥⾯的代码是当前classloader 加载不到的,需要⾃定义 classloader 来加载.
如何加载
但是如何来加载jar 包⾥⾯的⽂件呢?假如外⾯这层 jar 包的名字为demo.jar. 你可能会⾃定义⼀个 classloader, 加⼊有个 World.class位于a.jar 中,⾃定义 classloader 当然需要覆写 findClass ⽅法,如何把这个⽂件加载到内存呢?
思路1
我们都知道对于读取 jar 包⾥⾯的路径都有特定的格式,⽐如读取 a.jar 的 jarEntry可以这样读取
JarFile jarFile = new JarFile(new File(""));
Enumeration<JarEntry> entries = ies();
while (entries.hasMoreElements()) {
JarEntry jarEntry = Element();
String name = Name();
if("plugins/a.jar".equals(name)){
.....
}
}
复制代码
这样我们可以拿到这个 jar 包对应的 jarEntry, 但是拿到之后好像并不能⼲啥,也没有⽅法把他当成⼀个 jar file 继续获取⾥⾯的类.所以这种⽅式暂时不可⾏
思路2
直接⽤ URL 获取路径,⽐如获取 a.jar
URL url = new URL("jar:file:","",-1,"demo.jar!/plugins/a.jar");
mkdirs方法
....
复制代码
这样貌似可以将⼀个 jar 获取为⼀个 inputstream, 但是 a.jar ⾥⾯的类怎么获取呢?获取你会想这样
URL url = new URL("jar:file:","",-1,"demo.jar!/plugins/a.jar!/World.class");
url.openConnection();
复制代码
但是好像是不⾏的.
思路3
既然读取 jar 包⾥⾯的内可以⽤!/这样的格式,那么读取⼀层 jar 包应该是没有问题的,如果我们可以
在运⾏前将 jar 包中的 jar ⽂件解压出来,放在⼀个⽬录,那么就有办法读取其中的内容了,所以我们的思路是:
解压需要读取的嵌套 jar 包⽂件到⼀个⼀个临时的⽂件夹,并且每次解压要唯⼀
通过临时⽂件夹读取其中的类,加载到类加载器.
JVM 退出的时候删除这个临时⽂件夹,避免⽆谓的存储消耗.
按照这样的思路,于是有了下⾯的⽅法:
获取临时⽬录
if (TEMP_FOLDER == null) {
synchronized (AgentClassLoader.class) {
if (TEMP_FOLDER == null) {
TEMP_FOLDER = unpackToFolder(jarPath);
}
}
}
复制代码
//需要的 jar 包解压到⽂件夹
private File unpackToFolder(File jarPath) {
try {
File tempFolder = new Property("pdir"));
File folder = new File(tempFolder, "test-loader-" + UUID.randomUUID());
File pluginsFolder = new File(folder, "plugins");
if (!pluginsFolder.mkdirs() || !activationsFolder.mkdirs()) {
<("cannot makedir temp dir");
throw new RuntimeException("can not mkdir temp dir");
}
folder.deleteOnExit();
pluginsFolder.deleteOnExit();
logger.info(" temp folder is {}",CanonicalPath());
JarFile jarFile = new JarFile(jarPath);
Enumeration<JarEntry> entries = ies();
while (entries.hasMoreElements()) {
JarEntry jarEntry = Element();
String name = Name();
String[] split = name.split("/");
if (name.startsWith("plugins/") && split.length > 1) {
File file = new File(pluginsFolder, split[1]);
unpack(jarFile, jarEntry, file);
file.deleteOnExit();
}
}
return folder;
} catch (Exception e) {
<(" unpack to folder error", e);
throw new RuntimeException(e);
}
}
// 解压 jar 包;
private static void unpack(JarFile jarFile, JarEntry entry, File file) throws IOException {
try (InputStream inputStream = InputStream(entry)) {
try (OutputStream outputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = ad(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
}
}
复制代码
其实我们最终获取到TEMP_FOLDER其他操作都像读⽂件⼀样了,关键在于如何解压,这⾥⾯的⼏个⼩细节:
file.deleteOnExit(); 的使⽤,相当于给⽂件删除注册了⼀个钩⼦,当 JVM 退出的时候,⾃动回删除这个⽂件,最终被删除的⽂件是保存在⼀个队列⾥⾯的,所以这⾥的删除代码顺序注册也是有讲究的.
每次创建的⽂件夹都不⼀样,避免污染环境,读取的⽂件过多或者过少.
直接读取
虽然不能随意读取嵌套jar 包中的内容,但是JarFileEntry 中可以读取manifest ⽂件,我们可以⼀些需要读取的放在这个⽂件⾥⾯,然后在外⾯直接读取.
public synchronized Manifest getManifest() throws IOException {}
复制代码
引⽤
ps: 除了临时⽬录的⽅法,可能还有更好的⽅法.

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。