你所不知道的五件事情--JAR文件
這是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,講述了關于JAR的一些應用竅門,值得大家學習。(2010.06.27最后更新)
摘要:許多Java開發(fā)者從沒有深入思考過JAR--他們只是在將類傳到產(chǎn)品服務器之前使用JAR打包這些文件罷了。但JAR并不僅僅是一個被重命名的 ZIP文件。學習如何使用Java歸檔文件的全部能力,包括打包Spring依賴和配置文件的小竅門。
對大多數(shù)Java開發(fā)者而言,JAR與它的兄弟們,WAR和EAR,都是一長串Ant或Maven處理后的最終結(jié)果。一個標準的過程是將JAR復制到服務器的適應位置(或者,更少見地,復制到用戶的機器上),然后就把它遺忘了。
準確地說,JAR能做的遠不止存儲源代碼,但你必須要知道它能做的其它事情,以及怎樣去使用它。在本"5 things"系列的分期文章中所介紹的竅門將展示如何制作大部分的Java歸檔文件(在有些例子中,也會涉及WAR和EAR),特別是在開發(fā)時期。
因為有眾多的Java開發(fā)者在使用Spring(也因為Spring框架展示了一些相對于我們對JAR傳統(tǒng)應用的挑戰(zhàn)),其中若干竅門是特別針對Spring應用中的JAR文件。
我將以一個標準的Java歸檔文件處理的例子開始,該例是下面各竅門的基礎。
置于JAR中
一般地,在編譯源代碼之后你會制作一個JAR文件,通過jar命令工具,或更為通用的Ant的jar工作,去把Java代碼(已經(jīng)被包分隔開)歸集到單個文件中。這種處理很明了,在此處我就不作展示了,但在本文的后面我將回到JAR文件是如何被構(gòu)造的這個主題中來。現(xiàn)在,我們只需打包Hello類,這是一個獨立運行的控制臺工具應用,該應用會向控制臺打印一條信息,這無疑是很有用的任務,如清單1所示:
清單1. 用于打包的控制臺工具
package com.tedneward.jars;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Howdy!");
}
}
Hello工具并不復雜,但以可執(zhí)行程序開始,它是探索JAR文件的一個有用的輔助手段。
1. JAR是可執(zhí)行的
像.NET和C++這樣的編程語言,在歷史上有操作系統(tǒng)友好方面優(yōu)勢,只需在命令行中引入它們的名字(helloWorld.exe)或在GUI Shell中雙擊它們的圖標就會啟動這些應用。然而在Java編程中,啟動器程序--java--引導JVM運行,而后我們必須傳入一個命令行參數(shù) (com.tedneward.Hello)用于指定將要啟動的含有main()方法的類。
這些額外的步驟使得很難為Java創(chuàng)建用戶友好的應用。不僅僅是由于最終用戶必須在命令行中鍵入所有的這些元素(很多用戶都想避免這種情況),而且他或她會由于某種原因打錯字并得到一個晦澀的錯誤返回。
解決方案就是使JAR文件"可執(zhí)行",以便在執(zhí)行JAR文件時,能讓Java啟動器自動地知道啟動哪個類。我們所需要做的只是在JAR文件的manifest(JAR文件META-INF中的MENIFEST.MF文件)中引入一個屬性,例如:
清單2. 顯示入口點
Main-Class: com.tedneward.jars.Hello
manifest就是一組名值對。因為mainfest有時候?qū)剀嚭涂崭癖容^敏感,所以在制作JAR時使用Ant去生成該文件要方便些。在清單3中,我就在Ant的jar任務中使用了manifest元素去指定要生成的manifest:
清單3. 構(gòu)建入口點
<target name="jar" depends="build">
<jar destfile="outapp.jar" basedir="classes">
<manifest>
<attribute name="Main-Class" value="com.tedneward.jars.Hello" />
</manifest>
</jar>
</target>
現(xiàn)在為了執(zhí)行JAR文件,用戶所需要做的只是在命令行中指定文件名,通過命令java -jar outapp.jar。對于這種情況,在有些GUI Shell中雙擊JAR文件也是可以的。
2. JAR能夠包含依賴信息
Hello工具類的文字似乎已經(jīng)擴展了,這樣對不同的實現(xiàn)的需求就變得很緊急了。像Spring或Guice這樣的依賴注入(DI)容器為我們處理了許多細節(jié),但仍有一點兒障礙:
清單4. Hello, Spring world!
package com.tedneward.jars;
import org.springframework.context.*;
import org.springframework.context.support.*;
public class Hello
{
public static void main(String[] args)
{
ApplicationContext appContext =
new FileSystemXmlApplicationContext("./app.xml");
ISpeak speaker = (ISpeak) appContext.getBean("speaker");
System.out.println(speaker.sayHello());
}
}
因為啟動器的-jar選項會被命令行中的-classpath選項所覆蓋,那么當你運行上述程序時,Spring需要出現(xiàn)在CLASSPATH中,并且要在環(huán)境變量中。幸運地是,JAR允許針對其它JAR依賴的聲明出現(xiàn)在manfiest中,這就隱式地創(chuàng)建了CLASSPATH而不需要你去聲明,如清單5 所示:
清單5. Hello, Spring CLASSPATH!
<target name="jar" depends="build">
<jar destfile="outapp.jar" basedir="classes">
<manifest>
<attribute name="Main-Class" value="com.tedneward.jars.Hello" />
<attribute name="Class-Path"
value="./lib/org.springframework.context-3.0.1.RELEASE-A.jar
./lib/org.springframework.core-3.0.1.RELEASE-A.jar
./lib/org.springframework.asm-3.0.1.RELEASE-A.jar
./lib/org.springframework.beans-3.0.1.RELEASE-A.jar
./lib/org.springframework.expression-3.0.1.RELEASE-A.jar
./lib/commons-logging-1.0.4.jar" />
</manifest>
</jar>
</target>
注意到Class-Path屬性包含有該應用的依賴相對于JAR的引用路徑。你也可以寫絕對引用路徑,或者完全不需要前綴,在這種情況下就要假設這些依賴 JAR文件與應用程序的JAR文件在同一目錄下。
不幸地是,Ant的Class-Path屬性對應的value屬性必須出現(xiàn)在一行中,因為JAR manfiest無法應對多個Class-Path屬性,所以,所有的依賴必須出現(xiàn)在manifest文件的同一行中。可以肯定的是,這種做法很丑陋,但為了能使用命令java -jar outapp.jar,這是值得的。
3. 可隱式地引用JAR
如果你有多個不同的命令行工具(或其它的應用)需要使用Spring框架的JAR文件,那么將這些Spring JAR文件置于公共路徑中,以便所有的工具類都能被引用到。這樣做就能避免文件系統(tǒng)中滿是JAR文件的多份拷貝。Java運行時環(huán)境的公共JAR文件路徑,即大家所知的"擴展目錄",默認是位于JRE安裝路徑下的lib/ext子目錄中。
JRE是一個可定制的路徑,但仍然很少在一個給定的Java環(huán)境中定制該路徑使我們能夠安全放心地假設lib/ext是一個存放JAR文件的安全地方,該目錄中的JAR文件將默認出現(xiàn)在Java運行時環(huán)境的CLASSPATH中。
4. Java 6允許類路通配符
作為一種避免龐大CLASSPATH環(huán)境變量(Java開發(fā)者在多年前就已經(jīng)拋棄它了)和/或命令行-classpath參數(shù)的努力,Java 6引入了類路徑通配符選項。與在啟動時必須在一個參數(shù)中顯示地列出每個JAR文件不同,類路徑通配符允許你通過lib/*來指定該目錄下的所有JAR文件 (但不允許遞歸其子目錄中的JAR文件)設置到類路徑中。
不幸地是,類路徑通配符并不能支持之前討論過的Class-Path屬性manifest條目。為了某些開發(fā)者任務,例如代碼生成或分析工具,使用類路徑通配符可以更方便地啟動Java應用程序(包括服務器)。
5. JAR不只是包含代碼
就像Java生態(tài)系統(tǒng)中的許多組成部分那樣,Spring依賴一個配置文件,該文件描述了如何去構(gòu)建運行環(huán)境。如前所述,Spring依賴app.xml 文件,該文件與JAR文件存在于同一個目錄下--但經(jīng)常地,開發(fā)者們會忘記復制JAR文件邊上的配置文件。
sysadmin會編輯某些配置文件,但也有大量的配置文件(如Hibernate映射文件)在sysadmin的域之外,這將導致發(fā)布錯誤。一種明智的解決方案就是將代碼與配置文件打包在一起--這是可行的,因為JAR本質(zhì)上就是改頭換面的ZIP。只需將配置文件包含在Ant任務中,或使用jar命令去構(gòu)建JAR文件。
不僅僅是配置文件,JAR還可以包含其它類型的文件。例如,如果我的SpeakEnglish組件想到訪問一個屬性文件,那么我會像清單6那樣進行設置:
清單6. 隨機響應
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
Random random = new Random();
public String sayHello()
{
// Pick a response at random
int which = random.nextInt(5);
return responses.getProperty("response." + which);
}
}
將responses.properties置入JAR文件就意味著,不需要操心有太多文件要隨JAR文件一同部署了。要做到這些,只需在制作JAR的過程中包含上responses.properties文件。
一旦你在JAR中存放了配置文件,你就可能就想著如何得到它。如果你所想要的數(shù)據(jù)位于同一JAR文件中,可以讓類的ClassLoader將該文件作為 JAR文件中的"資源"進行查找,使用ClassLoader的getResourceAsStream()方法,如清單7所示:
清單7. ClassLoader定位資源
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
// 
public SpeakEnglish()
{
try
{
ClassLoader myCL = SpeakEnglish.class.getClassLoader();
responses.load(
myCL.getResourceAsStream(
"com/tedneward/jars/responses.properties"));
}
catch (Exception x)
{
x.printStackTrace();
}
}
// 
}
你能使用這種操作找到任何類型的資源:配置文件,音頻文件,圖形文件,以及你所命名的其它文件。事實上,任何文件類型都可以綁定到JAR文件中,并通過 InputStream可再獲得該文件(使用ClassLoader類),然后就可以任何符合你喜好的方式來使用它們了。
結(jié)論
本文涵蓋了關于JAR的多數(shù)Java開發(fā)者最不知道的5件事情--至少基于歷史和軼事證據(jù)可以這么認為。注意,所有這些與JAR相關的竅門也同樣適用于 WAR。有些竅門(特別是Class-Path和Main-Class屬性)對于WAR不完全正確,因為Servlet環(huán)境會獲取目錄中的全部內(nèi)容并有一個預定義的入口點。但綜合來看,這些竅門還是讓我們超越了這樣一種范式:"好吧,讓我們開始復制目錄下的所有文件吧..."。除了這些,他們還使得部署 Java應用變得非常容易。
本系列的下一往篇文章是:你所不知道的五件事情--Java應用的性能監(jiān)控。