http://www.oracle.com/technology/global/cn/pub/articles/bodewig_ant1.6.html
為大型項(xiàng)目提供的 Ant 1.6 新特性
作者:Stefan Bodewig
了解 Ant 1.6 的新特性以及它們?nèi)绾斡绊懩M織編譯過程的方式。
雖然 Ant 版本的 1.5.x 系列在任務(wù)級方面有很大的改善,但它沒有改變?nèi)藗兪褂?Ant 的方式。而 Ant 1.6 卻有所不同。它增加了幾個(gè)新特性,以支持大型或非常復(fù)雜的編譯情況。但是,要充分利用它們的功能,用戶可能需要稍微調(diào)整它們的編譯過程。
本文重點(diǎn)介紹了其中的三種新特性 — <macrodef>、<import>、<subant> 任務(wù),表明使用它們可以有什么收獲,以及它們?nèi)绾斡绊懩M織編譯設(shè)置的方式。
宏
大多數(shù)編譯工程師遲早會面臨必須執(zhí)行相同的任務(wù)組合但在幾個(gè)地方配置稍微有點(diǎn)不同的情況。一個(gè)常見的例子是創(chuàng)建一個(gè)web 應(yīng)用程序存檔,對于開發(fā)系統(tǒng)、測試系統(tǒng)和生產(chǎn)系統(tǒng)有著不同的配置。
讓我們假設(shè) web 應(yīng)用程序擁有依賴于目標(biāo)系統(tǒng)的不同的 web 部署描述符,并為開發(fā)環(huán)境使用了一個(gè)不同的 JSP 集合以及一個(gè)不同的資料庫集合。配置信息將放在屬性中,創(chuàng)建 web 存檔的任務(wù)看起來將類似于
<target name="war" depends="jar"> <war destfile="${war.name}" webxml="${web.xml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> </war> </target>
其中 support-libraries 是引用一個(gè)在其它位置定義的 <fileset> ,該引用指向您的應(yīng)用程序所需的附加資料庫的一個(gè)公共集合。
如果您只想一次創(chuàng)建一個(gè) web 存檔,那么您只需要正確地設(shè)置屬性。比如說,您可以從一個(gè)您的目標(biāo)專有的屬性文件中加載它們。
利用 Ant 1.5 創(chuàng)建存檔
現(xiàn)在,假定您想為測試系統(tǒng)和生產(chǎn)系統(tǒng)同時(shí)創(chuàng)建存檔,以確保您真正為兩個(gè)系統(tǒng)打包了相同的應(yīng)用程序。利用 Ant 1.5,您可能使用 <antcall> 來調(diào)用擁有不同屬性設(shè)置的 "war" 目標(biāo),類似:
<target name="production-wars"> <antcall target="war"> <param name="war.name" value="${staging.war.name}"/> <param name="web.xml" value="${staging.web.xml}"/> </antcall> <antcall target="war"> <param name="war.name" value="${production.war.name}"/> <param name="web.xml" value="${production.web.xml}"/> </antcall> </target>
當(dāng)然,這假定兩個(gè)目標(biāo)系統(tǒng)都將使用相同的 jar 和 JSP。
但這種方法有一個(gè)主要缺點(diǎn) — 就是速度慢。<antcall> 重新分析編譯文件,并為每一次調(diào)用重新運(yùn)行調(diào)用的目標(biāo)所依賴的所有目標(biāo)。在上面的例子中,"jar" 目標(biāo)將被運(yùn)行兩次。我們希望這對第二次調(diào)用沒有影響,因?yàn)?"war" 目標(biāo)依賴于它。
利用 Ant 1.6 創(chuàng)建存檔
使用 Ant 1.6,您可以忘掉用 <antcall> 來實(shí)現(xiàn)宏的方法,相反您可以通過參數(shù)化現(xiàn)有的任務(wù)來創(chuàng)建一個(gè)新的任務(wù)。因而上面的例子將變?yōu)椋?/SPAN>
<macrodef name="makewar"> <attribute name="webxml"/> <attribute name="destfile"/> <sequential> <war destfile="@{destfile}" webxml="@{webxml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> </war> </sequential> </macrodef>
這定義了一個(gè)名稱為 makewar 的任務(wù),該任務(wù)可以和任何其它的任務(wù)一樣使用。該任務(wù)有兩個(gè)必需的屬性,webxml 和 destfile。要使屬性可選,我們必需在任務(wù)定義中提供一個(gè)默認(rèn)值。這個(gè)示例假定 ${jar.name} 和 ${jsps} 在編譯期間為常量,從而它們?nèi)匀蛔鳛閷傩灾付āW⒁猓瑢傩栽谑褂萌蝿?wù)時(shí)展開而不是在定義宏的地方展開。
所用任務(wù)的特性幾乎完全和屬性一樣,它們通過 @{} 而不是 ${} 展開。與屬性不同,它們是可變的,也就是說,它們的值可以(并將)隨著每一次調(diào)用而改變。它們也只在您的宏定義程序塊內(nèi)部可用。這意味著如果您的宏定義還包含了另一個(gè)定義了宏的任務(wù),那么您內(nèi)部的宏將看不到包含的宏的屬性。
于是新的 production-wars 目標(biāo)將類似于:
<target name="production-wars"> <makewar destfile="${staging.war.name}" webxml="${staging.web.xml}"/> <makewar destfile="${production.war.name}" webxml="${production.web.xml}"/> </target>
這個(gè)新的代碼段不僅執(zhí)行得快一些,而且也更易讀,因?yàn)閷傩悦Q提供了更多的信息。
宏任務(wù)還可以定義嵌套的元素。<makewar> 定義中的 <war> 任務(wù)的嵌套 <fileset> 可以是這種嵌套元素的一種。可能開發(fā)目標(biāo)需要一些額外的文件或想從不同的位置中挑選 JSP 或資源。以下代碼段將一個(gè)可選的嵌套 <morefiles> 元素添加到了 <makewar> 任務(wù)中
<macrodef name="makewar"> <attribute name="webxml"/> <attribute name="destfile"/> <element name="morefiles" optional="true"/> <sequential> <war destfile="@{destfile}" webxml="@{webxml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> <morefiles/> </war> </sequential> </macrodef>
調(diào)用將類似于:
<makewar destfile="${development.war.name}" webxml="${development.web.xml}"> <morefiles> <fileset dir="${development.resources}"/> <lib refid="development-support-libraries"/> </morefiles> </makewar>
這就像 <morefiles> 的嵌套元素直接在 <war> 任務(wù)內(nèi)部使用的效果一樣。
即使迄今為止的示例僅顯示了包裝單個(gè)任務(wù)的 <macrodef>,但它不限于此。
下面的宏不僅將創(chuàng)建 web 存檔,還將確保包含最終存檔的目錄在試圖寫入之前存在。在一個(gè)實(shí)際的編譯文件中,您可能在調(diào)用任務(wù)之前使用一個(gè)設(shè)置目標(biāo)來完成這個(gè)操作。
<macrodef name="makewar"> <attribute name="webxml"/> <attribute name="destfile"/> <element name="morefiles" optional="true"/> <sequential> <dirname property="@{destfile}.parent" file="@{destfile}"/> <mkdir dir="${@{destfile}.parent}"/> <war destfile="@{destfile}" webxml="@{webxml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> <morefiles/> </war> </sequential> </macrodef>
這里注意兩件事情:
首先,特性在屬性展開之前展開,因此結(jié)構(gòu) ${@{destfile}.parent} 將展開一個(gè)名稱包含了 destfile 特性的值和 ".parent" 后綴的屬性。這意味著您可以將特性展開嵌入到屬性展開中,而不是將屬性展開嵌入特性展開中。
其次,這個(gè)宏定義了屬性,該屬性的名稱基于一個(gè)特性的值,因?yàn)?Ant 中的屬性是全局的并且不可改變。第一次嘗試使用
<dirname property="parent" file="@{destfile}"/>
相反將不會在 "production-wars" 目標(biāo)中的第二次 <makewar> 調(diào)用產(chǎn)生期望的結(jié)果。第一次調(diào)用將定義一個(gè)新的名稱為 parent 的屬性,該屬性指向父目錄 ${staging.war.name}。第二次調(diào)用將查看這個(gè)屬性但不會修改它的值。
預(yù)期 Ant 未來的版本將支持某些類型的限定范圍的屬性,這種屬性只在宏執(zhí)行期間定義。在此之前,使用特性的名稱來構(gòu)建屬性名稱是一種變通辦法,潛在的副作用是要創(chuàng)建大量的屬性。
提示:如果您查看您的編譯文件時(shí)發(fā)現(xiàn)使用了 <antcall> 代替宏,那么強(qiáng)烈建議您考慮使用 macrodef 將其轉(zhuǎn)換成真正的宏。性能影響可能非常顯著,并且還可能產(chǎn)生更易讀和更易于維護(hù)的編譯文件。 |
將一個(gè)編譯文件分成多個(gè)文件有幾個(gè)原因。
- 文件可能變得太大,需要分成幾個(gè)單獨(dú)的部分,以便更易于維護(hù)。
- 您有某個(gè)功能集是多個(gè)編譯文件公用的,您想共享它。
共享公用功能/在 Ant 1.6 之前包含文件
在 Ant 1.6 之前,您唯一的選擇是實(shí)體包含的 XML 方法,類似于:
<!DOCTYPE project [ <!ENTITY common SYSTEM "file:./common.xml"> ]> <project name="test" default="test" basedir="."> <target name="setup"> ... </target> &common; ... </project>
摘自 Ant 常見問題解答。
這種方法有兩個(gè)主要的缺點(diǎn)。您不能使用 Ant 屬性指向您想包含的文件,因此被迫在您的編譯文件中對位置進(jìn)行硬編碼。您想包含的文件只是一個(gè) XML 文件的一部分,它可能沒有一個(gè)根元素,因而使用支持 XML 的工具進(jìn)行維護(hù)更加困難。
共享公用功能/使用 Ant 1.6 包含文件
Ant 1.6 自帶了一個(gè)名稱為 import 的新任務(wù),您現(xiàn)在可以使用它。上面的示例將變?yōu)?/SPAN>
<project name="test" default="test" basedir="."> <target name="setup"> ... </target> <import file="common.xml"/> ... </project>
因?yàn)樗且粋€(gè)任務(wù),因此您可以使用 Ant 所有的特性來指定文件位置。主要的差異是被導(dǎo)入的文件本身必須是一個(gè)有效的 Ant 編譯文件,因而必須有一個(gè)名稱為 project 的根元素。如果您想從實(shí)體包含轉(zhuǎn)換到導(dǎo)入,那么您必須在導(dǎo)入的文件的內(nèi)容首尾放上 <project> 標(biāo)記;然后 Ant 將在讀取文件時(shí)再次劃分它們。
注意文件名稱由 Ant 任務(wù)根據(jù)編譯文件的位置(而不是指定的基本目錄)確定。如果您沒有設(shè)置項(xiàng)目的 basedir 屬性或?qū)⑵湓O(shè)為 ".",那么您將不會注意到任何差異。如果您需要根據(jù)基本目錄解析一個(gè)文件,那么您可以使用一個(gè)屬性作為變通辦法,類似于:
<property name="common.location" location="common.xml"/> <import file="${common.location}"/>
屬性 common.location 將包含文件 common.xml 的絕對路徑,并已根據(jù)導(dǎo)入項(xiàng)目的基本目錄解析。
使用 Ant 1.6,所有的任務(wù)都可能放在目標(biāo)之外或之內(nèi),除了兩個(gè)例外。<import> 一定不能嵌入到目標(biāo)中,<antcall> 一定不能在目標(biāo)外使用(否則它將創(chuàng)建一個(gè)無限循環(huán))。
而 <import> 可做的不僅僅是導(dǎo)入另一個(gè)文件。
首先,它定義了名稱為 ant.file.NAME 的特殊屬性,其中 NAME 替換為每一個(gè)導(dǎo)入文件的 <project> 標(biāo)記的名稱屬性。這個(gè)屬性包含了導(dǎo)入文件的絕對路徑,導(dǎo)入文件可用來根據(jù)它自己的位置(而不是導(dǎo)入文件的基本目錄)定位文件和資源。
這意味著 <project> 的名稱屬性在 <import> 任務(wù)環(huán)境中變得更加重要。它還用來為在被導(dǎo)入的編譯文件中定義的目標(biāo)提供別名。如果導(dǎo)入了以下文件
<project name="share"> <target name="setup"> <mkdir dir="${dest}"/> </target> </project>
導(dǎo)入編譯文件可以查看作為 "setup" 或 "share.setup" 的目標(biāo)。后者在目標(biāo)覆蓋的上下文中變得非常重要。
讓我們假定有一個(gè)包含了多個(gè)獨(dú)立的組件(每個(gè)組件擁有它自己的編譯文件)的編譯系統(tǒng)。這些編譯文件幾乎相同,因此我們決定將公用功能轉(zhuǎn)移到一個(gè)共享和已導(dǎo)入的文件中。為了簡單起見,我們只介紹 Java 文件的編譯和創(chuàng)建結(jié)果的一個(gè) JAR 存檔。共享的文件將類似于
<project name="share"> <target name="setup" depends="set-properties"> <mkdir dir="${dest}/classes"/> <mkdir dir="${dest}/lib"/> </target> <target name="compile" depends="setup"> <javac srcdir="${src}" destdir="${dest}/classes"> <classpath refid="compile-classpath"/> </javac> </target> <target name="jar" depends="compile"> <jar destfile="${dest}/lib/${jar.name}" basedir="${dest}/classes"/> </target> </project>
這個(gè)文件不會作為一個(gè)獨(dú)立的 Ant 編譯文件進(jìn)行工作,因?yàn)樗鼪]有定義 "setup" 所依賴的 "set-properties" 目標(biāo)。
組件 A 的編譯文件可能類似于
<project name="A" default="jar"> <target name="set-properties"> <property name="dest" location="../dest/A"/> <property name="src" location="src"/> <property name="jar.name" value="module-A.jar"/> <path id="compile-classpath"/> </target> <import file="../share.xml"/> </project>
它僅設(shè)置適當(dāng)?shù)沫h(huán)境,然后將全部的編譯邏輯交給被導(dǎo)入的文件負(fù)責(zé)。注意該編譯文件創(chuàng)建了一個(gè)空的路徑作為編譯 CLASSPATH,因?yàn)樗亲园摹DK B 依賴于 A,它的編譯文件將類似于
<project name="B" default="jar"> <target name="set-properties"> <property name="dest" location="../dest/B"/> <property name="src" location="src"/> <property name="jar.name" value="module-B.jar"/> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> </target> <import file="../share.xml"/> </project>
您將注意到該編譯文件與 A 的編譯文件幾乎一樣,因此似乎有可能將大多數(shù)的 set-properties 目標(biāo)也推送到 shared.xml 中。實(shí)際上,我們可以假定有一個(gè)對 dest 和 src 目標(biāo)一致的命名慣例,以實(shí)現(xiàn)這一目的。
<project name="share"> <target name="set-properties"> <property name="dest" location="../dest/${ant.project.name}"/> <property name="src" location="src"/> <property name="jar.name" value="module-${ant.project.name}.jar"/> </target> ... contents of first example above ... </project>
ant.project.name 是一個(gè)內(nèi)置的屬性,它包含了最外面的 <project> 標(biāo)記的名稱屬性的值。因此,如果模塊 A 的編譯文件導(dǎo)入了 share.xml,那么它將擁有值 A。
注意,所有的文件都與導(dǎo)入編譯文件的基本目錄相關(guān),因此 scr 屬性的實(shí)際值依賴于導(dǎo)入文件。
為此,A 的編譯文件將簡單地變?yōu)?/SPAN>
<project name="A" default="jar"> <path id="compile-classpath"/> <import file="../share.xml"/> </project>
B 的編譯文件將變?yōu)?/SPAN>
<project name="B" default="jar"> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> <import file="../share.xml"/> </project>
現(xiàn)在假定 B 增加了一些 RMI 接口,需要在編譯類之后但在創(chuàng)建 jar 之前運(yùn)行 <rmic>。這就是目標(biāo)覆蓋能派上用場的地方。如果我們在導(dǎo)入編譯文件中定義了一個(gè)目標(biāo),該目標(biāo)與被導(dǎo)入的編譯文件中的一個(gè)目標(biāo)名稱相同,那么將使用導(dǎo)入編譯文件中的目標(biāo)。例如,B 可以使用:
<project name="B" default="jar"> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> <import file="../share.xml"/> <target name="compile" depends="setup"> <javac srcdir="${src}" destdir="${dest}/classes"> <classpath refid="compile-classpath"/> </javac> <rmic base="${dest}/classes" includes="**/Remote*.class"/> </target> </project>
在上面的示例中將使用 "compile" 目標(biāo),而不是 share.xml 中的目標(biāo);然而,不幸的是,這只是從共享那里復(fù)制 <javac> 任務(wù)。一種更好的解決方案是:
<project name="B" default="jar"> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> <import file="../share.xml"/> <target name="compile" depends="share.compile"> <rmic base="${dest}/classes" includes="**/Remote*.class"/> </target> </project>
這只是使 B 的 "compile" 在原來的 "compile" 目標(biāo)使用之后運(yùn)行 <rmic>。
如果我們想在編譯之前生成一些 Java 源代碼(例如通過 XDoclet),我們可以使用類似下面的方法:
<import file="../share.xml"/> <target name="compile" depends="setup,xdoclet,share.compile"/> <target name="xdoclet"> .. details of XDoclet invocation omitted .. </target>
因此您可以完全覆蓋一個(gè)目標(biāo)或通過在原始目標(biāo)之前或之后運(yùn)行任務(wù)來增強(qiáng)它。
這里要注意一個(gè)危險(xiǎn)。目標(biāo)覆蓋機(jī)制使導(dǎo)入編譯文件依賴于在導(dǎo)入文件中使用的名稱屬性。如果任何人修改了導(dǎo)入文件的名稱屬性,那么導(dǎo)入編譯文件將被破壞。Ant 開發(fā)社區(qū)目前正在討論在 Ant 的一個(gè)未來的版本中為此提供一個(gè)解決方案。
提示:如果您在編譯文件中發(fā)現(xiàn)了非常常見的結(jié)構(gòu),那么值得嘗試將文件重構(gòu)為一個(gè)(一些)共享文件,并在必要時(shí)使用目標(biāo)覆蓋。這可以使您的編譯系統(tǒng)更加一致,并讓您能夠重用編譯邏輯。 |
在某種意義上,subant 是兩種任務(wù)合二為一,因?yàn)樗私獠僮鞯膬煞N模式。
如果您使用 <subant> 的 genericantfile 屬性,那么它的工作方式和 <antcall> 一樣,調(diào)用包含任務(wù)的同一個(gè)編譯文件中的目標(biāo)。與 <antcall> 不同,<subant> 獲取目錄的列表或集合,并將為每一個(gè)目錄調(diào)用一次目標(biāo),以設(shè)定項(xiàng)目的基本目錄。如果您想在任意數(shù)量的目錄中執(zhí)行完全一樣的操作,那么這非常有用。
第二種模式不使用 genericantfile 屬性,而獲取一個(gè)編譯文件的列表和集合進(jìn)行迭代,以在每一個(gè)編譯文件中調(diào)用目標(biāo)。這種工作方式類似于在一個(gè)循環(huán)中使用 <ant> 任務(wù)。
第二種形式的典型情景是幾個(gè)能夠獨(dú)立編譯的模塊的一個(gè)編譯系統(tǒng),但是該系統(tǒng)需要一個(gè)主編譯文件來一次性編譯所有的模塊。
使用以下資源了解關(guān)于 Ant 的更多信息,并開始編譯和部署 Java 項(xiàng)目。 Ant 業(yè)界趨勢 閱讀關(guān)于 JDeveloper 中的 Ant 集成的更多信息 下載 Oracle JDeveloper 10g 測試驅(qū)動:將 Ant 用于編譯 Ant 入門第 1 部分 Ant 入門第 2 部分 在 Linux 上創(chuàng)建 Java 應(yīng)用程序的命令行方法 閱讀關(guān)于 Ant 的更多信息 相關(guān)文章與下載 Blog: 如何從一個(gè) JDeveloper 項(xiàng)目文件中將屬性動態(tài)檢索到一個(gè) ant 編譯文件中? |
在 Ant 1.6 之前構(gòu)建主編譯文件
在導(dǎo)入部分中討論的例子使用了這樣一個(gè)主編譯文件。
<target name="build-all"> <ant dir="module-A" target="jar"/> <ant dir="module-B" target="jar"/> </target>
在 Ant 1.6 之前的 Ant 中。
使用 Ant 1.6 構(gòu)建主編譯文件
在 Ant 1.6 中使用 <subant>,這可以重寫為
<target name="build-all"> <subant target="jar"> <filelist dir="."> <file name="module-A/build.xml"/> <file name="module-B/build.xml"/> </filelist> </subant> </target>
這看起來并沒有很大的改善,因?yàn)槟匀槐仨殕为?dú)指定每一個(gè)子編譯文件。相反如果您轉(zhuǎn)用 <fileset>,那么情況將有所改觀。
<target name="build-all"> <subant target="jar"> <fileset dir="." includes="module-*/build.xml"/> </subant> </target>
這將自動發(fā)現(xiàn)所有模塊的編譯文件。如果您增加了一個(gè)模塊 C,主編譯文件中的目標(biāo)不需要修改。
但小心。與 <filelist> 或 <path>(也被 <subant> 支持)不同,<fileset> 是無序的。在我們的例子中,模塊 B 依賴于模塊 A,因此我們需要確保首先編譯模塊 A,而使用 <fileset> 沒有辦法這么做。
如果編譯完全彼此獨(dú)立或者它們對于一個(gè)給定的操作彼此獨(dú)立,那么 <fileset> 仍然有用。模塊 B 的文檔目標(biāo)可能完全不依賴于模塊 A,同樣還有從您的 SCM 系統(tǒng)中更新源代碼的目標(biāo)。
如果您想將編譯文件的自動發(fā)現(xiàn)與根據(jù)編譯的相互依賴性對編譯進(jìn)行排序結(jié)合在一起,那么您將必須編寫一個(gè)定制的 Ant 任務(wù)。基本的想法是編寫一個(gè)使用 <fileset> 的任務(wù)(讓我們目前稱之為 <buildlist>),確定依賴關(guān)系并計(jì)算 <subant> 必須使用的順序。然后它創(chuàng)建一個(gè)以正確的順序包含編譯文件的 <path>,然后將對這個(gè)路徑的一個(gè)引用放到項(xiàng)目中。調(diào)用將類似于
<target name="build-all"> <buildlist reference="my-build-path"> <fileset dir="." includes="module-*/build.xml"/> </buildlist> <subant target="jar"> <buildpath refid="my-build-path"/> </subant> </target>
這個(gè)假想的 buildlist 任務(wù)已經(jīng)在 Ant 用戶郵件列表和 bug 跟蹤系統(tǒng)中進(jìn)行了討論。很有可能 Ant 的一個(gè)將來的版本中將包含這樣的一個(gè)任務(wù)。
在 Ant 1.6 中已經(jīng)增加了大量的新特性。這些新功能中的許多功能使得編譯模板易于創(chuàng)建、構(gòu)造和定制。特別是 <import> 和 <target> 進(jìn)行了覆蓋。<import>、<macrodef> 和 <subant> 特性很有可能使得 Ant 編譯可高度重用。<scriptdef>(本文中未討論)對于需要一些腳本但不想用 Java 編寫定制任務(wù)的人而言可能非常有吸引力。