假如您曾經試圖把 java 應用程序交付為單一的 Java 檔案文件(JAR 文件),那么您很有可能碰到過這樣的需求:在構建最終檔案文件之前,要展開支持 JAR 文件(supporting JAR file)。這不但是一個開發的難點,還有可能讓您違反許可協議。在本文中,Tuffs 向您介紹了 One-JAR 這個工具,它使用定制的類裝入器,動態地從可執行 JAR 文件內部的 JAR 文件中裝入類。 有人曾經說過,歷史總是在不斷地重復自身,首先是悲劇,然后是鬧劇。 最近,我第一次對此有了親身體會。我不得不向客戶交付一個可以運行的 Java 應用程序,但是我已經交付了許多次,它總是布滿了復雜性。在搜集應用程序的所有 JAR 文件、為 DOS 和 Unix(以及 Cygwin)編寫啟動腳本、確保客戶端環境變量都指向正確位置的時候,總是有許多輕易出錯的地方。假如每件事都能做好,那么應用程序能夠按它預期的方式運行。但是在出現麻煩時(而這又是常見的情況),結果就是大量時間耗費在客戶端支持上。
最近與一個被大量 ClassNotFound 異常弄得暈頭轉向的客戶交談之后,我決定自己再也不能忍受下去了。所以,我轉而尋找一個方法,可以把我的應用程序打包到單一 JAR 文件中,給我的客戶提供一個簡單的機制(比如 java -jar)來運行程序。
努力的結果就是 One-JAR,一個非常簡單的軟件打包解決方案,它利用 Java 的定制類裝入器,動態地從單一檔案文件中裝入應用程序所有的類,同時保留支持 JAR 文件的結構。在本文中,我將介紹我開發 One-JAR 的過程,然后告訴您如何利用它在一個自包含的文件中交付您自己的可以運行的應用程序。
說明與線索 URLClassloader 是 sun.misc.Launcher$AppClassLoader 的基類,它支持一個相當神秘的 URL 語法,讓您能夠引用 JAR 文件內部的資源。這個語法用起來像這樣: jar:file:/fullpath/main.jar!/a.resource。
從理論上講,要獲得一個在 JAR 文件 內部 的 JAR 文件中的項,您必須使用像 jar:file:/fullpath/main.jar!/lib/a.jar!/a.resource 這樣的方式,但是很不幸,這么做沒有用。JAR 文件協議處理器在找 JAR 文件時,只熟悉最后一個 “!/” 分隔符。
但是,這個語法確實為我最終的 One-JAR 解決方案提供了線索……
這能工作么? 當我把 main.jar 移動到另外一個地方,并試著運行它時,似乎是可以了。為了裝配 main.jar ,我創建了一個名為 lib 的子目錄,并把 a.jar 和 b.jar 放在里面。不幸的是,應用程序的類裝入器只從文件系統提取支持 JAR 文件,而不能從嵌入的 JAR 文件中裝入類。
JAR 文件何時不是 JAR 文件? 為了能夠裝入在 JAR 文件內部 的 JAR 文件中的類(這是要害問題,您可以回想起來),我首先必須能夠打開并讀取頂層的 JAR 文件(上面的 main.jar 文件)。現在,因為我使用的是 java -jar 機制,所以, java.class.path 系統屬性中的第一個(也是惟一一個)元素是 One-JAR 文件的完整路徑名!用下面的代碼您可以得到它:
jarName = System.getProperty("java.class.path");
我接下來的一步是遍歷應用程序的所有 JAR 文件項,并把它們裝入內存,如清單 1 所示:
清單 1. 遍歷查找嵌入的 JAR 文件
JarFile jarFile = new JarFile(jarName); Enumeration enum = jarFile.entries(); while (enum.hasMoreElements()) { JarEntry entry = (JarEntry)enum.nextElement(); if (entry.isDirectory()) continue; String jar = entry.getName(); if (jar.startsWith(LIB_PREFIX) jar.startsWith(MAIN_PREFIX)) { // Load it! InputStream is = jarFile.getInputStream(entry); if (is == null) throw new IOException("Unable to load resource /" + jar + " using " + this); loadByteCode