亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

用One-JAR簡化應用程序交付

系統 1567 0
如果您曾經試圖把 Java 應用程序交付為單一的 Java 檔案文件(JAR 文件),那么您很有可能遇到過這樣的需求:在構建最終檔案文件之前,要展開支持 JAR 文件(supporting JAR file)。這不但是一個開發的難點,還有可能讓您違反許可協議。在本文中,Tuffs 向您介紹了 One-JAR 這個工具,它使用定制的類裝入器,動態地從可執行 JAR 文件內部的 JAR 文件中裝入類。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --> <!--END RESERVED FOR FUTURE USE INCLUDE FILES-->

有人曾經說過,歷 史總是在不斷地重復自身,首先是悲劇,然后是鬧劇。 最近,我第一次對此有了親身體會。我不得不向客戶交付一個可以運行的 Java 應用程序,但是我已經交付了許多次,它總是充滿了復雜性。在搜集應用程序的所有 JAR 文件、為 DOS 和 Unix(以及 Cygwin)編寫啟動腳本、確保客戶端環境變量都指向正確位置的時候,總是有許多容易出錯的地方。如果每件事都能做好,那么應用程序能夠按它預期的方式 運行。但是在出現麻煩時(而這又是常見的情況),結果就是大量時間耗費在客戶端支持上。


最近與一個被大量 ClassNotFound 異常弄得暈頭轉向的客戶交談之后,我決定自己再也不能忍受下去了。所以,我轉而尋找一個方法,可以把我的應用程序打包到單一 JAR 文件中,給我的客戶提供一個簡單的機制(比如 java -jar )來運行程序。

努力的結果就是 One-JAR,一個非常簡單的軟件打包解決方案,它利用 Java 的定制類裝入器,動態地從單一檔案文件中裝入應用程序所有的類, 同時保留支持 JAR 文件的結構。在本文中,我將介紹我開發 One-JAR 的過程,然后告訴您如何利用它在一個自包含的文件中交付您自己的可以運行的應用程序。


One-JAR 概述


在介紹 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 的目標。

解決方案 1:展開支持 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 也是如此。雖然這看起來只是個小問題,但卻會真正帶來問題,我很快就會解釋為什么。

One-JAR 和 FJEP

最近發布的一個叫做 FJEP (FatJar Eclipse Plugin) 的工具支持在 Eclipse 內部直接構建扁平 JAR 文件。 One-JAR 已經與 FatJar 集成在一起,以支持在不展開 JAR 文件的情況下嵌入 JAR 文件。請參閱 參考資料 了解有關詳細內容。

把 JAR 文件展開到文件系統以創建一個扁平結構,這可能非常耗時。還需要使用 Ant 這樣的構建工具來展開和重新歸檔支持類。

除了這個小麻煩之外,我很快又遇到了兩個與展開支持 JAR 文件有關的嚴重問題:

  • 如果 a.jar 和 b.jar 包含的資源的路徑名相同 (比如說,都是 log4j.properties ),那么您該選哪個?

  • 如果 b.jar 的許可明確要求您在重新發布它的時候不能修改它,那您怎么辦?您無法在不破壞許可條款的前提下像這樣展開它。

我覺得這些限制為另外一種方法提供了線索。

解決方案 2: MANIFEST Class-Path

我決定研究 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 1.1 的類裝入器,可以看到 loadClass() 是您可以有效擴展類裝入器的惟一地方,但是從 JDK 1.2 起,最好讓類裝入器單獨做它所做的工作,即以下工作:

  • 檢查類是否已經裝入。
  • 檢查上級類裝入器能否裝入類。
  • 調用 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 文件項,并把它們裝入內存,如清單 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(is, jar);
...


注意, LIB_PREFIX 生成字符串 lib/ MAIN_PREFIX 生成字符串 main/ 。我想把任何以 lib/ main/ 開始的東西的字節碼裝入內存,供類裝入器使用,并在循環中忽略任何其他 JAR 文件項。

main 目錄

前面我已經談到過 lib/ 子目錄的角色,那么 main/ 目錄是干什么的呢? 簡要來說,類裝入器的代理模式要求我把主要類 com.main.Main 放在它自己的 JAR 文件中, 這樣它才能找到庫類(它依賴的庫類)。新的 JAR 文件看起來像這樣:

            	one-jar.jar
            
| META-INF/MANIFEST.MF
| main/main.jar
| lib/a.jar
| lib/b.jar

在上面的清單 1 中, loadByteCode() 方法接受來自 JAR 文件項的流和一個項名稱,把項的字節裝入內存,并根據項代表的是 還是 資源 ,給它分配最多兩個名稱。演示這個技術的最好方法是通過示例。假設 a.jar 包含一個類 A.class 和一個資源 A.resource 。One-JAR 類裝入器構造以下 Map 結構,名為 JarClassLoader.byteCode ,它對于類只有一對關鍵字/值組合,而對于資源則有兩個關鍵字。


