如果您曾經試圖把 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 的過程
然後告訴您如何利用它在一個自包含的文件中交付您自己的可以運行的應用程序
OneJAR 概述 在介紹 One
JAR 的細節之前
請讓我首先討論一下我構建它的目的
我確定一個 One
JAR 檔案文件應該是
可以用 java
jar 機制執行
能夠包含應用程序需要的 所有 文件 —— 也就是說
包括原始形式(未展開)的類和資源
擁有簡單的內部結構
僅僅用 jar 工具就可以被裝配起來
對原來的應用程序不可見 —— 也就是說
無需修改原來的應用程序
就可以把它打包在 One
JAR 檔案文件內部
問題和解決方案 在開發 One
JAR 的過程中
我解決的最大問題
就是如何裝入包含在另外一個 JAR 文件中的 JAR 文件
Java 類裝入器 sun
misc
Launcher$AppClassLoader(在 java
jar 開始的時候出現)只知道如何做兩件事
裝入在 JAR 文件的根出現的類和資源
裝入 META
INF/MANIFEST
MF 中的 Class
Path 屬性指向的代碼基中的類和資源
而且
它還故意忽略針對 CLASSPATH 的全部環境變量設置
還忽略您提供的命令行參數
cp
所以它不知道如何從一個包含在其他 JAR 文件中的 JAR 文件裝入類或資源
顯然
我需要克服這個問題
才能實現 One
JAR 的目標
解決方案
展開支持 JAR 文件
我為了創建單一可執行 JAR 文件所做的第一個嘗試
顯然就是在可交付的 JAR 文件內展開支持 JAR 文件
我們把可交付的文件稱為 main
jar
假設有一個應用程序的類叫做 com
main
Main
而且它依賴兩個類 —— com
a
A (在 a
jar 中) 和 com
b
B(在 b
jar 中)
那麼 One
JAR 文件看起來應該像這樣
main
jar
| com/main/Main
class
| com/a/A
class
| com/b/B
class
這樣
最初來源於 a
jar 文件的 A
class 丟失了
B
class 也是如此
雖然這看起來只是個小問題
但卻會真正帶來問題
我很快就會解釋為什麼
OneJAR 和 FJEP 最近發布的一個叫做 FJEP (FatJar Eclipse Plugin) 的工具支持在 Eclipse 內部直接構建扁平 JAR 文件
One
JAR 已經與 FatJar 集成在一起
以支持在不展開 JAR 文件的情況下嵌入 JAR 文件
請參閱 參考資料 了解有關詳細內容
把 JAR 文件展開到文件系統以創建一個扁平結構
這可能非常耗時
還需要使用 Ant 這樣的構建工具來展開和重新歸檔支持類
除了這個小麻煩之外
我很快又遇到了兩個與展開支持 JAR 文件有關的嚴重問題
如果 a
jar 和 b
jar 包含的資源的路徑名相同 (比如說
都是 log
j
properties )
那麼您該選哪個?
如果 b
jar 的許可明確要求您在重新發布它的時候不能修改它
那您怎麼辦?您無法在不破壞許可條款的前提下像這樣展開它
我覺得這些限制為另外一種方法提供了線索
解決方案 : MANIFEST ClassPath 我決定研究 java
jar 裝入器中的另外一種機制
裝入的類是在檔案文件中一個叫做 META
INF/MANIFEST
MF 的特殊文件中指定的
通過指定稱為 Class
Path 的屬性
我希望能夠向啟動時的類裝入器添加其他檔案文件
下面就是這樣的一個 One
JAR 文件看起來的樣子
main
jar
| META
INF/MANIFEST
MF
| + Class
Path: lib/a
jar lib/b
jar
| com/main/Main
class
| lib/a
jar
| lib/b
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:!/ 語法的幾種變體來使用 Class
Path(請參閱
說明和線索
)
但是沒有一次成功
我能 做的
就只有分別交付 a
jar 和 b
jar
並把它們與 main
jar 一起放在文件系統中了
但是這正是我想避免的那類事情
進入 JarClassLoader 此時
我感到備受挫折
我如何才能讓應用程序從它自己的 JAR 文件中的 lib 目錄裝入它自己的類呢?我決定應當創建定制類裝入器來承擔這個重任
編寫定制類裝入器不是一件容易的事情
但是實際上這個工作並沒有那麼復雜
類裝入器對它所控制的應用程序有非常深刻的影響
所以在發生故障的時候
很難診斷和解釋故障
雖然對於類裝入的完整處理超出了本文的范圍(請參閱 參考資料)
我還是要介紹一些基本概念
好保證您能從後面的討論中得到最大收獲
裝入類 當 JVM 遇到一個對象的類未知的時候
就會調用類裝入器
類裝入器的工作是找到類的字節碼(基於類的名稱)
然後把這些字節傳遞給 JVM
JVM 再把這些字節碼鏈接到系統的其余部分
使得正在運行的代碼可以使用新裝入的類
JDK 中關鍵的類是 java
lang
Classloader 以及 loadClass 方法
摘要如下
public abstract class ClassLoader {
protected synchronized Class loadClass(String name
boolean resolve)
throws ClassNotFoundException {
}
}
ClassLoader 類的主要入口點是 loadClass() 方法
您會注意到
ClassLoader 是一個抽象類
但是它沒有聲明任何抽象方法
這樣
關於 loadClass() 方法是不是要關注的方法
一點線索也沒留下
實際上
它不是 要關注的主方法
回到過去的好時光
看看 JDK
的類裝入器
可以看到 loadClass() 是您可以有效擴展類裝入器的惟一地方
但是從 JDK
起
最好讓類裝入器單獨做它所做的工作
即以下工作
檢查類是否已經裝入
檢查上級類裝入器能否裝入類
調用 findClass(String name) 方法
讓派生的類裝入器裝入類
ClassLoader
findClass() 的實現是拋出一個新的 ClassNotFoundException 異常
並且是我們實現定制類裝入器時要考慮的第一個方法
JAR 文件何時不是 JAR 文件?
為了能夠裝入在 JAR 文件內部 的 JAR 文件中的類(這是關鍵問題
您可以回想起來)
我首先必須能夠打開並讀取頂層的 JAR 文件(上面的 main
jar 文件)
現在
因為我使用的是 java
jar 機制
所以
java
class
path 系統屬性中的第一個(也是惟一一個)元素是 One
JAR 文件的完整路徑名!用下面的代碼您可以得到它
jarName = System
getProperty(
java
class
path
);
我接下來的一步是遍歷應用程序的所有 JAR 文件項
並把它們裝入內存
如清單
所示
清單
遍歷查找嵌入的 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
From:http://tw.wingwit.com/Article/program/Java/JSP/201311/19228.html