ANT的基本概念:Java的Makefile
當一個代碼項目大了以后,每次重新編譯,打包,測試等都會變得非常復雜而且重復,因此c語言中有make腳本來幫助這些工作的批量完成。在Java中應用是平臺無關性的,當然不會用平臺相關的make腳本來完成這些批處理任務了,ANT本身就是這樣一個流程腳本引擎,用于自動化調用程序完成項目的編譯,打包,測試等。除了基于JAVA是平臺無關的外,腳本的格式是基于XML的,比make腳本來說還要好維護一些。
每個ant腳本(缺省叫build.xml)中設置了一系列任務(target):比如對于一個一般的項目可能需要有以下任務。
任務1:usage 打印本腳本的幫助信息(缺?。?BR> 任務2:
任務3:
javadoc <-- build <-- init 生成JAVADOC
| 任務4:
jar <-- build <-- init 生成JAR
| 任務5:
all <-- jar + javadoc <-- build <-- init
| 完成以上所有任務:
而多個任務之間往往又包含了一定了依賴關系:比如把整個應用打包任務(jar)的這個依賴于編譯任務(build),而編譯任務又依賴于整個環境初始化任務(init)等。
注:我看到很多項目的ant腳本中的命名基本上都是一致的,比如:編譯一般叫build或者compile;打包一般叫jar或war;生成文檔一般命名為javadoc或javadocs;執行全部任務all。在每個任務的中,ANT會根據配置調用一些外部應用并配以相應參數執行。雖然ANT可調用的外部應用種類非常豐富,但其實最常用的就2,3個:比如javac javadoc jar等。 ANT的安裝
解包后在系統可執行路徑中加入指向ant的bin的路徑就可以了,比如可以在GNU/Linux上把以下配置加入/etc/profile中:
export ANT_HOME=/home/ant
export JAVA_HOME=/usr/java/j2sdk1.4.1
export PATH=$PATH:$JAVA_HOME/bin:$ANT_HOME/bin
|
這樣執行ant后,如果不指定配置文件ant會缺省找build.xml這個配置文件,并根據配置文件執行任務,缺省的任務設置可以指向最常用的任務,比如:build,或指向打印幫助信息:usage,告訴用戶有那些腳本選項可以使用。
ANT的使用
最好的學習過程就是看懂那些open source項目中的build.xml腳本,然后根據自己的需要簡化成一個更簡單的,ANT和APACHE上很多非常工程派的項目:簡單易用,而且適應性非常強,因為這些項目的建立往往來源于開發人員日常最直接的需求。
以下是的一個WebLucene應用的例子:修改自JDOM的build.xml:
<project default="usage" basedir=".">
<!-- ====================== -->
<!-- Initialization target -->
<!-- ====================== -->
<target name="init">
<tstamp/>
<property file="${basedir}
/build.properties" />
<property name="Name"
value="ProjectFullName"/>
<property name="name"
value="project_name"/>
<property name="version"
value="0.2"/>
<property name="year"
value="2003"/>
<echo message="-----------
${Name} ${version} [${year}]
------------"/>
<property name="debug"
value="off"/>
<property name="optimize"
value="on"/>
<property name="deprecation"
value="on"/>
<property name="src.dir"
value="./src/WEB-INF/src"/>
<property name="lib.dir"
value="./src/WEB-INF/lib"/>
<property name="packages" value="com.chedong.*,org.apache.lucene.*"/>
<property name="build.src"
value="./src/WEB-INF/build"/>
<property name="build.dest"
value="./src/WEB-INF/classes"/>
<property name="build.javadocs"
value="./src/doc"/>
<path id="classpath">
<pathelement path="${jsdk_jar}"/>
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
</path>
<filter token="year"
value="${year}"/>
<filter token="version"
value="${version}"/>
<filter token="date"
value="${TODAY}"/>
<filter token="log"
value="true"/>
<filter token="verbose"
value="true"/>
</target>
<!-- ================== -->
<!-- Help on usage -->
<!-- ================== -->
<target name="usage" depends="init">
<echo message="${Name} Build file"/>
<echo message="----------"/>
<echo message=""/>
<echo message=" available targets are:"/>
<echo message=""/>
<echo message=" jar
--> generates the ${name}.jar file"/>
<echo message=" build
--> compiles the source code"/>
<echo message=" javadoc
--> generates the API documentation"/>
<echo message=" clean
--> cleans up the directory"/>
<echo message=""/>
<echo message=" Please rename
build.properties.default to build.properties"/>
<echo message=" and edit
build.properties to specify JSDK 2.3 classpath."/>
<echo message=""/>
<echo message=" See the comments
inside the build.xml file for more details."/>
<echo message="--------------"/>
<echo message=""/>
<echo message=""/>
</target>
<!-- ===================== -->
<!-- Prepares the source code -->
<!-- ===================== -->
<target name="prepare-src"
depends="init">
<!-- create directories -->
<mkdir dir="${build.src}"/>
<mkdir dir="${build.dest}"/>
<!-- copy src files -->
<copy todir="${build.src}">
<fileset dir="${src.dir}"/>
</copy>
</target>
<!-- ===================== -->
<!-- Compiles the source directory -->
<!-- ===================== -->
<target name="build" depends="prepare-src">
<javac srcdir="${build.src}"
destdir="${build.dest}"
debug="${debug}"
optimize="${optimize}">
<classpath refid="classpath"/>
</javac>
</target>
<!-- ==================== -->
<!-- Creates the class package -->
<!-- ==================== -->
<target name="jar" depends="build">
<jar jarfile="${lib.dir}/${name}.jar"
basedir="${build.dest}"
includes="**"/>
</target>
<!-- ==================== -->
<!-- Creates the API documentation -->
<!-- ==================== -->
<target name="javadoc" depends="build">
<mkdir dir="${build.javadocs}"/>
<javadoc packagenames="${packages}"
sourcepath="${build.src}"
destdir="${build.javadocs}"
author="true"
version="true"
use="true"
splitindex="true"
windowtitle="${Name} API"
doctitle="${Name}">
<classpath refid="classpath"/>
</javadoc>
</target>
<!-- ======================= -->
<!-- Clean targets -->
<!-- ======================= -->
<target name="clean" depends="init">
<delete dir="${build.src}"/>
<delete dir="${build.dest}/org"/>
<delete dir="${build.dest}/com"/>
<delete>
<fileset dir="${build.dest}"
includes="**/*.class"/>
</delete>
</target>
</project>
<!-- End of file -->
|
缺省任務:usage 打印幫助文檔,告訴有那些任務選項:可用的有build, jar, javadoc和clean。
初始化環境變量:init
所有任務都基于一些基本環境變量的設置初始化完成,是后續其他任務的基礎,在環境初始化過程中,有2點比較可以方便設置:
1、除了使用卻缺省的property設置了JAVA源路徑和輸出路徑外,引用了一個外部的build.properties文件中的設置:
<property file="${basedir}
/build.properties" />
|
這樣大部分簡單配置用戶只要會看懂build.properties就可以了,畢竟XML比起key value的屬性文件還是要可讀性差一些。用build.properties也可以方便其他用戶從編譯的細節中解放出來。
2、CLASSPATH設置:使用了其中的:
<path id="classpath">
<pathelement path="${jsdk_jar}"/>
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
</path>
|
則相當于設置了:
CLASSPATH=/path/to/resin/lib/jsdk23.jar;
/path/to/project/lib/*.jar;
|
文件復制:prepare-src
創建臨時SRC存放目錄和輸出目錄。
<!-- ============ -->
<!-- Prepares the source code -->
<!-- ========== -->
<target name="prepare-src"
depends="init">
<!-- create directories -->
<mkdir dir="${build.src}"/>
<mkdir dir="${build.dest}"/>
<!-- copy src files -->
<copy todir="${build.src}">
<fileset dir="${src.dir}"/>
</copy>
</target>
|
編譯任務:build
編譯時的CLASSPATH環境通過一下方式找到引用一個path對象
<classpath refid="classpath"/>
|
打包任務:jar
對應用打包生成項目所寫名的.jar文件
<!-- ============== -->
<!-- Creates the class package -->
<!-- ============ -->
<target name="jar" depends="build">
<jar jarfile="${lib.dir}/${name}.jar"
basedir="${build.dest}"
includes="**"/>
</target>
|
生成JAVADOC文檔任務: javadoc
<!-- ================ -->
<!-- Creates the API documentation -->
<!-- ============== -->
<target name="javadoc" depends="build">
<mkdir dir="${build.javadocs}"/>
<javadoc packagenames="${packages}"
sourcepath="${build.src}"
destdir="${build.javadocs}"
author="true"
version="true"
use="true"
splitindex="true"
windowtitle="${Name} API"
doctitle="${Name}">
<classpath refid="classpath"/>
</javadoc>
</target>
|
清空臨時編譯文件:clean
<!-- ============= -->
<!-- Clean targets -->
<!-- =========== -->
<target name="clean" depends="init">
<delete dir="${build.src}"/>
<delete dir="${build.dest}/org"/>
<delete dir="${build.dest}/com"/>
<delete>
<fileset dir="${build.dest}"
includes="**/*.class"/>
</delete>
</target>
|
8. 定義恰當的任務參數關系
假設dist任務從屬于jar任務,那么哪個任務從屬于compile任務,哪個任務從屬于prepare任務呢?Ant構建文件最終定義了任務的從屬關系圖,它必須被仔細地定義和維護。 應該定期檢查任務的從屬關系以保證構建工作得到正確執行。大的構建文件隨著時間推移趨向于增加更多的任務,所以到最后由于不必要的從屬關系導致構建工作非常困難。比如,你可能發現在程序員只是需要編譯一些沒有使用EJB的GUI代碼時,重新生成EJB代碼。 以“優化”的名義忽略任務的從屬關系是另一種常見的錯誤。這種錯誤迫使程序員為了得到恰當的結果必須記住并按照特定的順序調用一串任務。更好的做法是:提供描述清晰的公共任務,這些任務包含正確的任務從屬關系;另外提供一套“專家”任務讓你能夠手工執行個別的構建步驟,這些任務不提供完整的構建過程,但是讓那些專家在快速而惱人的編碼期間跳過某些步驟。
9.使用配置屬性
任何需要配置或可能發生變化的信息都應作為Ant屬性定義下來。對于在構建文件中多次出現的值也同樣處理。屬性既可以在構建文件頭部定義,也可以為了更好的靈活性而在單獨的屬性文件中定義。以下是在構建文件中定義屬性的樣式:
<project name="sample" default="compile" basedir=".">
<property name="dir.build" value="build"/>
<property name="dir.src" value="src"/>
<property name="jdom.home" value="../java-tools/jdom-b8"/>
<property name="jdom.jar" value="jdom.jar"/>
<property name="jdom.jar.withpath"
value="${jdom.home}/build/${jdom.jar}"/>
etc...
</project>
|
或者你可以使用屬性文件:
<project name="sample" default="compile" basedir=".">
<property file="sample.properties"/>
etc...
</project>
|
在屬性文件 sample.properties中:
dir.build=build
dir.src=src
jdom.home=../java-tools/jdom-b8
jdom.jar=jdom.jar
jdom.jar.withpath=${jdom.home}/build/${jdom.jar}
| 用一個獨立的文件定義屬性是有好處的,它可以清晰地定義構建中的可配置部分。另外,在開發者工作在不同操作系統的情況下,你可以在不同的平臺上提供該文件的不同版本。
10. 保持構建過程獨立 為了最大限度的擴展性,不要應用外部路徑和庫文件。最重要的是不要依賴于程序員的CLASSPATH設置。取而代之的是,在構建文件中使用相對路徑并定義自己的路徑。如果你引用了絕對路徑如C:\java\tools,其他開發者未必使用與你相同的目錄結構,所以就無法使用你的構建文件 如果你部署開發源碼項目,應該提供包括所有需要的JAR文件的發行版本,當然是在遵守許可協議的基礎上。對于內部項目,相關的JAR文件都應在版本控制系統的管理中,并撿出到大家都知道的位置。 當你不得不應用外部路徑時,應將路徑定義為屬性。使程序員能夠涌適合他們自己的機器的參數重載這些屬性。你也可以使用以下語法引用環境變量:
<property environment="env"/>
<property name="dir.jboss" value="${env.JBOSS_HOME}"/>
|
11. 使用版本控制系統 構建文件是一個重要的文件,應該象代碼一樣進行版本控制。當你標記你的代碼時,也應用同樣的標簽標記構建文件。這樣當你需要回溯構建舊版本的軟件時,能夠使用相對應的舊版本構建文件。 除構建文件之外,你還應在版本控制中維護第三方JAR文件。同樣,這使你能夠重新構建舊版本的軟件。這也能夠更容易保證所有開發者擁有一致的JAR文件,因為他們都是同構建文件一起從版本控制系統中撿出的。 通常應避免在版本控制系統中存放構建輸出品。倘若你的源代碼很好地得到了版本控制,那么通過構建過程你能夠重新生成任何版本的產品。
12. 把Ant作為“最小公分母”
假設你的開發團隊使用IDE,為什么要為程序員通過點擊圖標就能夠構建整個應用而煩惱呢? IDE的問題在團隊中是一個關于一致性和重現性的問題。幾乎所有的IDE設計初衷都是為了提高程序員的個人生產率,而不是開發團隊的持續構建。典型的IDE要求每個程序員定義自己的項目文件。程序員可能擁有不同的目錄結構,可能使用不同版本的庫文件,還可能工作在不同的平臺上。這將導致出現這種情況:在A那里運行良好的代碼,到B那里就無法運行。 不管你的開發團隊使用何種IDE,一定要建立所有程序員都能夠使用的Ant構建文件。要建立一個程序員在將新代碼提交版本控制系統前必須執行Ant構建文件的規則。這將確保代碼是經過同一個Ant構建文件構建的。當出現問題時,要使用項目標準的Ant構建文件,而不是通過某個IDE來執行一個干凈的構建。
程序員可以自由選擇任何他們習慣使用的IDE。但是Ant應作為公共基線以保證永遠是可構建的。
13. 使用 zipfileset屬性
人們經常使用Ant產生WAR、JAR、ZIP和 EAR文件。這些文件通常都要求有一個特定的內部目錄結構,但其往往與你的源代碼和編譯環境的目錄結構不匹配。 一個最常用的方法是寫一個Ant任務按照期望的目錄結構把一大堆文件拷貝到臨時目錄中,然后生成壓縮文件。這不是最有效的方法。使用zipfileset屬性是更好的解決方案。它讓你從任何位置選擇文件,然后把它們按照不同目錄結構放進壓縮文件中。以下是一個例子:
<ear earfile="${dir.dist.server}/payroll.ear"
appxml="${dir.resources}/application.xml">
<fileset dir="${dir.build}" includes="commonServer.jar"/>
<fileset dir="${dir.build}">
<include name="payroll-ejb.jar"/>
</fileset>
<zipfileset dir="${dir.build}" prefix="lib">
<include name="hr.jar"/>
<include name="billing.jar"/>
</zipfileset>
<fileset dir=".">
<include name="lib/jdom.jar"/>
<include name="lib/log4j.jar"/>
<include name="lib/ojdbc14.jar"/>
</fileset>
<zipfileset dir="${dir.generated.src}" prefix="META-INF">
<include name="jboss-app.xml"/>
</zipfileset>
</ear>
| 在這個例子中,所有JAR文件都放在EAR文件包的lib目錄中。hr.jar和billing.jar是從構建目錄拷貝過來的。因此我們使用zipfileset屬性把它們移動到EAR文件包內部的lib目錄。prefix屬性指定了其在EAR文件中的目標路徑。
14. 運行 Clean 構建任務的測試
假設你的構建文件中有clean和compile的任務,執行以下的測試。第一步,執行ant clean;第二步,執行ant compile;第三步,再執行ant compile。第三步應該不作任何事情。如果文件再次被編譯,說明你的構建文件有問題。 構建文件應該只在與輸出文件相關聯的輸入文件發生變化時,才應該執行任務。一個構建文件在不必執行諸如編譯、拷貝或其他工作任務的時候執行這些等任務是低效的。當項目規模增長時,即使是小的低效工作也會成為大的問題。
15. Avoid Platform-Specific Ant Wrappers
不管什么原因,有人喜歡用簡單的、名稱叫做compile之類的批文件或腳本裝載他們的產品。當你去看腳本的內容,你會發現以下內容: ant compile 其實開發人員熟悉Ant,并且完全能夠自己鍵入ant compile。請不要僅僅為了調用Ant而使用特定平臺的腳本。這只會使其他人在首次使用你的腳本時,增加學習和理解的煩擾。除此之外,你不可能提供適用于每個操作系統的腳本,這是真正煩擾其他用戶的地方。
總結 太多的公司依靠手工方法和程序來編譯代碼和生成軟件發布版本。那些不使用Ant或類似工具定義構建過程的開發團隊,花費了令人驚異的時間來捕捉代碼編譯過程中出現的問題,這些在某些開發者那里編譯成功的代碼,到另一些開發者那里卻失敗了。
生成并維護構建腳本不是一項迷人的工作,但卻是一項必需的工作。一個好的Ant構建文件將使你集中到更喜歡的工作——寫代碼中!
參考
· Ant
· AntGraph: Ant依賴性的可視化工具
· Ant: The Definitive Guide, O'Reilly
· Java Extreme Programming Cookbook, O'Reilly |
|