JAR 文件是什么?
JAR 文件格式以流行的 ZIP 文件格式為基礎(chǔ),用于將許多個文件聚集為一個文件。與 ZIP 文件不同的是,JAR 文件不僅用于壓縮和發(fā)布,而且還用于部署和封裝庫、組件和插件程序,并可被像編譯器和 JVM 這樣的工具直接使用。在 JAR 中包含特殊的文件,如 manifests 和部署描述符,用來指示工具如何處理特定的 JAR。
一個 JAR 文件可以用于:
□ 用于發(fā)布和使用類庫
□ 作為應(yīng)用程序和擴(kuò)展的構(gòu)建單元
□ 作為組件、applet 或者插件程序的部署單位
□ 用于打包與組件相關(guān)聯(lián)的輔助資源
JAR 文件格式提供了許多優(yōu)勢和功能,其中很多是傳統(tǒng)的壓縮格式如 ZIP 或者 TAR 所沒有提供的。它們包括:
☆ 安全性。 可以對 JAR 文件內(nèi)容加上數(shù)字化簽名。這樣,能夠識別簽名的工具就可以有選擇地為您授予軟件安全特權(quán),這是其他文件做不到的,它還可以檢測代碼是否被篡改過。
☆ 減少下載時間。 如果一個 applet 捆綁到一個 JAR 文件中,那么瀏覽器就可以在一個 HTTP 事務(wù)中下載這個 applet 的類文件和相關(guān)的資源,而不是對每一個文件打開一個新連接。
☆ 壓縮。JAR 格式允許您壓縮文件以提高存儲效率。
☆ 傳輸平臺擴(kuò)展。 Java 擴(kuò)展框架(Java Extensions Framework)提供了向 Java 核心平臺添加功能的方法,這些擴(kuò)展是用 JAR 文件打包的(Java 3D 和 JavaMail 就是由 Sun 開發(fā)的擴(kuò)展例子)。
☆ 包密封。 存儲在 JAR 文件中的包可以選擇進(jìn)行密封,以增強(qiáng)版本一致性和安全性。密封一個包意味著包中的所有類都必須在同一 JAR 文件中找到。
☆ 包版本控制。 一個 JAR 文件可以包含有關(guān)它所包含的文件的數(shù)據(jù),如廠商和版本信息。
☆ 可移植性。 處理 JAR 文件的機(jī)制是 Java 平臺核心 API 的標(biāo)準(zhǔn)部分。
壓縮的和未壓縮的 JAR
jar 工具在默認(rèn)情況下壓縮文件。未壓縮的 JAR 文件一般可以比壓縮過的 JAR 文件更快地裝載,因?yàn)樵谘b載過程中要解壓縮文件,但是未壓縮的文件在網(wǎng)絡(luò)上的下載時間可能更長。
META-INF 目錄
大多數(shù) JAR 文件包含一個 META-INF 目錄,它用于存儲包和擴(kuò)展的配置數(shù)據(jù),如安全性和版本信息。Java 2 平臺識別并解釋 META-INF 目錄中的下述文件和目錄,以便配置應(yīng)用程序、擴(kuò)展和類裝載器:
☆ MANIFEST.MF。 這個 manifest 文件定義了與擴(kuò)展和包相關(guān)的數(shù)據(jù)。
☆ INDEX.LIST。 這個文件由 jar 工具的新選項(xiàng) -i 生成,它包含在應(yīng)用程序或者擴(kuò)展中定義的包的位置信息。它是 JarIndex 實(shí)現(xiàn)的一部分,并由類裝載器用于加速類裝載過程。
☆ xxx.SF。 這是 JAR 文件的簽名文件。占位符 xxx 標(biāo)識了簽名者。
☆ xxx.DSA。 與簽名文件相關(guān)聯(lián)的簽名程序塊文件,它存儲了用于簽名 JAR 文件的公共簽名。
jar 工具
為了用 JAR 文件執(zhí)行基本的任務(wù),要使用作為Java Development Kit 的一部分提供的 Java Archive Tool (jar 工具)。用 jar 命令調(diào)用 jar 工具。表 1 顯示了一些常見的應(yīng)用:
表 1. 常見的 jar 工具用法
功能 | 命令 |
用一個單獨(dú)的文件創(chuàng)建一個 JAR 文件 | jar cf jar-file input-file... |
用一個目錄創(chuàng)建一個 JAR 文件 | jar cf jar-file dir-name |
創(chuàng)建一個未壓縮的 JAR 文件 | jar cf0 jar-file dir-name |
更新一個 JAR 文件 | jar uf jar-file input-file... |
查看一個 JAR 文件的內(nèi)容 | jar tf jar-file |
提取一個 JAR 文件的內(nèi)容 | jar xf jar-file |
從一個 JAR 文件中提取特定的文件 | jar xf jar-file archived-file... |
運(yùn)行一個打包為可執(zhí)行 JAR 文件的應(yīng)用程序 | java -jar app.jar |
可執(zhí)行的 JAR
一個可執(zhí)行的 jar 文件是一個自包含的 Java 應(yīng)用程序,它存儲在特別配置的JAR 文件中,可以由 JVM 直接執(zhí)行它而無需事先提取文件或者設(shè)置類路徑。要運(yùn)行存儲在非可執(zhí)行的 JAR 中的應(yīng)用程序,必須將它加入到您的類路徑中,并用名字調(diào)用應(yīng)用程序的主類。但是使用可執(zhí)行的 JAR 文件,我們可以不用提取它或者知道主要入口點(diǎn)就可以運(yùn)行一個應(yīng)用程序??蓤?zhí)行 JAR 有助于方便發(fā)布和執(zhí)行 Java 應(yīng)用程序。
創(chuàng)建可執(zhí)行 JAR
創(chuàng)建一個可執(zhí)行 JAR 很容易。首先將所有應(yīng)用程序代碼放到一個目錄中。假設(shè)應(yīng)用程序中的主類是 com.mycompany.myapp.Sample。您要創(chuàng)建一個包含應(yīng)用程序代碼的 JAR 文件并標(biāo)識出主類。為此,在某個位置(不是在應(yīng)用程序目錄中)創(chuàng)建一個名為 manifest 的文件,并在其中加入以下一行:
Main-Class: com.mycompany.myapp.Sample
然后,像這樣創(chuàng)建 JAR 文件:
jar cmf manifest ExecutableJar.jar application-dir
所要做的就是這些了 -- 現(xiàn)在可以用 java -jar 執(zhí)行這個 JAR 文件 ExecutableJar.jar。
一個可執(zhí)行的 JAR 必須通過 menifest 文件的頭引用它所需要的所有其他從屬 JAR。如果使用了 -jar 選項(xiàng),那么環(huán)境變量 CLASSPATH 和在命令行中指定的所有類路徑都被 JVM 所忽略。
啟動可執(zhí)行 JAR
既然我們已經(jīng)將自己的應(yīng)用程序打包到了一個名為 ExecutableJar.jar 的可執(zhí)行 JAR 中了,那么我們就可以用下面的命令直接從文件啟動這個應(yīng)用程序:
java -jar ExecutableJar.jar
包密封
密封 JAR 文件中的一個包意味著在這個包中定義的所有類都必須在同一個 JAR 文件中找到。這使包的作者可以增強(qiáng)打包類之間的版本一致性。密封還提供了防止代碼篡改的手段。
要密封包,需要在 JAR 的 manifest 文件中為包添加一個 Name 頭,然后加上值為“true”的 Sealed 頭。與可執(zhí)行的 JAR 一樣,可以在創(chuàng)建 JAR 時,通過指定一個具有適當(dāng)頭元素的 manifest 文件密封一個 JAR,如下所示:
Name: com/samplePackage/
Sealed: true
Name 頭標(biāo)識出包的相對路徑名。它以一個“/”結(jié)束以與文件名區(qū)別。在 Name 頭后面第一個空行之前的所有頭都作用于在 Name 頭中指定的文件或者包。在上述例子中,因?yàn)?Sealed 頭出現(xiàn)在 Name 頭后并且中間沒有空行,所以 Sealed 頭將被解釋為只應(yīng)用到包 com/samplePackage 上。
如果試圖從密封包所在的 JAR 文件以外的其他地方裝載密封包中的一個類,那么 JVM 將拋出一個 SecurityException。
擴(kuò)展打包
擴(kuò)展為 Java 平臺增加了功能,在 JAR 文件格式中已經(jīng)加入了擴(kuò)展機(jī)制。擴(kuò)展機(jī)制使得 JAR 文件可以通過 manifest 文件中的 Class-Path 頭指定所需要的其他 JAR 文件。
假設(shè) extension1.jar 和 extension2.jar 是同一個目錄中的兩個 JAR 文件,extension1.jar 的 manifest 文件包含以下頭:
Class-Path: extension2.jar
這個頭表明 extension2.jar 中的類是 extension1.jar 中的類的擴(kuò)展類。extension1.jar 中的類可以調(diào)用 extension2.jar 中的類,并且不要求 extension2.jar 處在類路徑中。
在裝載使用擴(kuò)展機(jī)制的 JAR 時,JVM 會高效而自動地將在Class-Path 頭中引用的 JAR 添加到類路徑中。不過,擴(kuò)展 JAR 路徑被解釋為相對路徑,所以一般來說,擴(kuò)展 JAR 必須存儲在引用它的 JAR 所在的同一目錄中。
例如,假設(shè)類 ExtensionClient 引用了類 ExtensionDemo,它捆綁在一個名為 ExtensionClient.jar 的 JAR 文件中,而類 ExtensionDemo 則捆綁在 ExtensionDemo.jar 中。為了使 ExtensionDemo.jar 可以成為擴(kuò)展,必須將 ExtensionDemo.jar 列在 ExtensionClient.jar 的 manifest 的 Class-Path 頭中,如下所示:
Manifest-Version: 1.0
Class-Path: ExtensionDemo.jar
在這個 manifest 中 Class-Path 頭的值是沒有指定路徑的 ExtensionDemo.jar,表明 ExtensionDemo.jar 與 ExtensionClient JAR 文件處在同一目錄中。
JAR 文件中的安全性
JAR 文件可以用 jarsigner 工具或者直接通過 java.security API 簽名。一個簽名的 JAR 文件與原來的 JAR 文件完全相同,只是更新了它的 manifest,并在 META-INF 目錄中增加了兩個文件,一個簽名文件和一個簽名塊文件。
JAR 文件是用一個存儲在 Keystore 數(shù)據(jù)庫中的證書簽名的。存儲在 keystore 中的證書有密碼保護(hù),必須向 jarsigner 工具提供這個密碼才能對 JAR 文件簽名。
Keystore 數(shù)據(jù)庫
JAR 的每一位簽名者都由在 JAR 文件的 META-INF 目錄中的一個具有 .SF 擴(kuò)展名的簽名文件表示。這個文件的格式類似于 manifest 文件 -- 一組 RFC-822 頭。如下所示,它的組成包括一個主要部分,它包括了由簽名者提供的信息、但是不特別針對任何特定的 JAR 文件項(xiàng),還有一系列的單獨(dú)的項(xiàng),這些項(xiàng)也必須包含在 menifest 文件中。在驗(yàn)證一個簽名的 JAR 時,將簽名文件的摘要值與對 JAR 文件中的相應(yīng)項(xiàng)計算的摘要值進(jìn)行比較。
清單 1. 簽名 JAR 中的 Manifest 和 signature 文件
Contents of signature file META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.3.0 (Sun Microsystems Inc.)
Name: Sample.java
SHA1-Digest: 3+DdYW8INICtyG8ZarHlFxX0W6g=
Name: Sample.class
SHA1-Digest: YJ5yQHBZBJ3SsTNcHJFqUkfWEmI=
Contents of signature file META-INF/JAMES.SF
Signature-Version: 1.0
SHA1-Digest-Manifest: HBstZOJBuuTJ6QMIdB90T8sjaOM=
Created-By: 1.3.0 (Sun Microsystems Inc.)
Name: Sample.java
SHA1-Digest: qipMDrkurQcKwnyIlI3Jtrnia8Q=
Name: Sample.class
SHA1-Digest: pT2DYby8QXPcCzv2NwpLxd8p4G4=
數(shù)字簽名
一個數(shù)字簽名是.SF 簽名文件的已簽名版本。數(shù)字簽名文件是二進(jìn)制文件,并且與 .SF 文件有相同的文件名,但是擴(kuò)展名不同。根據(jù)數(shù)字簽名的類型 -- RSA、DSA 或者 PGP -- 以及用于簽名 JAR 的證書類型而有不同的擴(kuò)展名。
Keystore
要簽名一個 JAR 文件,必須首先有一個私鑰。私鑰及其相關(guān)的公鑰證書存儲在名為 keystores 的、有密碼保護(hù)的數(shù)據(jù)庫中。JDK 包含創(chuàng)建和修改 keystores 的工具。keystore 中的每一個密鑰都可以用一個別名標(biāo)識,它通常是擁有這個密鑰的簽名者的名字。
所有 keystore 項(xiàng)(密鑰和信任的證書項(xiàng))都是用唯一別名訪問的。別名是在用 keytool -genkey 命令生成密鑰對(公鑰和私鑰)并在 keystore 中添加項(xiàng)時指定的。之后的 keytool 命令必須使用同樣的別名引用這一項(xiàng)。
例如,要用別名“james”生成一個新的公鑰/私鑰對并將公鑰包裝到自簽名的證書中,要使用下述命令:
keytool -genkey -alias james -keypass jamespass
-validity 80 -keystore jamesKeyStore
-storepass jamesKeyStorePass
這個命令序列指定了一個初始密碼“jamespass”,后續(xù)的命令在訪問 keystore “jamesKeyStore”中與別名“james”相關(guān)聯(lián)的私鑰時,就需要這個密碼。如果 keystore“jamesKeyStore”不存在,則 keytool 會自動創(chuàng)建它。
jarsigner 工具
jarsigner 工具使用 keystore 生成或者驗(yàn)證 JAR 文件的數(shù)字簽名。
假設(shè)像上述例子那樣創(chuàng)建了 keystore “jamesKeyStore”,并且它包含一個別名為“james”的密鑰,可以用下面的命令簽名一個 JAR 文件:
jarsigner -keystore jamesKeyStore -storepass jamesKeyStorePass
-keypass jamespass -signedjar SSample.jar Sample.jar james
這個命令用密碼“jamesKeyStorePass”從名為“jamesKeyStore”的 keystore 中提出別名為“james”、密碼為“jamespass”的密鑰,并對 Sample.jar 文件簽名、創(chuàng)建一個簽名的 JAR -- SSample.jar。
jarsigner 工具還可以驗(yàn)證一個簽名的 JAR 文件,這種操作比簽名 JAR 文件要簡單得多,只需執(zhí)行以下命令:
jarsigner -verify SSample.jar
如果簽名的 JAR 文件沒有被篡改過,那么 jarsigner 工具就會告訴您 JAR 通過驗(yàn)證了。否則,它會拋出一個 SecurityException, 表明哪些文件沒有通過驗(yàn)證。
還可以用 java.util.jar 和 java.security API 以編程方式簽名 JAR(有關(guān)細(xì)節(jié)參閱參考資料)。也可以使用像 Netscape Object Signing Tool 這樣的工具。
JAR 索引
如果一個應(yīng)用程序或者 applet 捆綁到多個 JAR 文件中,那么類裝載器就使用一個簡單的線性搜索算法搜索類路徑中的每一個元素,這使類裝載器可能要下載并打開許多個 JAR 文件,直到找到所要的類或者資源。如果類裝載器試圖尋找一個不存在的資源,那么在應(yīng)用程序或者 applet 中的所有 JAR 文件都會下載。對于大型的網(wǎng)絡(luò)應(yīng)用程序和 applet,這會導(dǎo)致啟動緩慢、響應(yīng)遲緩并浪費(fèi)帶寬。
從 JDK 1.3 以后,JAR 文件格式開始支持索引以優(yōu)化網(wǎng)絡(luò)應(yīng)用程序中類的搜索過程,特別是 applet。JarIndex 機(jī)制收集在 applet 或者應(yīng)用程序中定義的所有 JAR 文件的內(nèi)容,并將這些信息存儲到第一個 JAR 文件中的索引文件中。下載了第一個 JAR 文件后,applet 類裝載器將使用收集的內(nèi)容信息高效地裝載 JAR 文件。這個目錄信息存儲在根 JAR 文件的 META-INF 目錄中的一個名為 INDEX.LIST 的簡單文本文件中。
創(chuàng)建一個 JarIndex
可以通過在 jar 命令中指定 -i 選項(xiàng)創(chuàng)建一個 JarIndex。假設(shè)我們的目錄結(jié)構(gòu)如下圖所示:
JarIndex
您將使用下述命令為 JarIndex_Main.jar、JarIndex_test.jar 和 JarIndex_test1.jar 創(chuàng)建一個索引文件:
jar -i JarIndex_Main.jar JarIndex_test.jar SampleDir/JarIndex_test1.jar
INDEX.LIST 文件的格式很簡單,包含每個已索引的 JAR 文件中包含的包或者類的名字,如清單 2 所示:
清單 2. JarIndex INDEX.LIST 文件示例
JarIndex-Version: 1.0
JarIndex_Main.jar
sp
JarIndex_test.jar
Sample
SampleDir/JarIndex_test1.jar
org
org/apache
org/apache/xerces
org/apache/xerces/framework
org/apache/xerces/framework/xml4j
結(jié)束語
JAR 格式遠(yuǎn)遠(yuǎn)超出了一種壓縮格式,它有許多可以改進(jìn)效率、安全性和組織 Java 應(yīng)用程序的功能。因?yàn)檫@些功能已經(jīng)建立在核心平臺 -- 包括編譯器和類裝載器 -- 中了,所以開發(fā)人員可以利用 JAR 文件格式的能力簡化和改進(jìn)開發(fā)和部署過程。
轉(zhuǎn)自:http://linxh.bokee.com/2946294.html
Thread.currentThread().getContextClassLoader().getResource("")
Test.class.getClassLoader().getResource("")
ClassLoader.getSystemResource("")
Test.class.getResource("")
Test.class.getResource("/")
new File("/").getAbsolutePath()
System.getProperty("user.dir")
關(guān)于Classloader詳見http://www.aygfsteel.com/lengxinCEO/archive/2007/12/07/166055.html
當(dāng)JVM(Java虛擬機(jī))啟動時,會形成由三個類加載器組成的初始類加載器層次結(jié)構(gòu):
bootstrap classloader
|
extension classloader
|
system classloader
bootstrap classloader -引導(dǎo)(也稱為原始)類加載器,它負(fù)責(zé)加載Java的核心類。在Sun的JVM中,在執(zhí)行java的命令中使用-Xbootclasspath選項(xiàng)或使用 - D選項(xiàng)指定sun.boot.class.path系統(tǒng)屬性值可以指定附加的類。這個加載器的是非常特殊的,它實(shí)際上不是 java.lang.ClassLoader的子類,而是由JVM自身實(shí)現(xiàn)的。大家可以通過執(zhí)行以下代碼來獲得bootstrap classloader加載了那些核心類庫:
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
在我的計算機(jī)上的結(jié)果為:
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
文件:/C:/j2sdk1.4.1_01/jre/classes
這時大家知道了為什么我們不需要在系統(tǒng)屬性CLASSPATH中指定這些類庫了吧,因?yàn)镴VM在啟動的時候就自動加載它們了。
extension classloader -擴(kuò)展類加載器,它負(fù)責(zé)加載JRE的擴(kuò)展目錄(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系統(tǒng)屬性指定的)中JAR的類包。這為引入除Java核心類以外的新功能提供了一個標(biāo)準(zhǔn)機(jī)制。因?yàn)槟J(rèn)的擴(kuò)展目錄對所有從同一個JRE中啟動的JVM都是通用的,所以放入這個目錄的 JAR類包對所有的JVM和system classloader都是可見的。在這個實(shí)例上調(diào)用方法getParent()總是返回空值null,因?yàn)橐龑?dǎo)加載器bootstrap classloader不是一個真正的ClassLoader實(shí)例。所以當(dāng)大家執(zhí)行以下代碼時:
System.out.println(System.getProperty("java.ext.dirs"));
ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
結(jié)果為:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader是system classloader的parent,而bootstrap classloader是extension classloader的parent,但它不是一個實(shí)際的classloader,所以為null。
system classloader -系統(tǒng)(也稱為應(yīng)用)類加載器,它負(fù)責(zé)在JVM被啟動時,加載來自在命令java中的-classpath或者java.class.path系統(tǒng)屬性或者 CLASSPATH*作系統(tǒng)屬性所指定的JAR類包和類路徑。總能通過靜態(tài)方法ClassLoader.getSystemClassLoader()找到該類加載器。如果沒有特別指定,則用戶自定義的任何類加載器都將該類加載器作為它的父加載器。執(zhí)行以下代碼即可獲得:
System.out.println(System.getProperty("java.class.path"));
輸出結(jié)果則為用戶在系統(tǒng)屬性里面設(shè)置的CLASSPATH。
classloader 加載類用的是全盤負(fù)責(zé)委托機(jī)制。所謂全盤負(fù)責(zé),即是當(dāng)一個classloader加載一個Class的時候,這個Class所依賴的和引用的所有 Class也由這個classloader負(fù)責(zé)載入,除非是顯式的使用另外一個classloader載入;委托機(jī)制則是先讓parent(父)類加載器 (而不是super,它與parent classloader類不是繼承關(guān)系)尋找,只有在parent找不到的時候才從自己的類路徑中去尋找。此外類加載還采用了cache機(jī)制,也就是如果 cache中保存了這個Class就直接返回它,如果沒有才從文件中讀取和轉(zhuǎn)換成Class,并存入cache,這就是為什么我們修改了Class但是必須重新啟動JVM才能生效的原因。
每個ClassLoader加載Class的過程是:
1.檢測此Class是否載入過(即在cache中是否有此Class),如果有到8,如果沒有到2
2.如果parent classloader不存在(沒有parent,那parent一定是bootstrap classloader了),到4
3.請求parent classloader載入,如果成功到8,不成功到5
4.請求jvm從bootstrap classloader中載入,如果成功到8
5.尋找Class文件(從與此classloader相關(guān)的類路徑中尋找)。如果找不到則到7.
6.從文件中載入Class,到8.
7.拋出ClassNotFoundException.
8.返回Class.
其中5.6步我們可以通過覆蓋ClassLoader的findClass方法來實(shí)現(xiàn)自己的載入策略。甚至覆蓋loadClass方法來實(shí)現(xiàn)自己的載入過程。
類加載器的順序是:
先是bootstrap classloader,然后是extension classloader,最后才是system classloader。大家會發(fā)現(xiàn)加載的Class越是重要的越在靠前面。這樣做的原因是出于安全性的考慮,試想如果system classloader“親自”加載了一個具有破壞性的“java.lang.System”類的后果吧。這種委托機(jī)制保證了用戶即使具有一個這樣的類,也把它加入到了類路徑中,但是它永遠(yuǎn)不會被載入,因?yàn)檫@個類總是由bootstrap classloader來加載的。大家可以執(zhí)行一下以下的代碼:
System.out.println(System.class.getClassLoader());
將會看到結(jié)果是null,這就表明java.lang.System是由bootstrap classloader加載的,因?yàn)閎ootstrap classloader不是一個真正的ClassLoader實(shí)例,而是由JVM實(shí)現(xiàn)的,正如前面已經(jīng)說過的。
下面就讓我們來看看JVM是如何來為我們來建立類加載器的結(jié)構(gòu)的:
sun.misc.Launcher,顧名思義,當(dāng)你執(zhí)行java命令的時候,JVM會先使用bootstrap classloader載入并初始化一個Launcher,執(zhí)行下來代碼:
System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
結(jié)果為:
the Launcher's classloader is null (因?yàn)槭怯胋ootstrap classloader加載,所以class loader為null)
Launcher 會根據(jù)系統(tǒng)和命令設(shè)定初始化好class loader結(jié)構(gòu),JVM就用它來獲得extension classloader和system classloader,并載入所有的需要載入的Class,最后執(zhí)行java命令指定的帶有靜態(tài)的main方法的Class。extension classloader實(shí)際上是sun.misc.Launcher$ExtClassLoader類的一個實(shí)例,system classloader實(shí)際上是sun.misc.Launcher$AppClassLoader類的一個實(shí)例。并且都是 java.net.URLClassLoader的子類。
讓我們來看看Launcher初試化的過程的部分代碼。
Launcher的部分代碼:
public class Launcher {
public Launcher() {
ExtClassLoader extclassloader;
try {
//初始化extension classloader
extclassloader = ExtClassLoader.getExtClassLoader();
} catch(IOException ioexception) {
throw new InternalError("Could not create extension class loader");
}
try {
//初始化system classloader,parent是extension classloader
loader = AppClassLoader.getAppClassLoader(extclassloader);
} catch(IOException ioexception1) {
throw new InternalError("Could not create application class loader");
}
//將system classloader設(shè)置成當(dāng)前線程的context classloader(將在后面加以介紹)
Thread.currentThread().setContextClassLoader(loader);
......
}
public ClassLoader getClassLoader() {
//返回system classloader
return loader;
}
}
extension classloader的部分代碼:
static class Launcher$ExtClassLoader extends URLClassLoader {
public static Launcher$ExtClassLoader getExtClassLoader()
throws IOException
{
File afile[] = getExtDirs();
return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
}
private static File[] getExtDirs() {
//獲得系統(tǒng)屬性“java.ext.dirs”
String s = System.getProperty("java.ext.dirs");
File afile[];
if(s != null) {
StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
int i = stringtokenizer.countTokens();
afile = new File;
for(int j = 0; j < i; j++)
afile[j] = new File(stringtokenizer.nextToken());
} else {
afile = new File[0];
}
return afile;
}
}
system classloader的部分代碼:
static class Launcher$AppClassLoader extends URLClassLoader
{
public static ClassLoader getAppClassLoader(ClassLoader classloader)
throws IOException
{
//獲得系統(tǒng)屬性“java.class.path”
String s = System.getProperty("java.class.path");
File afile[] = s != null ? Launcher.access$200(s) : new File[0];
return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
}
}
看了源代碼大家就清楚了吧,extension classloader是使用系統(tǒng)屬性“java.ext.dirs”設(shè)置類搜索路徑的,并且沒有parent。system classloader是使用系統(tǒng)屬性“java.class.path”設(shè)置類搜索路徑的,并且有一個parent classloader。Launcher初始化extension classloader,system classloader,并將system classloader設(shè)置成為context classloader,但是僅僅返回system classloader給JVM。
這里怎么又出來一個context classloader呢?它有什么用呢?我們在建立一個線程Thread的時候,可以為這個線程通過setContextClassLoader方法來指定一個合適的classloader作為這個線程的context classloader,當(dāng)此線程運(yùn)行的時候,我們可以通過getContextClassLoader方法來獲得此context classloader,就可以用它來載入我們所需要的Class。默認(rèn)的是system classloader。利用這個特性,我們可以“打破”classloader委托機(jī)制了,父classloader可以獲得當(dāng)前線程的context classloader,而這個context classloader可以是它的子classloader或者其他的classloader,那么父classloader就可以從其獲得所需的 Class,這就打破了只能向父classloader請求的限制了。這個機(jī)制可以滿足當(dāng)我們的classpath是在運(yùn)行時才確定,并由定制的 classloader加載的時候,由system classloader(即在jvm classpath中)加載的class可以通過context classloader獲得定制的classloader并加載入特定的class(通常是抽象類和接口,定制的classloader中是其實(shí)現(xiàn)),例如web應(yīng)用中的servlet就是用這種機(jī)制加載的.
好了,現(xiàn)在我們了解了classloader的結(jié)構(gòu)和工作原理,那么我們?nèi)绾螌?shí)現(xiàn)在運(yùn)行時的動態(tài)載入和更新呢?只要我們能夠動態(tài)改變類搜索路徑和清除 classloader的cache中已經(jīng)載入的Class就行了,有兩個方案,一是我們繼承一個classloader,覆蓋loadclass方法,動態(tài)的尋找Class文件并使用defineClass方法來;另一個則非常簡單實(shí)用,只要重新使用一個新的類搜索路徑來new一個 classloader就行了,這樣即更新了類搜索路徑以便來載入新的Class,也重新生成了一個空白的cache(當(dāng)然,類搜索路徑不一定必須更改)。噢,太好了,我們幾乎不用做什么工作,java.netURLClassLoader正是一個符合我們要求的classloader!我們可以直接使用或者繼承它就可以了!
這是j2se1.4 API的doc中URLClassLoader的兩個構(gòu)造器的描述:
URLClassLoader(URL[] urls)
Constructs a new URLClassLoader for the specified URLs using the default delegation parent ClassLoader.
URLClassLoader(URL[] urls, ClassLoader parent)
Constructs a new URLClassLoader for the given URLs.
其中URL[] urls就是我們要設(shè)置的類搜索路徑,parent就是這個classloader的parent classloader,默認(rèn)的是system classloader。
好,現(xiàn)在我們能夠動態(tài)的載入Class了,這樣我們就可以利用newInstance方法來獲得一個Object。但我們?nèi)绾螌⒋薕bject造型呢?可以將此Object造型成它本身的Class嗎?
首先讓我們來分析一下java源文件的編譯,運(yùn)行吧!javac命令是調(diào)用“JAVA_HOME/lib/tools.jar”中的“com.sun.tools.javac.Main”的compile方法來編譯:
public static int compile(String as[]);
public static int compile(String as[], PrintWriter printwriter);
返回0表示編譯成功,字符串?dāng)?shù)組as則是我們用javac命令編譯時的參數(shù),以空格劃分。例如:
javac -classpath c:\foo\bar.jar;. -d c:\ c:\Some.java
則字符串?dāng)?shù)組as為{"-classpath","c:\\foo\\bar.jar;.","-d","c:\\","c:\Some.java"},如果帶有PrintWriter參數(shù),則會把編譯信息出到這個指定的printWriter中。默認(rèn)的輸出是 System.err。
其中 Main是由JVM使用Launcher初始化的system classloader載入的,根據(jù)全盤負(fù)責(zé)原則,編譯器在解析這個java源文件時所發(fā)現(xiàn)的它所依賴和引用的所有Class也將由system classloader載入,如果system classloader不能載入某個Class時,編譯器將拋出一個“cannot resolve symbol”錯誤。
所以首先編譯就通不過,也就是編譯器無法編譯一個引用了不在CLASSPATH中的未知Class的java源文件,而由于拼寫錯誤或者沒有把所需類庫放到CLASSPATH中,大家一定經(jīng)??吹竭@個“cannot resolve symbol”這個編譯錯誤吧!
其次,就是我們把這個Class放到編譯路徑中,成功的進(jìn)行了編譯,然后在運(yùn)行的時候不把它放入到CLASSPATH中而利用我們自己的 classloader來動態(tài)載入這個Class,這時候也會出現(xiàn)“java.lang.NoClassDefFoundError”的違例,為什么呢?
我們再來分析一下,首先調(diào)用這個造型語句的可執(zhí)行的Class一定是由JVM使用Launcher初始化的system classloader載入的,根據(jù)全盤負(fù)責(zé)原則,當(dāng)我們進(jìn)行造型的時候,JVM也會使用system classloader來嘗試載入這個Class來對實(shí)例進(jìn)行造型,自然在system classloader尋找不到這個Class時就會拋出“java.lang.NoClassDefFoundError”的違例。
OK,現(xiàn)在讓我們來總結(jié)一下,java文件的編譯和Class的載入執(zhí)行,都是使用Launcher初始化的system classloader作為類載入器的,我們無法動態(tài)的改變system classloader,更無法讓JVM使用我們自己的classloader來替換system classloader,根據(jù)全盤負(fù)責(zé)原則,就限制了編譯和運(yùn)行時,我們無法直接顯式的使用一個system classloader尋找不到的Class,即我們只能使用Java核心類庫,擴(kuò)展類庫和CLASSPATH中的類庫中的Class。
還不死心!再嘗試一下這種情況,我們把這個Class也放入到CLASSPATH中,讓system classloader能夠識別和載入。然后我們通過自己的classloader來從指定的class文件中載入這個Class(不能夠委托 parent載入,因?yàn)檫@樣會被system classloader從CLASSPATH中將其載入),然后實(shí)例化一個Object,并造型成這個Class,這樣JVM也識別這個Class(因?yàn)?system classloader能夠定位和載入這個Class從CLASSPATH中),載入的也不是CLASSPATH中的這個Class,而是從 CLASSPATH外動態(tài)載入的,這樣總行了吧!十分不幸的是,這時會出現(xiàn)“java.lang.ClassCastException”違例。
為什么呢?我們也來分析一下,不錯,我們雖然從CLASSPATH外使用我們自己的classloader動態(tài)載入了這個Class,但將它的實(shí)例造型的時候是JVM會使用system classloader來再次載入這個Class,并嘗試將使用我們的自己的classloader載入的Class的一個實(shí)例造型為system classloader載入的這個Class(另外的一個)。大家發(fā)現(xiàn)什么問題了嗎?也就是我們嘗試將從一個classloader載入的Class的一個實(shí)例造型為另外一個classloader載入的Class,雖然這兩個Class的名字一樣,甚至是從同一個class文件中載入。但不幸的是JVM 卻認(rèn)為這個兩個Class是不同的,即JVM認(rèn)為不同的classloader載入的相同的名字的Class(即使是從同一個class文件中載入的)是不同的!這樣做的原因我想大概也是主要出于安全性考慮,這樣就保證所有的核心Java類都是system classloader載入的,我們無法用自己的classloader載入的相同名字的Class的實(shí)例來替換它們的實(shí)例。
看到這里,聰明的讀者一定想到了該如何動態(tài)載入我們的Class,實(shí)例化,造型并調(diào)用了吧!
那就是利用面向?qū)ο蟮幕咎匦灾坏亩嘈涡浴N覀儼盐覀儎討B(tài)載入的Class的實(shí)例造型成它的一個system classloader所能識別的父類就行了!這是為什么呢?我們還是要再來分析一次。當(dāng)我們用我們自己的classloader來動態(tài)載入這我們只要把這個Class的時候,發(fā)現(xiàn)它有一個父類Class,在載入它之前JVM先會載入這個父類Class,這個父類Class是system classloader所能識別的,根據(jù)委托機(jī)制,它將由system classloader載入,然后我們的classloader再載入這個Class,創(chuàng)建一個實(shí)例,造型為這個父類Class,注意了,造型成這個父類 Class的時候(也就是上溯)是面向?qū)ο蟮膉ava語言所允許的并且JVM也支持的,JVM就使用system classloader再次載入這個父類Class,然后將此實(shí)例造型為這個父類Class。大家可以從這個過程發(fā)現(xiàn)這個父類Class都是由 system classloader載入的,也就是同一個class loader載入的同一個Class,所以造型的時候不會出現(xiàn)任何異常。而根據(jù)多形性,調(diào)用這個父類的方法時,真正執(zhí)行的是這個Class(非父類 Class)的覆蓋了父類方法的方法。這些方法中也可以引用system classloader不能識別的Class,因?yàn)楦鶕?jù)全盤負(fù)責(zé)原則,只要載入這個Class的classloader即我們自己定義的 classloader能夠定位和載入這些Class就行了。
這樣我們就可以事先定義好一組接口或者基類并放入CLASSPATH中,然后在執(zhí)行的時候動態(tài)的載入實(shí)現(xiàn)或者繼承了這些接口或基類的子類。還不明白嗎?讓我們來想一想Servlet吧,web application server能夠載入任何繼承了Servlet的Class并正確的執(zhí)行它們,不管它實(shí)際的Class是什么,就是都把它們實(shí)例化成為一個Servlet Class,然后執(zhí)行Servlet的init,doPost,doGet和destroy等方法的,而不管這個Servlet是從web- inf/lib和web-inf/classes下由system classloader的子classloader(即定制的classloader)動態(tài)載入。說了這么多希望大家都明白了。在applet,ejb等容器中,都是采用了這種機(jī)制.
對于以上各種情況,希望大家實(shí)際編寫一些example來實(shí)驗(yàn)一下。
最后我再說點(diǎn)別的, classloader雖然稱為類加載器,但并不意味著只能用來加載Class,我們還可以利用它也獲得圖片,音頻文件等資源的URL,當(dāng)然,這些資源必須在CLASSPATH中的jar類庫中或目錄下。我們來看API的doc中關(guān)于ClassLoader的兩個尋找資源和Class的方法描述吧:
public URL getResource(String name)
用指定的名字來查找資源,一個資源是一些能夠被class代碼訪問的在某種程度上依賴于代碼位置的數(shù)據(jù)(圖片,音頻,文本等等)。
一個資源的名字是以'/'號分隔確定資源的路徑名的。
這個方法將先請求parent classloader搜索資源,如果沒有parent,則會在內(nèi)置在虛擬機(jī)中的classloader(即bootstrap classloader)的路徑中搜索。如果失敗,這個方法將調(diào)用findResource(String)來尋找資源。
public static URL getSystemResource(String name)
從用來載入類的搜索路徑中查找一個指定名字的資源。這個方法使用system class loader來定位資源。即相當(dāng)于ClassLoader.getSystemClassLoader().getResource(name)。
例如:
System.out.println(ClassLoader.getSystemResource("java/lang/String.class"));
的結(jié)果為:
jar:文件:/C:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/String.class
表明String.class文件在rt.jar的java/lang目錄中。
因此我們可以將圖片等資源隨同Class一同打包到j(luò)ar類庫中(當(dāng)然,也可單獨(dú)打包這些資源)并添加它們到class loader的搜索路徑中,我們就可以無需關(guān)心這些資源的具體位置,讓class loader來幫我們尋找了!
quartz報錯:
一直做jdk1.5的項(xiàng)目,突遇一個老項(xiàng)目jdk1.4的,導(dǎo)致tomcat啟動報java.lang.reflect.InvocationTargetException
解決:eclipse中
Windows-->Preferences在彈出的窗口中選擇
java-->compiler在右上方把Compile compliance level改成1.4
我用的installAnyWhere是6.1版本的,有關(guān)installAnyWhere的安裝、破解,網(wǎng)上有很多,我是用自己寫的cracker破解的,如果不注冊,生成的安裝包每次都會提示“該安裝包是用未注冊的installAnyWhere創(chuàng)建的”,非常不爽。所以,最好破解了再用。
網(wǎng)上有關(guān)installAnyWhere的使用,自己看了很久,都覺得不太明白,所以,自己摸索的幾天,下面,就把我自己的使用的一些細(xì)節(jié)介紹給大家,如果你覺得對你有所幫助,那就是我最大的榮幸。
使用步驟:
一、修改本地化文件
installAnyWhere本身支持多種語言的本地化,包括中文。但缺省自帶的簡體中文文件中,幾乎都是繁體中文,因此,如果你的安裝包要支持簡體中文安裝的話,最好先把簡體中文的本地化文件“簡化”一下。
簡化方法如下:
到installAnyWhere安裝目錄的\resource\i18nresources目錄下,找到custom_zh_CN文件,把其中的中文內(nèi)容刪除后重新輸入簡體的漢字,記得輸入法要用簡體中文的。
二、創(chuàng)建新的安裝工程
打開installAnyWhere后,如下圖所示:
選擇“Create New Project”,選擇“Basic Project Template”模版,選擇“Save as”按鈕,彈出保存新建工程的目錄以及名稱,名稱的后綴為.iap_xml。
點(diǎn)擊“Next”進(jìn)入工程設(shè)置界面,也可以直接點(diǎn)擊“Advanced Designer”進(jìn)入高級設(shè)計界面。如果想對工程作一些高級的設(shè)置,就選擇高級設(shè)計界面。
高級設(shè)計界面如下圖所示:
從該界面中可以看到,有很多項(xiàng)設(shè)置,一級導(dǎo)航包括:Project、Installer UI、Organization、Files、Pre-Install、Post-Install、Pre-Uninstall、Post-Uninstall、Build。下面分別逐個介紹。
三、工程設(shè)置
進(jìn)入高級設(shè)計界面的Projiect界面,其中包括了Info、Description、File Setting、Platforms、Locales、Rules、Config、Java。
1、Info設(shè)置
Info中主要設(shè)置工程的一些信息,包括安裝的title、名稱、產(chǎn)品名稱、工程的存放位置、構(gòu)建后的安裝包存放位置等信息,如下圖所示:
如無特殊情況,一般使用缺省設(shè)置即可。
2、Description設(shè)置
Description界面中設(shè)置安裝包的產(chǎn)品可能寫入注冊表中的一些信息,包括:產(chǎn)品名稱、ID、版本、以及支持、提供商等信息,如下圖所示:
3、File Settings
該界面中主要設(shè)置安裝過程中,被安裝的文件的時間如何處理,你可以選擇保留文件本身的時間,也可以設(shè)置為安裝時的時間,還可以設(shè)置成指定的時間。該界面中還可以設(shè)置當(dāng)文件已經(jīng)存在時,如何提示或操作。一般,使用缺省設(shè)置即可。
File Settings的操作界面如下:
4、Platforms
我用的6.1版本的InstallAnywhere,支持三種操作系統(tǒng)平臺:Mac OS X、Windows和UNIX。因此,Platforms設(shè)置界面中,你可以分別對這三種操作平臺的缺省安裝路徑和缺省快捷方式進(jìn)行設(shè)置。Windows平臺的設(shè)置界面如下:
一般使用缺省設(shè)置即可。
5、Locales
Locales項(xiàng)設(shè)置安裝包所支持的安裝界面的語言。缺省是英文安裝語言。只有選擇了除英文之外的至少一種其他語言,安裝包才會在安裝開始時顯示選擇安裝語言的界面。一般選擇支持簡體中文即可。操作界面如下:
在installAnyWhere 的其他的版本手冊中都提及需要注意中文的問題,我在前面已經(jīng)有說明。如果大家在使用中遇到中文顯示有問題的話(尤其是在非windows操作平臺上),也很簡單只要將”*locales”的文件夾中” custom_zh_CN”文件中的中文用UNICODE碼代替就OK了,JDK\BIN中提過了相關(guān)的工具native2ascii.exe運(yùn)行后替換原先的custom_zh_CN就OK了。
6、Rules
Rules項(xiàng)中可以配置多種安裝規(guī)則,如安裝前檢查安裝路徑屬性、安裝平臺或者其他自定義的規(guī)則。一般,我們會用到檢查安裝平臺,如是否是指定的操作系統(tǒng),如下圖所示:
7、Config
“Config”界面“installer debug output”中有倆個文本框,大家可以在里面填上“console”,這樣大家在安裝文件生成后,調(diào)試安裝時可以通過控制臺來查看相關(guān)的提示信息。正是發(fā)布的話,清空即可。其他幾項(xiàng)都可以使用缺省設(shè)置即可。
8、Java
如果需要安裝VM的話,就需要注意VM的安裝路徑,而設(shè)置VM的安裝路徑,就在該界面中:
一般用缺省設(shè)置即可
四、安裝界面
主要設(shè)置安裝的界面相關(guān)信息,一般選擇swing模式即可。該界面中,特別需要注意的就是圖片,如果沒有特別需要,最好使用默認(rèn)的,否則,經(jīng)常出現(xiàn)找不到圖片的問題。如果需要的話,最好在InstallAnywhere的resources目錄中建立圖片文件夾,然后使用該路徑。其他使用缺省設(shè)置即可。
五、組織
Organization界面下主要包含Install Set、Feathures、Components和Modules四個子界面,分別用來設(shè)置安裝集合(如典型安裝、最小化安裝、自定義安裝等)、特性(如公共組件、幫助文檔、應(yīng)用程序等)、組建(要安裝內(nèi)容的各個組成部分)、模塊(其他需要合并的模塊)。在配置安裝集合時,指定哪些特性在某個安裝類型時缺省選擇被安裝,而每個特性中具體包含哪些內(nèi)容,是在組建中配置的。沒有特殊需求的話,使用缺省設(shè)置即可。
六、文件
Files界面中,真正配置要安裝的文件內(nèi)容,也就是哪些內(nèi)容要被添加到安裝包里。缺省的操作界面如下圖所示:
添加文件后,效果圖下圖所示:
其他使用缺省設(shè)置即可。
七、預(yù)安裝
Pre-Install界面比較重要,安裝過程中的各種界面都在這里設(shè)置,如安裝協(xié)議、安裝路徑、程序快捷、預(yù)安裝摘要等。一般簡單安裝的話,使用缺省設(shè)置即可。缺省情況下,沒有安裝協(xié)議界面,如果需要的話,可以通過向?qū)砑印?/p>
點(diǎn)擊“Add Action”按鈕打開選擇action的界面,如下圖所示:
切換到Panels界面,選擇“Panel:License Agreement”條目,點(diǎn)擊“Add”按鈕,然后關(guān)閉選擇action界面。這樣就添加了安裝協(xié)議的界面,如下圖所示:
首先要選中剛剛添加的安裝協(xié)議條目,通過中間的上下箭頭移動安裝協(xié)議條目到合適的位置,然后修改下面的Path,使其指定到你所使用的安裝協(xié)議文件。目前只支持文本和htm倆中格式的協(xié)議文件。
八、安裝后
Post-Install界面中配置安裝完成后的操作,缺省會有倆個界面,一個是提示安裝完成,另一個是重啟操作系統(tǒng),如下圖所示:
如果安裝完成后不需要重啟操作系統(tǒng),就選中“RestartWindows”,然后點(diǎn)擊“Remove”按鈕即可。
九、預(yù)卸載
Pre-Uninstall界面中設(shè)置卸載過程的交互界面和步驟,缺省有三個界面:卸載介紹、選擇卸載類型、選擇要卸載的feature,如下圖所示:
其中的卸載類型與前面Organization中設(shè)置的install set對應(yīng),如install set中有多種安裝類型,那卸載。也就有多種選擇,否則就沒必要選擇卸載類型。卸載的feature也與organization中的feature對應(yīng),如果前面沒有配置,這里也就沒必要配置該panel。
十、卸載后
Post-Uninstall界面也很簡單,主要就是一個卸載完成的提示界面。缺省會有restart windows的步驟,如果不需要重啟操作系統(tǒng),可以直接remove掉restart windows的action。
十一、構(gòu)建
Build界面中,主要設(shè)置要構(gòu)建的安裝包的目標(biāo)平臺以及是否在安裝包中包含VM,如下圖所示:
如果運(yùn)行安裝程序的目標(biāo)機(jī)器上已經(jīng)安裝有VM,就可以選擇安裝包中不包含VM,否則,就要包含VM,否則,安裝包不能執(zhí)行。需要注意的是選擇VM的版本,如果你的程序本身也需要VM運(yùn)行環(huán)境的話,而且需要的VM版本較高,那就要從網(wǎng)上下載合適版本的VM。
依照上面的步驟,可以構(gòu)建出基本功能的安裝包了。后面將介紹一些特殊功能的安裝包的制作,如多語言安裝環(huán)境下的多語言的安裝協(xié)議等。