企業(yè)應(yīng)用的Ant模組編譯環(huán)境
作者:Les A. Hazlewood
06/22/2005
翻譯:tetsu版權(quán)聲明:可以任意轉(zhuǎn)載,轉(zhuǎn)載時(shí)請(qǐng)務(wù)必以超鏈接形式標(biāo)明文章原始出處和作者信息及本聲明
英文原文地址:
http://www.onjava.com/pub/a/onjava/2005/06/22/modularant.html中文地址:
http://www.matrix.org.cn/resource/article/43/43716_Ant.html關(guān)鍵詞: Ant Compile
編譯環(huán)境對(duì)于今日的Java企業(yè)級(jí)應(yīng)用程序來說,越來越難于管理了。堆積如山的代碼,配置文件,以及對(duì)第三方的依賴(third-party dependencies)都使得管理編譯環(huán)境變得困難。
簡(jiǎn)而言之,我們勉強(qiáng)接受那種把所有的源代碼放在一個(gè)根目錄下,所有的配置文件放在另一個(gè)根目錄下,而第三方類庫(kù)也這樣處理的做法。但是企業(yè)級(jí)編譯環(huán)境很少這么做。今日的企業(yè)級(jí)Java項(xiàng)目,在結(jié)構(gòu),功能,以及組織上都很復(fù)雜。它們通常都有大量的源代碼和支持資源(屬性文件,圖片,等等。編者注:原文為supporting artifacts,直譯為支持物件,但這里根據(jù)上下文意譯為支持資源較妥)要去管理。有這么多的東西去管理,當(dāng)一個(gè)開發(fā)團(tuán)隊(duì)試圖去建立一個(gè)優(yōu)化的編譯方案時(shí),他們常常感到困惑和挫敗。
如果,不管這個(gè)項(xiàng)目有多大,我們的編譯環(huán)境都能夠在統(tǒng)一的構(gòu)架中簡(jiǎn)潔地處理我們所有的源代碼,事情是不是會(huì)變得好一些呢?本文將展示一個(gè)Ant編譯環(huán)境的例子,它來自我對(duì)多年來的多個(gè)項(xiàng)目的經(jīng)驗(yàn)的修改。此時(shí)此地,它或許不是最好的方案,但是它的確經(jīng)歷了時(shí)間的考驗(yàn),也一定會(huì)幫助你建立并運(yùn)行在大多數(shù)項(xiàng)目上,不管是大是小。
警告先就一些問題說明一下,這樣你就不會(huì)讀完了這篇文章才發(fā)現(xiàn)它對(duì)你沒有任何價(jià)值:
· 本文基于對(duì)Ant的了解。它是針對(duì)那些會(huì)用并喜歡Ant的讀者的。
· 這里所說的編譯環(huán)境是指模組(modular)和模塊(module),而模塊又是由目錄和子目錄來定義的。(譯者注:模組modular是模塊module的集合。它由多個(gè)獨(dú)立的模塊構(gòu)成。)這意味著文件和源代碼被存放在許多不同的目錄中。因此,如果你使用類似Eclipse或IntelliJ Idea這種可以幫你管理類和文件的位置的IDE工具的話,本文對(duì)你會(huì)更加有益。當(dāng)然,你也可以使用文本編譯器,但是恐怕你會(huì)發(fā)現(xiàn)你頻頻地在多棵“目錄樹”上爬上爬下。
概念首先,讓我們來談及掉隱藏在編譯環(huán)境之后的幾個(gè)核心概念。它們是模組,層級(jí)結(jié)構(gòu)(hierarchical),和資源驅(qū)動(dòng)(artifact-driven)。它們確切的含義又是什么呢?
模組模組編譯是指圍繞軟件模塊來進(jìn)行組織的一種編譯方式。一個(gè)模塊是一個(gè)邏輯的,集合的,功能性單元,對(duì)應(yīng)于系統(tǒng)中的一個(gè)特性。對(duì)于編譯環(huán)境而言,一個(gè)模塊表現(xiàn)為源代碼和配置文件的一個(gè)自我包含集合(self-contained collection),這些源代碼和配置文件用來構(gòu)建表現(xiàn)了模塊所對(duì)應(yīng)的那個(gè)命名特性的軟件。它幾乎和你修訂控制系統(tǒng)(RCS:Revision Control System)(例如CVS或者Subversion)中的目錄樹是一一對(duì)應(yīng)的。舉幾個(gè)例子:security, administration, wiki, email都可以是一個(gè)模塊。
層級(jí)結(jié)構(gòu)層級(jí)結(jié)構(gòu)編譯是指含有分層模塊的編譯方式。也就是,對(duì)于一個(gè)模塊,它可能是由更小的,更特定的子模塊(submodule)來構(gòu)成的。
如果一個(gè)模塊含有子模塊,那么它有責(zé)任保證那些子模塊以合適的方式被編譯。
隨后,我們會(huì)討論例子是如何應(yīng)用層級(jí)結(jié)構(gòu)的概念來建立編譯環(huán)境的。
物件驅(qū)動(dòng)物件驅(qū)動(dòng)編譯是指每個(gè)存在的模塊(module)或子模塊(submodule),都是為了產(chǎn)生一個(gè)單獨(dú)的,可部署的物件。在Java項(xiàng)目中,這些物件主要是.jar,.war,或.ear文件。在其他類型的編譯中,它們通常是二進(jìn)制可執(zhí)行文件或動(dòng)態(tài)連接庫(kù)(.dll或.so)。
編譯環(huán)境的例子也是物件驅(qū)動(dòng)的,我們將會(huì)討論它是如何創(chuàng)建可部署的物件的。
盡管這三個(gè)概念都很容易理解,但結(jié)合起來用在編譯環(huán)境中的話,它們會(huì)變得非常強(qiáng)大。
現(xiàn)在讓我們來看看編譯環(huán)境是如何組織的。
模組結(jié)構(gòu)當(dāng)有很多要去實(shí)現(xiàn)的時(shí)候,把問題分解為若干個(gè)小的部分是個(gè)很有效的方法。我們需要一個(gè)好的分而治之(divide-and-conquer)的技術(shù)來幫助我們來管理大量的源碼。在編譯環(huán)境中創(chuàng)建編譯模塊是個(gè)好方法。
我們通過在應(yīng)用程序的根目錄下創(chuàng)建一個(gè)目錄來創(chuàng)建一個(gè)模塊。這個(gè)新的目錄成為這個(gè)模塊的基礎(chǔ)。在每個(gè)模塊目錄下,我們存放與其相關(guān)的文件和源碼。
這是一個(gè)示例程序的編譯環(huán)境,按照模塊來組織:
appname/
|-- admin/
|-- core/
|-- db/
|-- lib/
|-- ordermgt/
|-- reports/
|-- web/
|-- build.xml 下面是每個(gè)節(jié)點(diǎn)的含義:
· 除了lib/ 以外的每個(gè)目錄都是一個(gè)模塊。在這個(gè)例子中,admin模塊提供了POJO的實(shí)現(xiàn),它容許某人來管理應(yīng)用(例如,創(chuàng)建用戶,授權(quán)等等)。同樣的,reports模塊中,有能夠產(chǎn)生報(bào)告的組件的實(shí)現(xiàn)。而core 模塊中是那些在很多或全部模塊中都用到的組件,它們不是真正地和系統(tǒng)的某個(gè)功能相聯(lián)系。(例如,StringUtil 類)通常,其他地所有模塊都會(huì)依賴核心(core)模塊。
其他模塊與admin, reports, 及core模塊一樣:他們有著各自的自包含的系統(tǒng)功能,并與其他模塊區(qū)別開來。此外,由于我們的范例應(yīng)用可以支持基于web的交互,我們還可以有一個(gè)web模塊,包含了用以創(chuàng)建一個(gè).war文件所需要的一切內(nèi)容。
· lib/ 目錄比較特殊。它含有應(yīng)用程序編譯或運(yùn)行所需地所有第三方.jars文件。我們把其他模塊所需的所有第三方.jars文件放在這個(gè)目錄中,而不是它們自己的模塊中。原因如下:
1. 在一個(gè)地方更便于管理對(duì)第三方的依賴(third-party dependencies)??梢栽谝粋€(gè)模塊的build.xml 文件中,利用Ant的<path> 語(yǔ)句來定義改模塊是否使用這些庫(kù)文件。
2. 通過排除重復(fù).jars文件的可能性,從而避免了裝載類或API的版本沖突。如果有不止一個(gè)模塊使用了一個(gè)負(fù)責(zé)存儲(chǔ)commons-logging.jar文件的Jakarta Commons Logging模塊,會(huì)發(fā)生什么情況?假設(shè)每個(gè)模塊都持有Jakarta Commons Logging模塊的備份,這樣就會(huì)有一個(gè)潛在的問題――一個(gè)模塊所持有的備份和另外一個(gè)模塊所持有的版本不同。當(dāng)應(yīng)用程序開始運(yùn)行,只有第一個(gè)在classpath上找到的.jar文件被載入以滿足所需,這就潛在地引起了與其他模塊的沖突。我們通過在根目錄下只持有一個(gè).jar文件來避免這種沖突。
3. 對(duì)第三方的依賴隨你的源碼改變版本。瀏覽很多項(xiàng)目,會(huì)發(fā)現(xiàn),這是你想把你所依賴的庫(kù)文件放在CVS上的最重要原因。通過這樣做,你能確保,無論你從CVS上導(dǎo)出的是那個(gè)版本或那個(gè)分支的軟件,你都能找到第三方類庫(kù)的合適版本來支持你的軟件的特定版本。
· 根build.xml 文件是主要的管理文件。它知道為了編譯每個(gè)模塊,什么文件和目標(biāo)(target:譯者注,應(yīng)該是<target>,是Ant中的一個(gè)語(yǔ)句)是必須的。然后,由模塊來保證這些物件(artifact)被正確的編譯。
例如,假設(shè)一個(gè)項(xiàng)目正在編譯,現(xiàn)在是編譯ordermgt 模塊的時(shí)候了,根編譯文件(root build file)應(yīng)該知道,去調(diào)用ordermgt/build.xml 文件中一個(gè)Ant任務(wù)來完成這編譯。而ordermgt/build.xml 文件應(yīng)該確切的知道要編譯生成ordermgt .jar 文件需要些什么。而且,如果整個(gè)項(xiàng)目被編譯并合并入一個(gè).ear文件,這個(gè)根build.xml 文件應(yīng)該知道如何去構(gòu)建。
根build.xml 文件是怎么知道要去編譯一個(gè)模塊并且以何種順序來編譯的呢?下面是一個(gè)Ant XML文件的一部分,它顯示了build.xml文件是如何完成設(shè)定的目標(biāo)的:
<!-- =========================================
Template target. Never called explicitly,
only used to pass calls to underlying
children modules.
========================================= -->
<target name="template" depends="init">
<-- Define the modules and the order in which
they are executed for any given target.
This means _order matters_. Any
dependencies that are to be satisfied by
one module for another must be declared
in the order the dependencies occur. -->
<echo>Executing "${target}" \
target for the core module...</echo>
<ant target="${target}" dir="core"/>
<echo>Executing "${target}" \
target for the admin module...</echo>
<ant target="${target}" dir="admin"/>
...
</target>
無論根build.xml 文件調(diào)用了哪個(gè)編譯目標(biāo),都由這個(gè)template 目標(biāo)負(fù)責(zé)以一定的順序傳遞給相應(yīng)的子模塊。例如,如果我們想要清理整個(gè)工程,我們應(yīng)該只需在工程的根部調(diào)用clean 目標(biāo)即可,然后下面的任務(wù)將被執(zhí)行:
<!-- =========================================
Clean all modules.
========================================= -->
<target name="clean" depends="init">
<echo>Cleaning all builds"</echo>
<antcall target="template">
<param name="target" value="clean"/>
</antcall>
</target>
根build.xml 文件通過直接調(diào)用template 目標(biāo)來間接地實(shí)現(xiàn)調(diào)用clean 目標(biāo),從而保證了所有模塊都被清理。
上面的模塊組織和相關(guān)的編譯目標(biāo)真地使管理源碼和編譯變得更容易了。這種結(jié)構(gòu)有助于你更快,更容易地找到你想要的源碼。而template 目標(biāo)負(fù)責(zé)管理任務(wù)是如何執(zhí)行的。
但模塊結(jié)構(gòu)最精彩的部分是這里:
在完成了整個(gè)工程的完整編譯后,可以對(duì)任何模塊進(jìn)行獨(dú)立的編譯。只要在命令行中切換到該模塊目錄下,并執(zhí)行:
> ant target然后那個(gè)模塊的build.xml 文件就會(huì)被執(zhí)行。你可以在編譯的任何級(jí)別下運(yùn)行任何目標(biāo),而且只對(duì)該級(jí)別進(jìn)行編譯。
為什么這點(diǎn)很重要?因?yàn)樗菰S你獨(dú)立地工作在你的模塊中,并且只對(duì)該模塊進(jìn)行編譯。這樣,每次你對(duì)該模塊源碼的修改都不需要你重新編譯整個(gè)工程,對(duì)于一個(gè)巨大的工程而言,這將節(jié)省很多時(shí)間。
現(xiàn)在,讓我們來看看一個(gè)獨(dú)立的模塊的內(nèi)部是如何構(gòu)造的。
模塊的內(nèi)容我們按照一般的Java業(yè)界習(xí)慣來組織模塊的目錄結(jié)構(gòu),以便管理源碼。盡管有很多不同的習(xí)慣,我們的編譯環(huán)境中使用一下的目錄結(jié)構(gòu):
modulename
|-- build/
|-- etc/
|-- src/
|-- test/
|-- build.xml 下面是每個(gè)節(jié)點(diǎn)的含義:
· build: 這個(gè)目錄比較特殊,它是由模塊的編譯而產(chǎn)生的。除了它以外,上面所列出的其他文件和目錄都會(huì)被加入修訂控制系統(tǒng)(RCS:Revision Control System)。build目錄中包含編譯過程中所產(chǎn)生的所有文件,從自動(dòng)生成的XML文件到編譯完成的Java .class文件,以及最終的任何發(fā)布文件(distribution artifacts)(如.war, .jar, .ear,等等)。這使得清理編譯變得很容易,只要?jiǎng)h除這個(gè)目錄就好了。
· etc: 這個(gè)目錄中存放編譯或運(yùn)行時(shí)(run time)模塊所需的配置文件。大多數(shù)時(shí)候,你會(huì)在這里找到屬性文件和XML配置文件,例如log4j.properties 或 struts-config.xml。假設(shè)有很多文件,他們通常被存放在他們相關(guān)組件的子目錄中。例如:etc/spring/, etc/struts/, etc/ejb/, 等等。
· src: 這是你源文件目錄樹的根目錄。這里除了與包和(或者)路徑相對(duì)應(yīng)的目錄之外,就別無他物了。因此,在這里你經(jīng)常會(huì)看到com/ 或 net/ 或 org/ 目錄,作為com.whatever 或 net.something 或 org.mydomain 包的開始。要注意,只有和classpath有一一對(duì)應(yīng)關(guān)系的東西才能放在這個(gè)目錄中。例如:包目錄,或 .java源文件。
· test: 這個(gè)目錄用來存放你的測(cè)試類文件。(例如,JUnit測(cè)試單元)。從目錄組織的角度出發(fā),這里最重要的是,這里的包結(jié)構(gòu)是src 目錄的嚴(yán)格鏡像。這很便于管理測(cè)試程序,因?yàn)槟懔⒓淳蜁?huì)知道:moduleroot/test/com/domain/pkg/BusinessObjectTest
是對(duì)moduleroot/src/com/domain/pkg/BusinessObject的進(jìn)行的測(cè)試。這個(gè)簡(jiǎn)單的鏡像技術(shù)對(duì)于管理大量的code是非常有用的。它使你很容易的找到你的測(cè)試程序。
· build.xml: 這個(gè)Ant文件知道如何去做每件這個(gè)模塊需要做的事情,以完成編譯和分配它所負(fù)責(zé)的物件。如果當(dāng)前模塊含有子模塊,那么它也知道如何去編譯以及按何種順序去編譯那些子模塊。子模塊和編譯順序都是我們很快要解釋的,非常重要的概念。
子模塊子模塊就是另外一個(gè)模塊(父模塊)的子集。你或許看過其他的模式――基于Ant的扁平層級(jí):例如,只有一層深度的結(jié)構(gòu)。而我們的編譯結(jié)構(gòu)要走的更遠(yuǎn)一些:我們有2層。
繼續(xù)我們的編譯和子模塊的概念,你將會(huì)看到如下的一個(gè)編譯層次,模塊和子模塊目錄展開如下:
module1/
submodule1.1/
|-- etc/
|-- src/
...
|-- build.xml
submodule1.2/
|-- etc/
|-- src/
...
|-- build.xml
build.xml
module2/
... OK, 這個(gè)看起來有點(diǎn)復(fù)雜。我們?yōu)槭裁葱枰@個(gè)?
讓我們補(bǔ)充點(diǎn)企業(yè)級(jí)應(yīng)用和物件驅(qū)動(dòng)的背景知識(shí),再來回答這個(gè)問題。
企業(yè)級(jí)應(yīng)用程序大部分情況下都是基于客戶/服務(wù)器模式的。即便你只是開發(fā)一個(gè)網(wǎng)頁(yè)應(yīng)用程序,它通常構(gòu)成一個(gè)客戶/服務(wù)器MVC應(yīng)用。這樣,網(wǎng)頁(yè)本身就是一個(gè)客戶視角,而“服務(wù)器”端組件通常是商業(yè)POJO,它代替發(fā)布網(wǎng)頁(yè)組件執(zhí)行商業(yè)邏輯。盡管它們?cè)谝粋€(gè) .war文件中,但是在主要用于繪制視圖(rendering a view)(客戶端代碼)的代碼和用于處理商業(yè)請(qǐng)求(服務(wù)器端代碼)的代碼之間有明確的架構(gòu)分離。至少,在這里是這樣的!
在傳統(tǒng)的客戶/服務(wù)器應(yīng)用程序中,客戶和服務(wù)器的概念更加明顯。那里有獨(dú)立的客戶GUI通過socket和服務(wù)器端的商業(yè)對(duì)象進(jìn)行通信。
如果對(duì)于客戶應(yīng)用程序我們只需要改動(dòng)客戶端代碼,對(duì)于服務(wù)器端程序只改動(dòng)服務(wù)器端源碼,那將是簡(jiǎn)潔而優(yōu)雅的。這兩層也共享一些通用代碼,因此向客戶端和服務(wù)器端發(fā)送共用的.jar文件也是好主意。我們的編譯環(huán)境有能力實(shí)現(xiàn)我們的想法。
接下來我們會(huì)看到子模塊是如何幫我我們實(shí)現(xiàn)物件驅(qū)動(dòng)編譯的。
分級(jí)結(jié)構(gòu)和編譯物件部署場(chǎng)景(deployment scenario)只描述了物件驅(qū)動(dòng)編譯的表層需求:編譯環(huán)境中的每個(gè)模塊或子模塊復(fù)雜創(chuàng)建一個(gè)會(huì)被部署到客戶或服務(wù)器端的物件。在我們的編譯環(huán)境中這個(gè)很容易實(shí)現(xiàn),只要把已有的模塊進(jìn)一步分解為common, client, 和 server 子模塊就好了。父子關(guān)系以及編譯責(zé)任委托也促成這種編譯層級(jí)結(jié)構(gòu)。
以我們示例程序中的admin 模塊為例,讓我們看看在展開的目錄樹中這種層級(jí)結(jié)構(gòu)是怎樣的:
appname/
|-- admin/
|-- common/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- client/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- server/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- build.xml
... 每個(gè)子模塊的內(nèi)容都按照先前定義的構(gòu)建,但是有些值得注意的不同:
admin 模塊沒有通常模塊的那些內(nèi)容。它只含有子模塊和一個(gè)build.xml文件,而且它自己也不會(huì)產(chǎn)生任何編譯產(chǎn)物。而是通過先前描述的模板(template)技術(shù)來調(diào)用common/build.xml, server/build.xml, 和 client/build.xml 文件中的編譯目標(biāo)來完成編譯工作的。
因此,如果你想編譯admin 模塊,你只需要切換到admin目錄下,然后運(yùn)行Ant:
> cd admin/
> ant這個(gè)命令會(huì)調(diào)用admin的build.xml 文件,其結(jié)果是編譯common, server, 和 client 子模塊。在每個(gè)子模塊被編譯后,將會(huì)產(chǎn)生三個(gè)物件:
appname-admin-common.jar
appname-admin-server.jar
appname-admin-client.jar
common 和 server .jars 文件可以被部署到服務(wù)器端(例如,在一個(gè) .ear文件中),而common and client .jars 文件可以被部署到客戶端(如,在 .war文件的WEB-INF/lib 目錄中)。
每個(gè)子模塊的目的是什么呢?它們幫助以簡(jiǎn)潔的功能性子集來組織代碼,這些子集會(huì)被部署到應(yīng)用程序的不同層面上。一下是上述三個(gè)子模塊通常所含有的內(nèi)容:
· common: 模塊中,所有在客戶和服務(wù)器層都會(huì)用到的代碼。這通常意味這POJO接口,單元類,等等。
· server:服務(wù)器層所需類的實(shí)現(xiàn)。它們通常是商業(yè)POJO接口的實(shí)現(xiàn),DAO為EIS連接的實(shí)現(xiàn),等等。
· client: 客戶層所需類的實(shí)現(xiàn),例如Swing GUI對(duì)象,EJO遠(yuǎn)程接口,等等。
這種子模塊的獨(dú)立性(granularity)及其相應(yīng)的部署物件從4個(gè)方面對(duì)你產(chǎn)生實(shí)質(zhì)性幫助:
1. 下載時(shí)間:你可以確保獨(dú)立的客戶端應(yīng)用程序,例如applet或Java Web Start 程序,只需下載程序運(yùn)行所需的.jar文件的最小子集。這樣可以確保第一次運(yùn)行的applet或應(yīng)用程序能盡快的被下載。
2. 依賴性管理: 通過子模塊的build.xml 文件中Ant的<path> 標(biāo)識(shí),你可以添加當(dāng)前子模塊所依賴的其他模塊或子模塊。這樣避免了任何懶惰或意外的使用了開發(fā)者不支持的或在運(yùn)行時(shí)不支持的API
。
3. 依賴性順序: 因?yàn)楦改K決定了子模塊的編譯順序,因此你可以重新設(shè)定以確保你寫的客戶端代碼依賴于common 代碼,而不依賴于服務(wù)器端代碼。同樣,common 代碼不能依賴于服務(wù)器或客戶端代碼。如果你沒有這么做,編譯就會(huì)中止,你就會(huì)立即的得到警告,你用了你不該使用的類。這聽起來好像是很瑣碎的問題,但是在復(fù)雜的工程或那些程序員有著不同的經(jīng)驗(yàn)的情況下,這個(gè)問題會(huì)迅速浮出水面,而且不會(huì)在依賴性管理中被注意到。
4. 正如同處理模塊一樣,你也可以通過切換到子模塊的目錄里并運(yùn)行以下命令來對(duì)一個(gè)子模塊進(jìn)行單獨(dú)編譯:
> ant 而Ant會(huì)只編譯該模塊,節(jié)省了時(shí)間。
結(jié)論模塊和子模塊看起來有點(diǎn)復(fù)雜。在這點(diǎn)上它們?cè)谀憧磥砗孟鬀]此必要。但請(qǐng)相信我的經(jīng)驗(yàn),它們極大的簡(jiǎn)化你如何管理源碼和關(guān)聯(lián)文件,以及Ant是如何編譯你的產(chǎn)品的。在這里定義的結(jié)構(gòu)真真正正地使得產(chǎn)品特性和源碼管理在團(tuán)隊(duì)環(huán)境下變得更容易了。它剔除了為了實(shí)現(xiàn)全部組織工作所臆想的許多任務(wù),而且一旦建立起來,它是相當(dāng)透明的。如果你正在開始一項(xiàng)新的客戶/服務(wù)器模式的項(xiàng)目,那么就試著應(yīng)用它。你會(huì)有更多的時(shí)間投入到你的程序中,而不用在為配置管理而但心。
特別感謝Transdyn delivers 的Jeremy Haile,他提供了有意義的信息和審閱了本文。
資源· 本文的范例代碼:[
下載文件]
·
Ant ·
JUnit(參見O‘Reilly CodeZoo: JUnit)
Les A. Hazlewood 是Atlanta, Georgia.的 Roundbox Media 的軟件工程負(fù)責(zé)人。