讀者定位為具有 Java 和 Ant 使用經驗的開發人員。
讀者可以學習到如何使用 Ant 解決一些多用戶開發環境中,根據不同的目標環境編譯成不同部署包的問題。
工作場景
現在有一個 web 項目,是多人進行開發的,通過 CVS 來管理。另外該項目還有一些測試人員,他們測試部署在測試服務器上的應用程序,發現 bug 後通過 bug 管理系統通知開發人員,在開發人員修復 bug 並經過本地測試後,由專門的人負責檢出(check out)代碼,編譯打包後部署到測試服務器上。
該項目的成員小A就是負責檢出代碼、編譯打包,並部署到服務器上的人。除了這個任務之外,他還是該項目的編程人員。在項目進入測試階段後,小A在得到組中別的成員修復了 bug 並且檢入(check in)了代碼的消息後(也有可能是小A自己檢入了代碼),小A首先更新本地的代碼,先在本地做測試,確認修復了 bug 後打成 WAR 包部署到測試服務器上,並通知測試人員已經修復了 bug,讓測試人員繼續進行測試。
小A的煩惱
在該項目中,有一些為測試方便開發而寫的代碼和頁面,比如跳過用戶認證,但是在部署到測試機環境的時候,需要刪除這些代碼和頁面;另外作為一個具有靈活性和擴展性的應用程序,又有一些配置文件,配置文件中的值會根據環境的改變而變動。例如,在項目中使用了 Log4j 記錄日志,需要給 Log4j 指定日志文件的保存路徑,本地程序員開發的時候用的是 Windows 系統,給 Log4j 指定的也是 Windows 的文件系統,在測試階段的時候,需要部署到 Linux 系統中,那麼日志的保存路徑也需要做相應的改動。部署到測試服務器上的時候,除了 Log4j 需要改之外,還有很多別的配置項目也需要變動,並且分散在各個 package 中。小A的煩惱也隨之而來,每次他在做完本地測之後,就根據測試機的需要逐個找配置文件,更改相應的值,並刪除那些為測試方便寫的代碼和頁面,每天可能根據需要做好幾次這樣的事情,最煩的是他在快做完對測試機環境更改的時候,某開發人員突然通知小A說:“我又改了一點代碼,剛檢入,你再重打一個包吧。”,小A又不得不從頭開始做新一輪的檢出代碼、本地測試、更改配置文件、刪除不需要的文件、打包部署的工作。另外小A在測試階段的後期被通知要求除了每次生成一個測試環境的 WAR 包外還必須生成一個在產品環境下的 WAR 包,他做的事情就更多了。
從上面的場景可以看出,小A的工作效率低而且容易出錯,甚至有可能導致整個項目的工作效率低下。其實可以通過 Ant 來幫助小A快速而且有效地完成這個工作。在 Ant 中,根據目標環境的需要,可以把所有要更改的配置文件的項目集中寫到某個配置文件中。這樣根據不同的目標環境得到不同的配置文件,Ant 在編譯包時根據不同的目標環境切換不同的配置文件即可。比如小A現在碰到的有 3 中環境:開發環境、測試環境以及產品環境,根據這三種環境可以生成三個不同的配置文件:develop_deploy.property、test_deploy. property和product_deploy. property,當小A想生成不同的包時只需在這三個配置文件之間切換就可以了。
在正式開始編寫腳本之前,我們需要下載安裝相應的軟件。
Eclipse:為了使 Ant 的開發更加簡單,我們選擇了 Eclipse3.1 作為開發環境。當然你可以使用任何你喜歡的文本編輯工具來開發你的 Ant。Eclipse 的最新版本可以在 http://eclipse.org/ 上下載。
Ant:Ant 是基於 Java 的編譯工具,它就像 C/C++ 中的 make,但是沒有 make 那樣難用。Ant 的最新版本可以在 http://ant.apache.org/bindownload.cgi 上下載。如果你用 Eclipse 來開發 Ant,則不用去單獨下載 Ant,因為在 Eclipse 中已經集成了 Ant。
CVS 客戶端(cvs.exe):CVS 可以在 http://www.nongnu.org/cvs/ 上下載
用 Eclipse3.1 來創建 Ant 腳本
如果你使用 Eclipse 來編寫 Ant,建議使用 Eclipse 3.1 以後的版本。除了以前 Ant 編輯器提供的語法高亮,提示語法錯誤等功能外,Eclipse3.1 版本增加了許多新的功能。比如:腳本代碼的折疊;快速定位某屬性或者目標(target)段的定義;在同一 builder 文件中重構屬性名和目標名(快捷方式Alt + Shift +R);調試 Ant 腳本等。
下面我們就來看看 Eclipse 3.1 中對 Ant 的支持
打開“File”-“New”-“Project”-“Simple”-“Project”,點擊“Next”,輸入工程名“Ant”,然後點擊直到“Finish”
在新建的 Ant 工程中,新建 Test.xml,並且拷貝下面的腳本。該段腳本的內容就不做介紹了,我們主要看 Eclipse 提供了哪些功能。注意這時候打開的並不是 Ant 編輯器,將內容拷貝進去之後,關掉打開的“Test.xml”,然後再重新打開它,這樣 Eclipse 就會用 Ant 編輯器打開它,並且也語法高亮了。
<?xml version="1.0" encoding="UTF-8"?>
<project name="Test" default="init" basedir=".">
<property name="test" value="test"/>
<target name="init">
<echo>${test}</echo>
</target>
</project>
自動提示和代碼折疊功能。如果是 Ant 內置的命令或者前面已經定義的變量,編輯器會自動提示;編輯器右邊的加/減號可以代碼折疊。如下所示:
快速定位目標(target)或者定義變量處。在上圖中,將鼠標移至 default=”init” 中的 init 上之後,按下 ctrl 鍵,鼠標變成手狀,單擊就可以定位到定義該目標的地方
快速重構目標名或者屬性名。選中目標/屬性名,按下 Alt + Shift + R,然後鍵入你要修改後的值,所有引用到的地方都會隨之改動。如下圖所示,選中 init 後,按下快捷鍵,改成 initial:
調試 Ant 腳本。在標簽“<target name=..”的左邊設置一斷點,然後在編輯器中右擊,出現的菜單中選“Debug As”-“Ant Build”,出現後的窗口與調試 Java 程序的界面差不多。
這是調試窗口,這裡可以選擇單步跟進、跳出等:
下面是運行時變量窗口。可以看到 test 變量的值是“test”:
運行結果窗口:
由此可見,如果使用好 Eclipse Ant 編輯器所提供強大的功能的話能大大提高寫 Ant 腳本的效率。
Ant 中使用 property(屬性)文件
剛開始寫 Ant 的初學者可能會把所有的信息都放在 build.xml 中,下面就是這樣的一個例子。
<project name="testBuild" default="compile" basedir=".">
<target name="init">
<mkdir dir="c:/temp/dest/testProj" />
</target>
<target name="compile" depends="init">
<javac srcdir="c:/temp/src/testProj" destdir="c:/temp/dest/testProj "/>
<echo>Build into c:/temp/dest/testProj, successfully.</echo>
</target>
</project>
在上面的例子中,所有的路徑信息都是寫在 build.xml 中。但是 Ant 腳本可能在不同的機器或者不同的系統上運行,也有可能一些值需要根據環境的不同而變化,在 Ant 中可以把所有這些可能變化的地方都放到一個配置文件中,然後在 Ant 腳本中引用這個配置文件就可以了,針對上面的例子,如下所示:
<project name="testBuild" default="compile" basedir=".">
<property file="build.properties"/>
<target name="init">
<mkdir dir="${dest.dir}" />
</target>
<target name="compile" depends="init">
<javac srcdir="${src.dir}" destdir="${dest.dir}"/>
<echo>Build into ${dest.dir}, successfully.</echo>
</target>
</project>
build.properties的內容:
dest.dir=c:/temp/dest/testProj
src.dir=c:/temp/src/testProj
如果想在 Ant 腳本中引用值的話,只需用$符號開頭,在一對"{}"中寫入要引用的鍵值。如上例中,需要引用編譯的目標路徑用"${dest.dir}"。
使用 Ant 任務從 CVS 中檢出(check out)源代碼,並編譯打包
Ant 中提供了 cvs 任務(Task)可以從 CVS 服務器中檢出資源(注意:在使用 Ant 的 cvs 任務之前,請先將 cvs.exe 下載到你的機器,並且將它添加到你本地的 PATH 環境變量中,然後重新啟動 Eclipse。否則在執行下面腳本的時候就會得到 error=2 的錯誤)。cvs 的可選用屬性很多,在這裡介紹經常使用到的幾個屬性。從 CVS 中檢出資源一般需要指定:
CVS 所在的服務器地址:目標 CVS 服務器地址
用戶名:登錄該 CVS 服務器你指定的用戶名
密碼:登錄該 CVS 服務器需要的密碼
庫路徑(Repository Path):服務器中的庫路徑
模塊名:當前需要檢出的模塊名,一般都是以工程的名字作為模塊名
標簽名:需要從 CVS 中檢出哪個標簽
在介紹使用 Ant 的 cvs 之前,先說一下本地的目錄結構。在 C 盤的 temp 目錄下,分別有四個目錄,如下所示:
build 目錄:放編譯後的類以及資源文件
dist 目錄:放生成的 jar 文件或者 war 文件
lib 目錄:放在編譯過程中需要用到的 jar 文件
src 目錄:放從 cvs 中檢出的源文件(包括 JSP 等)
在 Ant 中這樣寫就可以從中檢出資源:
<cvs cvsRoot=":pserver:username:[email protected]:/home/testPath"
package="TestProj" dest=" c:/temp/src/testProj " failonerror="true" />
這段腳本片斷的意思就是從叫"cvs.server"的服務器中,用用戶名是 username、密碼為 pwd 的用戶檢出在庫路徑是 /home/testPath 下的 TestProj 模塊(項目),檢出後的資源放入本地目錄 c:/temp/src/testProj 中。在上面這段腳本中,可以看到有很多值可能會根據不同的環境或者用戶隨之改變的,比如用戶名和密碼等;而且從腳本的重復可利用性來說,需要把有些值抽出來放到配置文件中,如服務器的地址和庫路徑等。因此把這些可能需要更改的地方放到 property 文件中,效果會更好。改完後的完整 Ant 腳本如下所示:
<?xml version="1.0"?>
<project name="testWeb" default="checkout" basedir=".">
<target name="checkout">
<property file="TestWeb.properties" />
<cvs cvsRoot="${cvs.root}" package="${cvs.projectName}"
tag="${cvs.tag}" dest="${src.dir}" failonerror="true" />
</target>
</project>
對應的 TestWeb.properties 文件內容如下所示:
base.dir=c:/temp/
src.dir=${base.dir}/src
cvs.server=cvs.server
cvs.user=username
cvs.pw=pwd
cvs.repositoryPath=/home/testPath
cvs.projectName=TestProj
cvs.root=:pserver:${cvs.user}:${cvs.pw}@${cvs.server}:${cvs.repositoryPath}
cvs.tag=
在檢出了資源後,需要對其進行編譯打包。為了使 Ant 腳本更加具有可讀性和靈活性,我們需要對上面的 Ant 腳本進行一些改動。首先將 Ant 腳本中進行分段,如下所示:
<?xml version="1.0"?>
<project name="testWeb" default="all" basedir=".">
<target name="all" depends="init,clean,checkout,build">
<!--腳本的入口點-->
</target>
<target name="init">
<!--做初始化屬性文件和設置classpath等設置初始條件-->
</target>
<target name="clean">
<!--刪除上一次留下的沒用的目錄和文件-->
</target>
<target name="checkout">
<!--從CVS中檢出資源-->
</target>
<target name="build">
<!--編譯源文件並打包到指定的目錄-->
</target>
</project>
上面的腳本中,總共分成了5個目標(target),腳本的入口點是"all",all 按順序調用 init,clean,checkout,build。其中:
init 是用來做初始化屬性文件和設置 classpath 等設置初始條件的事情
clean 用來刪除上一次留下的沒用的目錄和文件
checkout 已經介紹過了,是用來從 CVS 中檢出資源
build 用來編譯源文件並打 WAR 包到指定的目錄
詳細的 Ant 腳本可以參見隨本文所附的 TestWeb.xml 和 TestWeb.properties。
編譯過程與產生不同目標環境的腳本分開執行
在前面介紹的 Ant 腳本中,根據從 CVS 服務器中檢出的資源打成了一個默認的 war 包,並沒有考慮根據不同的目標環境來生成不同的包,從下一節開始介紹如何根據不同的環境來生成不同的部署包。
還有一個問題是:為什麼需要把從 CVS 中檢出資源、編譯的過程跟根據目標環境打包的過程分開?
這是因為本身 CVS 檢出資源是需要花一定的時間,如果資源比較多這個過程就會花費挺長時間;另外,在多人開發的情況下必須保證在生成不同的部署包的時候是用的是同一套代碼生成的,否則會出現各個服務器上運行的版本不一致,如果檢出資源、編譯的過程跟生成包的腳本一起執行的話就會出現這個問題(比如小A在測試服務器測試通過的時候之後,再生成一個在產品環境下的部署包,如果分兩次從 CVS 服務器中檢出資源的話,在此期間可能會有開發人員往 CVS 服務器中檢入代碼,導致生成的版本不一致),因此,必須將這兩個過程分開執行。現在我們開始建立另外一個 Ant 腳本文件,叫 deploy.xml,專門用來生成包;另外與 deploy.xml 相對應的還有一個 deploy.properties 文件。在 deploy.xml 中會引用 deploy.properties 文件。另外根據在前面的場景中碰到的環境,創建三個不同的屬性文件, develop_deploy.property、test_deploy. property 和 product_deploy. Property,在打包的時候,根據不同的目標環境,將相應屬性文件中的內容拷貝至 deploy.properties 文件中(或者也可以直接在 deploy.xml 中直接切換不同的屬性文件),然後在 Eclipse 中直接執行 deploy.xml;如果在命令行中,可以用下面的命令來執行:
ant –f deploy.xml
解開 WAR 包
我們首先得建立一個目錄(這裡是 unpack)用來存放解壓後的文件。Ant 中提供了 unzip 命令用來解壓 ear/war/jar 包。除了這個目錄外,根據不同的目標環境,在運行目錄下建立一個與目標環境相對應的目錄,重新打好的 war 包就放在這個目錄下,比如針對場景中的情況,如果需要創建一個產品環境下的部署包,我們可以建立一個 TestWebProduct 目錄,目錄名寫在配置文件中(${pack.base.dir})。
<target name="init">
<echo>init in deploy</echo>
<property file="deploy.properties" />
<delete dir="${unpack.base.dir}" failonerror="false" />
<delete dir="${pack.base.dir}" failonerror="false" />
<mkdir dir="${unpack.base.dir}" />
<mkdir dir="${pack.base.dir}" />
</target>
<target name="unpack">
<echo>unpack the ${war.name}</echo>
<copy file="${dest.dir}/${war.name}" todir="${unpack.base.dir}" />
<unzip src="${unpack.base.dir}/${war.name}" dest="${unpack.base.dir}/${projectName}" />
<delete file="${unpack.base.dir}/${war.name}" />
</target>
在 init 段中首先刪除掉上次解壓的目錄和目標打包目錄,然後重新建立目錄;在 unpack 中,首先將編譯好的默認 war 包拷貝至 unpack 定義的目錄,解壓之後把 unpack 下的 war 包刪除。下面是這時候對應的屬性文件。
projectName=MTSWeb
war.name=MTSWeb.war
#根目錄
base.dir=c:/temp
#默認的war包所在的目錄
dest.dir=${base.dir}/dist
#解壓後的目錄
unpack.base.dir=${base.dir}/unpack
#目標環境相對應的目錄
pack.base.dir=${base.dir}/TestWebProduct
利用 Ant 提供的 filter 任務替換屬性值
現在根據不同環境的需要,對某些配置文件的值做一些替換。在 Ant 中,提供了 filter 任務,使得替換值很方便。當然也可以使用下面介紹的正則表達式來替換屬性值。filter 主要用來在同一行內容中的替換,而正則表達式一下子可以替換多行內容。filter 的使用例子:
<filter token=" log4j.logger" value="INFO"/>
<copy todir="${dest.dir}" filtering="true">
<fileset dir="${src.dir}"/>
</copy>
這段腳本的意思就是在 src.dir 目錄下的所有文件中,如果有預先定義好的"@log4j.logger@"占位符的話,在拷貝到 dest.dir 目錄後,所有的占位符都被替換成了"INFO"。
你也可以將所有被替換的值放到某個屬性文件中,filter 任務將屬性文件中的每一個條目讀出來並且設置成一個 Filter。如下所示:
<filter filtersfile="deploy_env.properties"/>
<copy todir="${dest.dir}" filtering="true">
<fileset dir="${src.dir}"/>
</copy>
上面的腳本表示所有在 deploy_env 中出現的條目將被作為一個 filter,在拷貝到 dest.dir 目錄後,所有 src.dir 目錄中存在的占位符將被替換成 deploy_env 中的值。具體的例子可以參見隨本文附帶的 deploy.xml, deploy_env.properties 和 Test.properties。
其中 deploy.xml 是 ant 腳本,deploy_env.properties 中包含所有要替換的值,在 Test.properties 中是包含有占位符的資源文件。
利用正則表達式替換屬性值
Ant 中支持多種正則表達式,在運行 Ant 的時候用哪種正則表達式可以通過設置 ant.regexp.regexpimpl 的值來切換,Ant 支持的的正則表達式有:
java.util.regex package of JDK 1.4
jakarta-regexp
installation dependencies
正則表達式的例子:
<replaceregexp byline="true">
<regexp pattern="正則表達式"/>
<substitution expression="將要替換的值"/>
<fileset dir="${unpack.war.dir}/WEB-INF" includes="web.xml"/>
</replaceregexp>
byline 屬性用來確認被替換的時候是一次替換一行還是多行;pattern 屬性用來指明正則表達式;substitution expression 中是替換的值,替換的值都定義在相對應的配置文件中;fileset 屬性中的 dir 用來指定被替換文件所在的目錄,includes 用來指定要替換哪個文件。需要注意的是,如果在正則表達式或者替換的值中出現"<"的話,需要用轉義符"<"。
在 Eclipse3.1 中已經內置了對正則表達式的支持;但是如果你在命令行中運行需要正則表達式支持的腳本的話,則需要自己將正則表達式的包下載下來加到 classpath 中。在隨文章的 deploy.xml 中提供了一個簡單的替換屬性文件的值的例子。正則表達式的例子可以在本文所帶的 deploy.xml 中找到。
Ant 使用條件表達式,根據屬性值刪除不需要的文件
在生成部署包的時候,還有可能會根據目標環境的不同,刪除一些不同的文件。比如在產品環境中那些作為測試需要的代碼往往需要刪除,但是測試環境中並不需要。因此就需要條件表達式來做判斷。如下所示:
<target name="checkTestEnv">
<condition property="isTestEnv">
<istrue value="${deploy.isTestEnv}" />
</condition>
<antcall target="trueCondition" />
<antcall target="falseCondition" />
</target>
<target name="trueCondition" if="isTestEnv">
<echo message="true condition in ${projectName}" />
</target>
<target name="falseCondition" unless="isTestEnv">
<echo message="false condition in ${projectName}" />
</target>
在上面的例子中,先讀出 ${deploy.isTestEnv} 的值(在配置文件 deloy.properties 中),根據讀出的值對屬性 isTestEnv 設值,如果 ${deploy.isTestEnv} 的值是 true,isTestEnv 的值也是 true;否則是 false。然後分別調用目標段 trueCondition 和 falseCondition。在這裡,表達式值的判斷是用"istrue",在 Ant 中還提供了很多別的表達式,比如 not/and/or,equals 等等,具體關於條件表達式的信息可以參考:http://ant.apache.org/manual/CoreTasks/condition.html ,該頁也可以在隨下載下來的文檔中找到。
在段 trueCondition 中,判斷 isTestEnv,如果是真的話就運行,否則不運行;在段 falseCondition 中,也判斷 isTestEnv,如果是假就運行,否則不運行,在段中可以根據情況做相應的事情。條件判斷式的例子可以在本文的 deploy.xml 中找到。
重新打包,並拷貝到不同的目錄
在上面的替換過程完成後,調用 war 將 unpack 目錄下的內容重新打包。
<target name="repack">
<war destfile="${pack.base.dir}/${projectName}.war"
basedir="${unpack.base.dir}/${projectName}"
webxml="${unpack.base.dir}/${projectName}/WEB-INF/web.xml"
manifest="${unpack.base.dir}/${projectName}/META-INF/MANIFEST.MF">
</war>
</target>
詳細的例子可以參見隨本文的附件 deploy.xml 和 deploy.properties。
結論
通過本文可以看出編寫好有效的 Ant 腳本能很好的解決在編譯部署包的時候出現的問題,將那些變化的內容放到配置文件中,在部署的時候切換不同的配置文件就可以實現生成不同的部署包。文中也介紹了如何使用 Eclipse 來提高你編寫/調試 Ant 腳本的效率。
本文配套源碼