(2006.04月號-147期)_實踐專案管理,使用Maven
在小型專案與一般於企業中建置的J2EE專案中,經常因導入的AP Server平台的不同而統一使用特定的IDE開發環境,例如:IBM的WebSphere使用WSAD、Oracle的OC4J使用JDeveloper、WebLogic使用Workshop等。
當進行大型專案開發,並分割出多數的系統模組都委外進行開發時,每一間委外廠商可能都使用不同的開發工具與IDE進行開發,因此如何管理、建置、佈署專案就是一門學問了。
而以往J2EE專案依IDE開發環境不同有不同的建置與佈署邏輯,而這些建置與佈署方式彼此並不相容,例如在WSAD建置的設定,無法移植至JDeveloper與Workshop使用,反之亦然,這對日後的維護上也將更形困難,我們沒辦法保證,當時開發時的IDE與目前維護時使用的IDE開發工具是相同的。
若欲將各模組委外進行開發,管理跨J2EE平台建置的大型專案,Ant是個很好的選擇,Ant可以撰寫腳本進行所有動作,要求委外廠商進行模組開發使用Ant也不是問題,因為目前業界大多數也是如此進行開發的。
但問題在於其Ant建構人員必須非常熟悉專案建置邏輯,並且善用一些開放原始碼元件協同進行開發、測試與佈署,若只是單純使用Ant基本腳本功能的話,則頂多也只是將原先手動進行複製、刪除、編譯、封裝JAR、WAR、EAR的動作轉為自動化而已,實質上並沒有太大的意義,還不如撰寫批次檔進行自動化來得簡單方便。
遺憾的是,於Ant中協同其他元件進行Ant功能的擴充並沒有想像中的簡單,參考設定 1中可以發現,於Ant中欲使用Cactus進行容器測試,必須進行以下步驟。
步驟1:首先必須於init中設定cactus的擴充標籤runservertests。
步驟2:testwar中進行原始碼的編譯與封裝。
步驟3:prepare.test.tomcat.40中複製出tomcat環境並進行佈署。
步驟4:test.tomcat.40中使用runservertests擴充標籤執行start.tomcat.40啟動tomcat、執行test進行容器測試、最後執行stop.tomcat.40關閉tomcat。
基本上,建構人員並不想建置這麼複雜的設定、不想引入這些與專案原始碼無關並且只供測試用的jar檔、不想了解tomcat伺服容器的結構,更不用說怎麼去啟動與關閉tomcat了。但若專案欲於Ant上使用Cactus,這一切似乎是無可避免的,一旦加入的擴充元件愈多,Ant腳本將愈來愈複雜,目前Ant中只加入了tomcat的設定,試想當專案需要於WebLogic、Resin、JBoss進行測試時,撰寫的Ant的腳本檔將會有多複雜與難以維護。
另外,在設定 1中我們也發現,多個專案中運行Cactus時的設定都是大同小異的,因此可以將其抽出作為template,並以ENTITY方式進行引入,於多個專案中使用。這是個很好的解決方案,解決了設定上的複雜性,卻還是免不了必須在專案中放置相依的jar檔,當建構範圍愈來愈大相依jar檔的管理也更加麻煩。更何況Ant中只包含建置邏輯的腳本,雖然目前在各IDE開發環境中幾乎都已支援Ant,但Ant並無法反向建置出各IDE開發環境的設定。
當使用版本控制程式進行管理時,相依jar檔愈多將使納入版本管理的程式愈顯龐大,但實際上該納入控管的只有原始碼而已,所有jar檔應該抽出並且共用才對。而若將IDE開發環境的設定納入控管則會造成災難,開發人員都會希望自己的開發環境是方便且友善的,為了如此的目的多多少少都會修改其相關設定,當每一位開發人員都有權利對IDE設定進行commit時,我們將發現開發環境的設定將會是一團亂,甚至於造成專案無法正常開啟。
總結上面所述,跨J2EE平台的專案管理工具最好能符合以下條件。
-
由建置設定檔反向產生個別IDE開發環境設定,並且由建置人員將IDE設定檔排除在版本控制之外。
-
將所有相依的jar檔統一放置於貯藏庫(repository)中,於建置專案的同時進行連結與Classpath的引用。
-
將所有經常使用的建置邏輯抽出成為template,並強迫使用統一的建置邏輯,才不會因為不同的建置人員而寫出不同的建置腳本檔,而使日後維護更加艱難。
-
將所有第三方協同專案開發的套件進行封裝,成為一個外掛模組。例如將Cactus封裝成為Plug-in,而之後只需簡易的呼叫Plug-in進行協同作業而無須理會原先該套件使用上有多複雜。
筆者於139期的RunPC中直接使用Maven進行了簡易專案開發的示範,從本期開始將與讀者一同分享,如何使用Maven完成上述的要求,並且更進一步的超越使其成為專案管理與建置工具的最佳實踐。
<target name="init">
<taskdef name="runservertests" classname="...">
[...設定cactus擴充標籤...]
</taskdef>
</target>
<target name="test.tomcat.40"
depends="prepare.test.tomcat.40">
<runservertests
testURL="[cactus測試網址]"
startTarget="start.tomcat.40"
stopTarget="stop.tomcat.40"
testTarget="test"/>
</target>
<target name="prepare.test.tomcat.40"
depends="check.test.tomcat.40,testwar">
[...設定Tomcat環境並將測試War檔進行佈署...]
<copy file="[...]/test.war"
tofile="[...]/webapps/test.war"/>
</target>
<target name="testwar" depends="compile">
<war warfile="..." webxml="...">
[...進行封裝War檔...]
</war>
</target>
<target name="start.tomcat.40">
<java classname="..." fork="yes">
[...設定Classpath並啟動tomcat...]
</java>
</target>
<target name="stop.tomcat.40">
<java classname="..." fork="yes">
[...設定Classpath並關閉tomcat...]
</java>
</target>
<target name="test">
<junit fork="yes">
<classpath>
[...設定cactus所需要的Classpath...]
</classpath>
<formatter type="plain" usefile="false"/>
<test name="[cactus測試單元]"/>
</junit>
</target>
Maven的起源
Ant曾經是筆者認為絕佳的建置工具,畢竟Ant為Another Neat Tool(另一個很棒的工具)的縮寫。經過多次專案的歷練後發現,要建置與維護多個Ant腳本的專案將非常的不易。而Maven的起源最初是由於進行Jakarta Turbine專案,原先專案中每一個check in到CVS的不同的JAR檔都擁有他們自己的build files,在進行維護上相當的困難,因此能夠進行很簡單的建置,並且定義出一個建置多個project的標準、一個對project構成清晰的描述、一個簡單的方式發佈project的資訊與一個能夠通過幾個不同的project去分享不同的JAR檔的方式就成為Maven這個專案的目標。
現在的Maven將可以協助開發人員更容易的進行日常工作的建立與更輕易的理解所有Java-based開發的專案。
因此可以在最短時間內使開發人員導入目前專案的開發。並且提供以下幾個特點。
-
使專案建置的過程更加容易
-
提供一個通用的專案建置系統
-
提供建立高品質的專案開發資訊
-
提供最佳化實踐的專案開發指南
-
允許以通透的方式進行新功能的整合
理念上Maven是一個管理與整合專案開發的建置工具,在使用上基於XML概念的Project Object Model (POM-專案元件模型)的設計,並且實際上Maven的架構與核心是非常微小的,幾乎所有的功能都是由Plugins所提供,因此,在Maven中對於Plug-in的應用非常重要。
Maven1.x環境設定
Maven相容於大多數的OS平台,讀者可參考設定 2與設定 3進行Maven環境的指定並執行maven確認是否正確執行。
首次執行時,Maven預設將[MAVEN_HOME]/plugins下所有jar檔進行解壓縮放置於${user.home}/.maven/cache路徑下,在Windows中是置於C:Documents and Settings[登入帳號] .mavencache,於Linux則是置於/home/[登入帳號] /.maven/cache下,這些所謂的plugins大多是基於Jelly語言所定義的建置腳本,也將是日後我們最常使用的套件。
Maven對於相依jar檔的管理則提供了Remote & Local Repository進行集中的管理,預設Remote Repository指向http://www.ibiblio.org/maven網站而Local Repository置於${user.home}/.maven/ repository下,Maven運行過程中相依的檔案會經由Remote Repository下載至Local Repository存放。
將plugins與local repository置於${user.home}/.maven/cache與${user.home}/.maven/ repository,原意在跨平台環境下都能指向開發人員登入後User Home的資料放置路徑,但通常在實際開發中我們會希望將其路徑指向特定位置。
set MAVEN_HOME=[maven放置路徑]maven
set PATH=%MAVEN_HOME%bin;%PATH%
export MAVEN_HOME= [maven放置路徑] /maven
export PATH=${PATH}:${MAVEN_HOME}/bin
Maven參數設定與優先權
在此我們可以參考圖 1進行參數的修改,該圖說明了Maven中參數優先權的順序,在Ant中當使用property標籤進行設定之後,將無法再次修改其參數值,而Maven與Ant的差異在於Maven依照設定檔放置的位置不同擁有其不同的優先權。
讀者可自行解壓縮[MAVEN_HOME]/lib/maven.jar,當中可以發現名為defaults.properties,該檔為所有預設參數放置的位置。另外在我們建置的專案中皆可包含二個設定檔,分別為project.properties與build.properties,並且build.properties優先權高於project.properties,也就是說當這二個檔案中使用相同參數時build.properties所設定的值會替代project.properties的值。但這二個檔案的用意究竟為何?一般初次接觸Maven的人都會搞混,觀念上來說project.properties所存的參數多為專案所需使用的參數,build.properties所設定的參數則為建置專案時所需的參數。
以另外一個觀點來說,假設於目前專案開發階段,我們將程式碼納入版本控管,並在一部測試伺服主機上進行自動化的每日構建與測試,則我們可以將這部測試機或正式機上所需的建置參數指定給project.properties並將該檔納入版本控管當中。因此在進行Daily Build時可以從project.properties取得參數。
而開發人員於自己的開發環境中進行check out原始碼後,一樣可以從版本控制系統中取得project.properties,但該檔的設定並不一定與目前開發人員的環境相同,因此建置上可能會發生問題,為此開發人員可自行建立build.properties並將設定上的不同處進行覆寫,來完成專案建置與測試。而build.properties我們通常不納入版本控管當中。另外Maven中協同多專案進行開發是非常容易的,所以目前進行開發的專案,有可能是父專案中的子專案,也因此繼承了父專案的相關屬性,在父專案下亦可包含project.properties與build.properties二個設定檔。
在User home的路徑下,也就是C:Documents and Settings[登入帳號],是屬於優先權次高的設定檔,通常在此進行對預設參數的覆寫,以取代原始的預設值,參考設定 4在這裏重新指定了maven.home.local與maven.repo.remote,透過maven.home.local參數的覆寫,相對的也指定了.maven/cache與.maven/ repository的路徑,這一點可以從maven.jar中的defaults.properties的設定maven.repo.local=${maven.home.local}/repository與maven.plugin.unpacked.dir = ${maven.home.local}/cache找到解答。
若為了加速下載速度或是企業內部網路受限的環境下而必須設定代理伺服器可使用設定 4中maven.proxy相關參數進行設定,該設定中亦支援NTLM授權。但在網路管制較嚴格的企業中,代理伺服器的設定可能還是無法使Maven順利進行檔案下載作業,則這時必須求助於網管人員調整其網路權限。
使用Command line方式進行設定的參數將覆蓋之前所有的設定,參考設定 6中我們運行了plugin:download並覆寫參數maven.repo.remote以及其他參數。使用方式同java參數設定-Dproperty=value,擁有最高的優先權。
設定 4 User home下的build.properties
#設定cache與repository放置路徑
maven.home.local=${maven.home}/.maven
#設定remote repository網址
maven.repo.remote=file:E:/maven/maven.remote.repository,
http://www.ibiblio.org/maven,
http://maven-plugins.sourceforge.net
#Maven使用代理伺服器
maven.proxy.host=[代理伺服器網址]
maven.proxy.port=[代理伺服器Port]
maven.proxy.username=[帳號]
maven.proxy.password=[密碼]
maven.proxy.ntlm.username=[帳號]
maven.proxy.ntlm.password=[密碼]
資料結構、groupId與artifactId
Maven提供了許多的Plug-in,讓專案建構變得非常簡單。但Maven對於建置專案也有限制,其限制就是每個專案﹙在Maven中一個專案是指帶有project.xml的一個資料夾﹚只能生成一個成品(artifact)。但這個限制對於複雜的專案架構上並沒有問題,因為Maven支援多專案的建置。一個Maven專案可以由子專案,每個子專案可以建置產生自己的成品,最頂層的專案可以將子專案的成品打包進行封裝生成JAR、WAR、EAR。並且Maven在專案之間還提供了繼承(inheritance)。
Maven使用了groupId與artifactId的觀念,參考資料結構 1,當進行小型專案的建置時,我們只需要類似artifact-id-implement或artifact-id-war簡易的建出JAR或WAR即可,這時groupId與artifactId通常是相同的。但若進行大型專案的建置,將可能將企業邏輯進行抽像化,產生介面、實作與其共用元件,而多個介面、實作、共用元件配合WAR與EAR的建置就可對大型專案進行封裝與佈署,如此的分層架構規劃下,許多Maven建置的artifact都將可以輕易的在各專案的建置下重覆使用,而這時的groupId與artifactId就必須有不同的名稱。例如Spring這個Framework,其groupId為org.springframework,而分別依功能區分了spring-aop、spring-beans、spring-context、spring-dao…等的artifactId。
註:關於project.xml中groupId與artifactId的設定請參考139期的Run PC。
group-id
|-- artifact-id-interface
| |-- src/
| | |-- main/
| | | |-- java/
| | | | `-- ...
| | | `-- resources/
| | | `-- ...
| | `-- test/
| | |-- java/
| | | `-- ...
| | `-- resources/
| | `-- ...
| |-- project.xml
| |-- maven.xml
| |-- project.properties
| `-- build.properties
|
|-- artifact-id-implement
| `-- ...結構同上...
|
|-- artifact-id-war
| |-- src/
| | |-- java/
| | |-- resources/
| | `-- webapp/
| |-- test/
| | |-- java/
| | `-- resources/
| |-- project.xml
| |-- maven.xml
| |-- project.properties
| `-- build.properties
|
|-- project.xml
|-- maven.xml
|-- project.properties
`-- build.properties
建置Maven的組件與其核心架構
Maven的基本概念就是Project,每一個目錄包含有project.xml的目錄都是一個Project,而另一個概念就是Repository。當運行maven命令時,將會從Remote repository中複製於project.xml中dependency標籤所相依的jar檔到Local repository,然後建構專案,封裝成品。參考設定 5與圖 2,Maven依groupId建立一個子目錄,在groupId資料夾中依所需的artifactId進行下載指定type的檔案,這裏所指的type有jar、ejb、plugin…等設定,若未指定則預設為jar。在設定 5的範例中將會建立groupId為org.springframework的資料夾並於預設type為jar的條件下建立jars資料夾並下載指定規則為<artifactid>-<version>.<type>的spring-aop-1.2.6.jar、spring-beans-1.2.6.jar、spring-context-1.2.6.jar三個檔案於該資料夾內。
Repository的角色是相當明顯的,每個專案不再需要各自包含自己所依賴的第三方套件,Repository將會協助開發人員在多個專案間共用所有套件。而另一方面每個專案也可以自己建置成品,依Maven的觀念可使用install將套件安裝到Local repository,使用deploy將套件佈署到Remote repository。使用這種方式可以幫助開發者用一種標準的方式在專案間共用自製的套件。並且相互依賴的進行專案間持續整合(continuous integration)。若透過Cruise Control工具。專案可以持續的建置,最新釋出的成品可以重覆佈署到Repository中。在建置主機上運行持續性整合,將會定時的持續建構、發佈成品到應用伺服器中、進行自動測試並檢驗建置狀態。參考圖 3顯示了在Maven建置過程中project.xml、Repository、goal、plug-in各自的角色。
<dependencies>
...
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>1.2.6</version>
</dependency>
...
</dependencies>
Maven Plug-in
在Maven中幾乎所有功能都是由Plug-in所提供的,在Plug-in中包含了多個goal,而針對每個goal可以因應多個property的設定呈現不同的實作結果。例如Maven Jar Plug-in中原先在執行maven jar:jar這個goal之後,將進行原始碼編譯、測試並依artifactid-version.jar規則封裝成jar檔,而當我們在進行參數設定並加入maven.jar.final.name=xxxx.jar後重新執行,則最後封裝結果的檔案將會是xxxx.jar。
究竟目前在本機上有多少的Plug-in可用,在此我們可以執行maven -g列出所有的Plug-in,亦可以使用maven -g > goal.txt重導至檔案中慢慢查看,若已經知道欲使用的Plug-in卻不知該Plug-in提供多少goal,可使用maven -P [plugin name]則可列出指定的Plug-in中所有的goal,maven -g 與maven -P 列出格式參考資料結構 2。
Maven -g與-P參數可以清楚的知道有多少的goal可使用,但Plug-in中有多少property就沒有參數可以協助列出以供查看了,關於這個部分可以連至Maven官方網站http://maven.apache.org/maven-1.x/reference/plugins參考詳細的Plug-in說明文件。
Maven在預先安裝中就已經包含了大多數的Plug-in了,但若欲使用的Plug-in有bug或欲使用的功能於本機上的Plug-in尚未提供,則可至Maven官方網站中任選以下方式進行下載並安裝,。
-
方式1:手動下載指定版本Plug-in的jar,置於[MAVEN_HOME]/plugins下即可。
-
方式2:參考設定 6執行plugin:download並指定所需的Plug-in進行手動下載與安裝。
-
方式3:參考設定 7執行maven任一goal的同時進行Plug-in進行自動下載與安裝。
註:Maven中的goal與Ant腳本中的target是意義相同的,所指的都是預先定義好的任務,而Maven的goal是由Jelly(基於XML的標記語言)編寫的一系列命令腳本,而使用goal的語法為maven [plugin name]:[goal name]。
資料結構 2 maven -g 與maven -P 輸出格式
__ __
| / |__ _Apache__ ___
| |/| / _` V / -_) ' ~ intelligent projects ~
|_| |___,_|_/___|_||_| v. 1.0.2
Available [Plugins] / Goals
===========================
[plugin name1] 針對plugin name1的描述
goal name1 ..... goal name1使用描述
goal name2 ..... goal name2使用描述
goal name3 ..... goal name3使用描述
...
[plugin name2] 針對plugin name2的描述
...
maven plugin:download
-Dmaven.repo.remote=http://www.ibiblio.org/maven, http://cvs.apache.org/repository/
-DgroupId=maven
-DartifactId=maven-jar-plugin
-Dversion=1.7
設定 7 Maven於POM中設定自動下載Plug-in
<dependencies>
...
<dependency>
<groupId> maven</groupId>
<artifactId> maven-jar-plugin </artifactId>
<version>1.7</version>
<type>plugin</type>
</dependency>
...
</dependencies>
Maven Goal
除了之前提過的project.xml、Repository與Plug-in之外,在圖 3中我們可以看到maven.xml檔的設置,這個檔案主要用途在於延伸、擴展與客製化Goal,並且在這個檔案中我們會使用到preGoal、goal、postGoal三個標籤進行Goal的定義。
參考設定 8中project標籤的default屬性設定為buildWeb,在Maven中的goal和Ant中的target是相同的概念,而且Maven中的default屬性的設定亦是相同於Ant。這個設定意味著在目前專案的目錄下,直接執行maven指令,這時將會執行的default的goal為buildWeb,在這個設定中buildWeb直接使用attainGoal標籤去執行另外一個goal為war Plug-in中的war這個goal。
從這個例子中我們可知,我們可以在goal標籤中撰寫所需的執行的Jelly腳本,當然這個腳本目前也相容於Ant的寫法,並且在goal中還可以使用attainGoal去執行其他Plug-in的goal。例如war:war這個goal依順序也會去執行java:compile、test:test、war:webapp…這幾個goal。
preGoal標籤則代表了在指定的goal執行之前所要執行的建構規則,如之前所提war:war運行過程中會去執行java:compile,而本例中在編譯之前將先行呼叫xdoclet:webdoclet這個goal協助在建置過程中產生web相關的設定檔。因此使用preGoal進行功能的擴展就無須直接修改java:compile這個goal。
同樣的postGoal的意義在於將在goal執行之後所運行的建構規則,在本例中當war:war完成後將產生一個war檔,透過postGoal功能的延伸的撰寫我們就可以進行簡易的tomcat的佈署動作了。
在Maven中無論是goal、preGoal、postGoal都可以嵌入任何Ant的task,。雖然給了Maven極大的靈活性,但是也很容易造成誤用,反而將之前使用Ant進行的設定、建置、佈署動作方式原封不動的搬到Maven來了,也就是說,只是在Maven的結構下依然故我的撰寫Ant的腳本,而造成在Maven下卻完全用不到Plug-in的好處。
建議在一般情況下,當找不到任何適當的的Plug-in時再自行撰寫,畢竟Maven最大的好處在於有無數的Plug-in可以使用,並且每一個Plug-in都是開放原始碼的心血結晶。
設定 8 maven.xml
<?xml version="1.0" encoding="Big5"?>
<project xmlns:j="jelly:core" xmlns:ant="jelly:ant" default="buildWeb">
<preGoal name="java:compile">
<attainGoal name="xdoclet:webdoclet" />
...在指定的goal之前執行建構規則...
</preGoal>
<goal name="buildWeb">
<attainGoal name="war:war" />
...建立一個新的或覆寫一個goal...
</goal>
<postGoal name="war:war">
<copy file="${maven.build.dir}/${pom.artifactId}.war"
todir="${tomcat.home.webapps}"/>
...在指定的goal之後執行建構規則...
</postGoal>
</project>
Maven1.x與Maven2.x的差異
目前上過Maven官方網站的讀者應該已經注意到,Maven2已經Release了,雖然Maven2與Maven1在概念上是非常相近的,但Maven2在架構上是對於Maven1完全的重寫,也因此將完全不相容。Maven1 Plug-in的部分也無法直接在Maven2上使用,而且因為效能考量下原先在Maven1建立Plug-in受到重用的Jelly語言,將不會在Maven2中出現,取而代之的將是回歸使用Java進行Plug-in的開發。
讀者看到這裏是否覺得,即然如此這時專案的開發應該全數移轉到Maven2上才是,也該放棄Maven1了。這也是當筆者看到Maven2的感想,但是實際上運作幾個Project後發現,雖然Maven2提供了不少相對於Maven1增加的新功能與特色,而且目前版本也尚稱穩定,但它最大的問題在於可用的Plug-in還不多,而少數的Plug-in目前還有Bug。
這是理所當然的,想當初Maven1也是經過二、三年的時間才延伸出這些為數可觀又好用的Plug-in,若要Maven2的Plug-in能夠成長到一個地步則還需要一段時間,但讀者無需擔心,目前Maven1建置的資料夾結構移到Maven2還是可以共用的,若日後需移轉到Maven2時,只需加上pom.xml並依Maven2規範設定就可以完成相容於Maven1與Maven2的專案了。
相關資源
1. http://maven.apache.org/ Maven官方網站
2. http://www.theserverside.com/articles/article.tss?l=MavenMagic
Srikanth Shenoy 於 TheServerSide.com發表的MavenMagic文章
3. Maven: A Developer's Notebook
By Vincent Massol, Timothy M. O'Brien
沒有留言:
張貼留言