讀取 jar 包中嵌套的 jar 包內(nèi)容

背景

最近在做 javaagent 的時(shí)候,我們需要將很多依賴的包打成一個(gè)大大的 jar 包,這時(shí)候可以用maven-shade-plugin 進(jìn)行操作,但是如果我們的代碼不想默認(rèn)被 AppClassloader 來加載(javaagent 的代碼默認(rèn)是由 AppClassloader 來進(jìn)行加載的),又不想將這些包放在這個(gè)大 jar 包的外面,這個(gè)時(shí)候我們就需要吧這些代碼以 jar 包的形式放在 resources 里面,最終 jar 包圖可能是這樣.

── org
    ├── Hello.class
── plugins
    ├── a.jar
    ├── b.jar
    ├── c.jar
    ...

org 文件夾里面存放的是我們編譯后的.class 文件, plugins 存放的是一些需要額外加載的 jar 包,默認(rèn)情況下,里面的代碼是當(dāng)前 classloader 加載不到的,需要自定義 classloader 來加載.

如何加載

但是如何來加載jar 包里面的文件呢?假如外面這層 jar 包的名字為demo.jar.
你可能會(huì)自定義一個(gè) classloader, 加入有個(gè) World.class位于 a.jar 中,自定義 classloader 當(dāng)然需要覆寫 findClass 方法,如何把這個(gè)文件加載到內(nèi)存呢?

思路1

我們都知道對(duì)于讀取 jar 包里面的路徑都有特定的格式,比如讀取 a.jar 的 jarEntry可以這樣讀取

         JarFile jarFile = new JarFile(new File(""));
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String name = jarEntry.getName();
               if("plugins/a.jar".equals(name)){
                .....
                }
          }

這樣我們可以拿到這個(gè) jar 包對(duì)應(yīng)的 jarEntry, 但是拿到之后好像并不能干啥,也沒有方法把他當(dāng)成一個(gè) jar file 繼續(xù)獲取里面的類.所以這種方式暫時(shí)不可行

思路2

直接用 URL 獲取路徑,比如獲取 a.jar

  URL url = new URL("jar:file:","",-1,"demo.jar!/plugins/a.jar");
  url.getInputstream();
....

這樣貌似可以將一個(gè) jar 獲取為一個(gè) inputstream, 但是 a.jar 里面的類怎么獲取呢?獲取你會(huì)想這樣

 URL url = new URL("jar:file:","",-1,"demo.jar!/plugins/a.jar!/World.class");
url.openConnection();

但是好像是不行的.

思路3

既然讀取 jar 包里面的內(nèi)可以用!/這樣的格式,那么讀取一層 jar 包應(yīng)該是沒有問題的,如果我們可以在運(yùn)行前將 jar 包中的 jar 文件解壓出來,放在一個(gè)目錄,那么就有辦法讀取其中的內(nèi)容了,所以我們的思路是:

  • 解壓需要讀取的嵌套 jar 包文件到一個(gè)一個(gè)臨時(shí)的文件夾,并且每次解壓要唯一
  • 通過臨時(shí)文件夾讀取其中的類,加載到類加載器.
  • JVM 退出的時(shí)候刪除這個(gè)臨時(shí)文件夾,避免無謂的存儲(chǔ)消耗.

按照這樣的思路,于是有了下面的方法:

獲取臨時(shí)目錄

      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 File(System.getProperty("java.io.tmpdir"));
            File folder = new File(tempFolder, "test-loader-" + UUID.randomUUID());
            File pluginsFolder = new File(folder, "plugins");
            if (!pluginsFolder.mkdirs() || !activationsFolder.mkdirs()) {
                logger.error("cannot makedir temp dir");
                throw new RuntimeException("can not mkdir temp dir");
            }
            folder.deleteOnExit();
            pluginsFolder.deleteOnExit();
            logger.info(" temp folder is {}",folder.getCanonicalPath());
            JarFile jarFile = new JarFile(jarPath);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String name = jarEntry.getName();
                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) {
            logger.error(" 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 = jarFile.getInputStream(entry)) {
            try (OutputStream outputStream = new FileOutputStream(file)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
                outputStream.flush();
            }
        }
    }

其實(shí)我們最終獲取到TEMP_FOLDER其他操作都像讀文件一樣了,關(guān)鍵在于如何解壓,這里面的幾個(gè)小細(xì)節(jié):

  • file.deleteOnExit(); 的使用,相當(dāng)于給文件刪除注冊(cè)了一個(gè)鉤子,當(dāng) JVM 退出的時(shí)候,自動(dòng)回刪除這個(gè)文件,最終被刪除的文件是保存在一個(gè)隊(duì)列里面的,所以這里的刪除代碼順序注冊(cè)也是有講究的.
  • 每次創(chuàng)建的文件夾都不一樣,避免污染環(huán)境,讀取的文件過多或者過少.

直接讀取

雖然不能隨意讀取嵌套jar 包中的內(nèi)容,但是JarFileEntry 中可以讀取manifest 文件,我們可以一些需要讀取的放在這個(gè)文件里面,然后在外面直接讀取.

public synchronized Manifest getManifest() throws IOException {}

引用

ps: 除了臨時(shí)目錄的方法,可能還有更好的方法.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容