圖 1. One-JAR 在內存中的結構
圖 1.  One-JAR 在內存中的結構

如 果您多看圖 1 一會,您可以看到類項是按照類名稱設置關鍵字的,而資源關鍵字的設置則根據一對名稱:全局名稱和局部名稱。用來解析資源名稱沖突的機制是:如果兩個庫 JAR 文件都用相同的全局名稱定義一個資源,那么則根據調用程序的堆棧幀來采用局部名稱。更多細節請參閱 參考資料

找到類

回憶一下,我在概述類裝入的時候,最后介紹的是 findClass() 方法。方法 findClass() 以類的名稱作為 String 參數,而且必須找到并定義該名稱所代表的字節碼。由于 loadByteCode 很好地構建了類名和字節碼之間的 Map ,所以實現這個方法現在非常簡單:只要根據類名查找字節碼,然后調用 defineClass() ,如清單 2 所示:


清單 2. findClass() 摘要
                protected Class findClass(String name) throws ClassNotFoundException {
            
ByteCode bytecode = (ByteCode)JarClassLoader.byteCode.get(name);
if (bytecode != null) {
...
byte bytes[] = bytecode.bytes;
return defineClass(name, bytes, pd);
}
throw new ClassNotFoundException(name);
}







裝入資源

在 One-JAR 開發期間, findClass 是我把自己的想法付諸實施的第一件事。 但是,當我開始部署更復雜的應用程序時,我發現除了要裝入類之外,還必須要處理資源的裝入問題。這一次,事情有點棘手。為了查找資源,需要在 ClassLoader 中找到一個合適的方法去覆蓋,我選了我最熟悉的一個,如清單 3 所示:


清單 3. getResourceAsStream() 方法
                public InputStream getResourceAsStream(String name) {
            
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}

這個時候應當響起警鐘:我就是無法理解為什么用 URL 來定位資源。所以我不用這個實現,而是插入我自己的實現,如清單 4 所示:


清單 4. One-JAR 中的 getResourceAsStream() 實現
                public InputStream getResourceAsStream(String resource) {
            
byte bytes[] = null;
ByteCode bytecode = (ByteCode)byteCode.get(resource);
if (bytecode != null) {
bytes = bytecode.bytes;
}
...
if (bytes != null) {
return new ByteArrayInputStream(bytes);
}
...
return null;
}

最后一個障礙

我對 getResourceAsStream() 方法的新實現看起來解決了問題,但是直到我試著用 One-JAR 來處理一個用 URL url = object.getClass().getClassLoader().getResource() 模式裝入資源的應用程序時,才發現實際情況與想像的不一樣。為什么?因為 ClassLoader 的默認實現返回的 URL 是 null,這個結果破壞了調用程序的代碼。

這時,事情變得真的是說不清了。我必須弄清應當用什么 URL 來引用 lib/ 目錄中的 JAR 文件內部的資源。是不是應該像 jar:file:main.jar!lib/a.jar!com.a.A.resource 這樣才好?

我試盡所有我能想到的組合,但是沒有任何一個起作用。 jar: 語法就是不支持嵌套 JAR 文件,這使得我的整個 One-JAR 方法好像面臨著死路一條。雖然大多數應用程序好像都不使用 ClassLoader.getResource 方法,但是確實有些使用了這個方法,所以我實在不愿意有需要排除的情況,讓我說“如果您的應用程序使用 ClassLoader.getResource() ,您就不能用 One-JAR。”

最后的解決方案……

當我試圖弄清楚 jar: 語法的時候,我意外地了解到了 Java 運行時環境把 URL 前綴映射到處理器的機制。這成為我修復 findResource 問題所需要的線索:我只要發明自己的協議前綴,稱為 onejar: 。 這樣,我就能把新的前綴映射到協議處理器,處理器就會返回資源的字節流,如清單 5 所示。注意,清單 5 表示的是兩個文件中的代碼,這兩個文件是 JarClassLoader 和一個叫做 com/simontuffs/onejar/Handler.java 的新文件。


清單 5. findResource 和 onejar: 協議
            
com/simontuffs/onejar/JarClassLoader.java
protected URL findResource(String $resource) {
try {
// resolve($resource) returns the name of a resource in the
// byteCode Map if it is known to this classloader.
String resource = resolve($resource);
if (resource != null) {
// We know how to handle it.
return new URL(Handler.PROTOCOL + ":" + resource);
}
return null;
} catch (MalformedURLException mux) {
WARNING("unable to locate " + $resource + " due to " + mux);
}
return null;
}
com/simontuffs/onejar/Handler.java
package com.simontuffs.onejar;
...
public class Handler extends URLStreamHandler {
/**
* This protocol name must match the name of the package in which this class
* lives.
*/
public static String PROTOCOL = "onejar";
protected int len = PROTOCOL.length()+1;

protected URLConnection openConnection(URL u) throws IOException {
final String resource = u.toString().substring(len);
return new URLConnection(u) {
public void connect() {
}
public InputStream getInputStream() {
// Use the Boot classloader to get the resource. There
// is only one per one-jar.
JarClassLoader cl = Boot.getClassLoader();
return cl.getByteStream(resource);
}
};
}
}









