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