啟動 JarClassLoader

到現在,您可能只剩下一個問題了:我是怎樣把 JarClassLoader 插入啟動順序,讓它首先開始從 One-JAR 文件裝入類的?具體的細節超出了本文的范圍;但是,基本上說,我沒有用主類 com.main.Main 作為 META-INF/MANIFEST.MF/Main-Class 屬性,而是創建了一個新的啟動主類 com.simontuffs.onejar.Boot ,它被指定作為 Main-Class 屬性。新類要做以下工作:

  • 創建新的 JarClassLoader

  • 用新的裝入器從 main/main.jar 裝入 com.main.Main (基于 main.jar 中的 META-INF/MANIFEST.MF Main-Class 項)。

  • 裝入類,用反射調用 main() ,從而調用 com.main.Main.main(String[]) (或者諸如 main.jar/MANIFEST.MF 文件中的 Main-Class 的名稱)。在 One-JAR 命令行上傳遞的參數,被不加修改地傳遞到應用程序的主方法。







結束語

如果前面這些讓您頭痛,不要擔心:使用 One-JAR 要比理解它的工作方式容易得多。隨著 FatJar Eclipse 插件(請參閱 參考資料 中的 FJEP)的推出, Eclipse 的用戶現在只要在向導中選中一個復選框,就可以創建 One-JAR 應用程序。依賴的庫被放進 lib/ 目錄,主程序和類被放進 main/main.jar,并自動寫好 META-INF/MANIFEST.MF 文件。如果您使用 JarPlug(還是請參閱 參考資料 ),您可以查看您構建的 JAR 文件的內部結構,并從 IDE 中啟動它。

總之,One-JAR 是一個簡單而強大的解決方案,解決了應用程序打包交付的問題。但是,它沒有解決所有的應用程序場景。例如,如果您的應用程序使用老式的 JDK 1.1 的類裝入器,不把裝入委托給上一層,那么類裝入器就無法在嵌套 JAR 文件中找到類。您可以構建和部署一個“包裝”類裝入器來修改頑固的類裝入器,從而克服這個問題,不過這可能需要與 Javassist 或者字節碼工程庫(Byte Code Engineering Library,BCEL)這樣的工具一起使用字節碼操縱技術。

對于嵌入式應用程序和 Web 服務器使用的特定類型的類裝入器,您還可能遇到問題。特別是對于那些不把裝入工作先委托給上一級的類裝入器,以及那些在文件系統中查找代碼基的裝入器,您 可能會碰到問題。不過,One-JAR 中包含了一個機制,可以在文件系統中展開 JAR 文件項,這應當有幫助。這個機制由 META-INF/MANIFEST.MF 文件中的 One-JAR-Expand 屬性控制。另外,您可以試著用字節碼操縱技術動態地修改類裝入器,這樣可以不破壞支持 JAR 文件的完整性。如果您采用這種方法,那么每種個別情況可能都需要一個定制的包裝類裝入器。

請參閱 參閱資料 以下載 FatJar Eclipse 插件和 JarPlug,并了解更多關于 One-JAR 的內容。



參考資料

關于作者

P. Simon Tuffs 博士是一位獨立顧問,目前的研究領域是 Java Web 服務的可伸縮性。在業余時間里,他創建并發布一些開源項目,比如 One-JAR。

用One-JAR簡化應用程序交付


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 香蕉视频黄色在线观看 | 一级黄色毛片播放 | 天天操视频 夜夜 | 天天看天天射 | 国产精品久久亚洲一区二区 | 四虎成人精品国产一区a | 精品久久洲久久久久护士免费 | 国产国语对白一级毛片 | 亚洲国产视频在线观看 | 97视频在线观看视频最新 | 一本大道香蕉大在线最新 | 91在线免费公开视频 | 久久99国产精品亚洲 | 女人十八毛片免费特黄 | 在线日产一区二区 | 精品久久久久久亚洲 | 草久在线观看视频 | 亚洲一区二区三区福利在线 | 国产成人aa在线观看视频 | 黄色四虎影院 | 久久综合成人网 | 久99久热只有精品国产99 | 国产精品 视频一区 二区三区 | 国产综合精品久久久久成人影 | 人人做人人爽久久久精品 | 欧美24video| 亚洲欧洲国产精品你懂的 | 国产日韩欧美一区 | 天天射天天添 | 欧美视频一区二区专区 | 我色综合 | 成年性午夜免费视频网站不卡 | 欧美成人激情视频 | 欧美一区二区在线 | 神马影院888不卡院 神马影院不卡不卡在线观看 | 一本色道久久综合亚洲精品高清 | 伊人色综合久久天天网 | 黄色视屏在线免费观看 | 久久久久久中文字幕 | 欧美精品亚洲精品日韩专区va | 91福利视频在线 |