本文件提供撰寫任務的分步教學。
Apache Ant 自行建置,我們也使用 Ant(如果不是這樣,我們為什麼要撰寫任務? :-) 因此我們應該使用 Ant 來建置。
我們選擇一個目錄作為根目錄。如果我沒有說不同,所有事情都將在此執行。我將此目錄作為專案的「根目錄」來參照。在此根目錄中,我們建立一個名為 build.xml 的文字檔案。Ant 應該為我們做什麼?
<?xml version="1.0" encoding="UTF-8"?> <project name="MyTask" basedir="." default="jar"> <target name="clean" description="Delete all generated files"> <delete dir="classes"/> <delete file="MyTasks.jar"/> </target> <target name="compile" description="Compiles the Task"> <javac srcdir="src" destdir="classes"/> </target> <target name="jar" description="JARs the Task"> <jar destfile="MyTask.jar" basedir="classes"/> </target> </project>此建置檔案經常使用相同的值(src、classes、MyTask.jar),因此我們應該使用
<property>
來重新撰寫它。其次,有一些障礙:<javac>
要求目的地目錄存在;如果 classes 目錄不存在,呼叫 clean將會失敗;
jar要求先執行一些步驟。因此,重構後的程式碼為
<?xml version="1.0" encoding="UTF-8"?> <project name="MyTask" basedir="." default="jar"> <property name="src.dir" value="src"/> <property name="classes.dir" value="classes"/> <target name="clean" description="Delete all generated files"> <delete dir="${classes.dir}" failonerror="false"/> <delete file="${ant.project.name}.jar"/> </target> <target name="compile" description="Compiles the Task"> <mkdir dir="${classes.dir}"/> <javac srcdir="${src.dir}" destdir="${classes.dir}"/> </target> <target name="jar" description="JARs the Task" depends="compile"> <jar destfile="${ant.project.name}.jar" basedir="${classes.dir}"/> </target> </project>
ant.project.name
是 Ant 的 內建屬性 [1] 之一。
現在我們撰寫最簡單的任務—HelloWorld 任務(還有什麼?)。在 src 目錄中建立一個文字檔案 HelloWorld.java,內容為
public class HelloWorld { public void execute() { System.out.println("Hello World"); } }
我們可以使用 ant 編譯並建立 jar(預設目標是 jar
,並透過其 depends 屬性在之前執行 compile
)。
但在建立 jar 之後,我們想要使用我們的任務。因此,我們需要一個新的目標 use
。在我們可以使用新的任務之前,我們必須使用 <taskdef>
[2] 宣告它。為了簡化流程,我們變更 default 屬性
<?xml version="1.0" encoding="UTF-8"?> <project name="MyTask" basedir="." default="use"> ... <target name="use" description="Use the Task" depends="jar"> <taskdef name="helloworld" classname="HelloWorld" classpath="${ant.project.name}.jar"/> <helloworld/> </target> </project>
重要的是 classpath 屬性。Ant 會在其 /lib 目錄中搜尋任務,而我們的任務不在其中。因此,我們必須提供正確的位置。
現在我們可以輸入 ant,所有內容都應該運作...
Buildfile: build.xml compile: [mkdir] Created dir: C:\tmp\anttests\MyFirstTask\classes [javac] Compiling 1 source file to C:\tmp\anttests\MyFirstTask\classes jar: [jar] Building jar: C:\tmp\anttests\MyFirstTask\MyTask.jar use: [helloworld] Hello World BUILD SUCCESSFUL Total time: 3 seconds
我們的類別與 Ant 無關。它不延伸任何超類別,也不實作任何介面。Ant 如何知道要整合?透過命名慣例:我們的類別提供具有簽章 public void execute()
的方法。此類別由 Ant 的 org.apache.tools.ant.TaskAdapter
包裝,它是一個工作,並使用反射來設定專案的參考並呼叫 execute()
方法。
設定專案的參考?這可能會很有趣。Project 類別提供一些不錯的功能:存取 Ant 的記錄設施、取得和設定屬性,以及更多功能。因此,我們嘗試使用該類別
import org.apache.tools.ant.Project; public class HelloWorld { private Project project; public void setProject(Project proj) { project = proj; } public void execute() { String message = project.getProperty("ant.project.name"); project.log("Here is project '" + message + "'.", Project.MSG_INFO); } }
並使用 ant 執行將顯示預期結果
use: Here is project 'MyTask'.
好的,這有效...但通常您會延伸 org.apache.tools.ant.Task
。該類別整合在 Ant 中,取得專案參考,提供文件欄位,提供更輕鬆存取記錄設施的方式,並(非常有用)提供您在建置檔中使用此工作實例的確切位置。
好的,讓我們使用其中一些
import org.apache.tools.ant.Task; public class HelloWorld extends Task { public void execute() { // use of the reference to Project-instance String message = getProject().getProperty("ant.project.name"); // Task's log method log("Here is project '" + message + "'."); // where this task is used? log("I am used in: " + getLocation() ); } }
執行時會提供我們
use: [helloworld] Here is project 'MyTask'. [helloworld] I am used in: C:\tmp\anttests\MyFirstTask\build.xml:23:
您的自訂工作之父專案可透過方法 getProject()
存取。不過,請勿從自訂工作建構函式呼叫此方法,因為傳回值會為 null。稍後,當設定節點屬性或文字,或呼叫方法 execute()
時,Project 物件就會可用。
以下是 Project 類別中的兩個有用方法
String getProperty(String propertyName)
String replaceProperties(String value)
方法 replaceProperties()
在 嵌套文字 章節中進一步討論。
現在我們要指定訊息的文字(看來我們正在改寫 <echo/>
工作 :-)。首先,我們將使用屬性來執行此操作。這非常容易,針對每個屬性提供 public void set屬性名稱(類型 newValue)
方法,而 Ant 會透過反射來執行其餘工作。
import org.apache.tools.ant.Task; import org.apache.tools.ant.BuildException; public class HelloWorld extends Task { String message; public void setMessage(String msg) { message = msg; } public void execute() { if (message == null) { throw new BuildException("No message set."); } log(message); } }
喔,execute()
中那是什麼?擲出 BuildException
?是的,這是向 Ant 顯示遺漏重要項目且完整的建置應該失敗的慣常方式。在那裡提供的字串會寫成建置失敗訊息。這裡有必要,因為 log()
方法無法處理 null
值作為參數,並會擲出 NullPointerException
。(當然,您可以使用預設字串初始化 訊息。)
之後,我們必須修改我們的建置檔
<target name="use" description="Use the Task" depends="jar"> <taskdef name="helloworld" classname="HelloWorld" classpath="${ant.project.name}.jar"/> <helloworld message="Hello World"/> </target>
這樣就完成了。
使用屬性的一些背景知識:Ant 支援下列任何資料類型作為 set 方法的引數
int
、long
、...java.lang.Integer
、java.lang.Long
、...java.lang.String
java.io.File
;請參閱 手冊「撰寫自己的工作」[3])在呼叫設定方法之前,所有屬性都會解析。因此,如果屬性 msg
有設定值,<helloworld message="${msg}"/>
就不會將訊息字串設定為 ${msg}
。
您可能曾經用類似 <echo>Hello World</echo>
的方式使用 <echo>
工作。為此,您必須提供 public void addText(String text)
方法。
... public class HelloWorld extends Task { private String message; ... public void addText(String text) { message = text; } ... }
但這裡的屬性不會解析!要解析屬性,我們必須使用專案的 replaceProperties(String propname)
方法,它將屬性名稱作為引數,並傳回其值(如果未設定,則傳回 ${propname}
)。
因此,若要取代巢狀節點文字中的屬性,我們的 addText()
方法可以寫成
public void addText(String text) { message = getProject().replaceProperties(text); }
有數種方式可以插入處理巢狀元素的能力。請參閱 手冊 [4] 以了解其他方式。我們使用所述三種方式中的第一種方式。這有幾個步驟
setAttributename()
方法)。execute()
方法會反覆執行清單並評估其值。
import java.util.ArrayList; import java.util.List; ... public void execute() { if (message != null) log(message); for (Message msg : messages) { // 4 log(msg.getMsg()); } } List<Message> messages = new ArrayList<>(); // 2 public Message createMessage() { // 3 Message msg = new Message(); messages.add(msg); return msg; } public class Message { // 1 public Message() {} String msg; public void setMsg(String msg) { this.msg = msg; } public String getMsg() { return msg; } } ...
然後,我們可以使用新的巢狀元素。但 XML 名稱在哪裡定義?XML 名稱 → 類別名稱的對應關係在工廠方法中定義:public classname createXML-name()
。因此,我們在建置檔中寫入
<helloworld> <message msg="Nested Element 1"/> <message msg="Nested Element 2"/> </helloworld>
請注意,如果您選擇使用方法 2 或 3,則表示巢狀元素的類別必須宣告為 static
現在,為了回顧,以下是經過小幅重構的建置檔
<?xml version="1.0" encoding="UTF-8"?> <project name="MyTask" basedir="." default="use"> <property name="src.dir" value="src"/> <property name="classes.dir" value="classes"/> <target name="clean" description="Delete all generated files"> <delete dir="${classes.dir}" failonerror="false"/> <delete file="${ant.project.name}.jar"/> </target> <target name="compile" description="Compiles the Task"> <mkdir dir="${classes.dir}"/> <javac srcdir="${src.dir}" destdir="${classes.dir}"/> </target> <target name="jar" description="JARs the Task" depends="compile"> <jar destfile="${ant.project.name}.jar" basedir="${classes.dir}"/> </target> <target name="use.init" description="Taskdef the HelloWorld-Task" depends="jar"> <taskdef name="helloworld" classname="HelloWorld" classpath="${ant.project.name}.jar"/> </target> <target name="use.without" description="Use without any" depends="use.init"> <helloworld/> </target> <target name="use.message" description="Use with attribute 'message'" depends="use.init"> <helloworld message="attribute-text"/> </target> <target name="use.fail" description="Use with attribute 'fail'" depends="use.init"> <helloworld fail="true"/> </target> <target name="use.nestedText" description="Use with nested text" depends="use.init"> <helloworld>nested-text</helloworld> </target> <target name="use.nestedElement" description="Use with nested 'message'" depends="use.init"> <helloworld> <message msg="Nested Element 1"/> <message msg="Nested Element 2"/> </helloworld> </target> <target name="use" description="Try all (w/out use.fail)" depends="use.without,use.message,use.nestedText,use.nestedElement"/> </project>
以及工作的程式碼
import org.apache.tools.ant.Task; import org.apache.tools.ant.BuildException; import java.util.ArrayList; import java.util.List; /** * The task of the tutorial. * Print a message or let the build fail. * @since 2003-08-19 */ public class HelloWorld extends Task { /** The message to print. As attribute. */ String message; public void setMessage(String msg) { message = msg; } /** Should the build fail? Defaults to false. As attribute. */ boolean fail = false; public void setFail(boolean b) { fail = b; } /** Support for nested text. */ public void addText(String text) { message = text; } /** Do the work. */ public void execute() { // handle attribute 'fail' if (fail) throw new BuildException("Fail requested."); // handle attribute 'message' and nested text if (message != null) log(message); // handle nested elements for (Message msg : messages) { log(msg.getMsg()); } } /** Store nested 'message's. */ List<Message> messages = new ArrayList<>(); /** Factory method for creating nested 'message's. */ public Message createMessage() { Message msg = new Message(); messages.add(msg); return msg; } /** A nested 'message'. */ public class Message { // Bean constructor public Message() {} /** Message to print. */ String msg; public void setMsg(String msg) { this.msg = msg; } public String getMsg() { return msg; } } }
而且它有效
C:\tmp\anttests\MyFirstTask>ant Buildfile: build.xml compile: [mkdir] Created dir: C:\tmp\anttests\MyFirstTask\classes [javac] Compiling 1 source file to C:\tmp\anttests\MyFirstTask\classes jar: [jar] Building jar: C:\tmp\anttests\MyFirstTask\MyTask.jar use.init: use.without: use.message: [helloworld] attribute-text use.nestedText: [helloworld] nested-text use.nestedElement: [helloworld] [helloworld] [helloworld] [helloworld] [helloworld] Nested Element 1 [helloworld] Nested Element 2 use: BUILD SUCCESSFUL Total time: 3 seconds C:\tmp\anttests\MyFirstTask>ant use.fail Buildfile: build.xml compile: jar: use.init: use.fail: BUILD FAILED C:\tmp\anttests\MyFirstTask\build.xml:36: Fail requested. Total time: 1 second C:\tmp\anttests\MyFirstTask>
下一步:測試 ...
我們已經撰寫了一個測試:建置檔中的 use.*
目標。但很難自動測試它。通常(以及在 Ant 中),JUnit 會用於此目的。為了測試工作,Ant 提供了一個 JUnit 規則 org.apache.tools.ant.BuildFileRule
。這個類別提供了一些用於測試工作的有用方法:初始化 Ant、載入建置檔、執行目標、擷取偵錯和執行記錄 ...
在 Ant 中,測試案例通常與任務同名,並加上前置的 Test
,因此我們將建立一個 HelloWorldTest.java 檔案。由於我們的專案很小,因此可以將此檔案放入 src 目錄中(Ant 本身的測試類別在 /src/testcases/... 中)。由於我們已經撰寫了「手動測試」的測試,因此也可以用於自動測試。所有測試支援類別都是 Ant 二進位發行版的一部分,自 Ant 1.7.0 起,以 ant-testutil.jar 的形式提供。您也可以使用「test-jar」目標從原始碼發行版建立 jar 檔案。
若要執行測試並建立報告,我們需要選用的任務 <junit>
和 <junitreport>
。因此,我們將加入 buildfile
<project name="MyTask" basedir="." default="test"> ... <property name="ant.test.lib" value="ant-testutil.jar"/> <property name="report.dir" value="report"/> <property name="junit.out.dir.xml" value="${report.dir}/junit/xml"/> <property name="junit.out.dir.html" value="${report.dir}/junit/html"/> <path id="classpath.run"> <path path="${java.class.path}"/> <path location="${ant.project.name}.jar"/> </path> <path id="classpath.test"> <path refid="classpath.run"/> <path location="${ant.test.lib}"/> </path> <target name="clean" description="Delete all generated files"> <delete failonerror="false" includeEmptyDirs="true"> <fileset dir="." includes="${ant.project.name}.jar"/> <fileset dir="${classes.dir}"/> <fileset dir="${report.dir}"/> </delete> </target> <target name="compile" description="Compiles Vector the Task"> <mkdir dir="${classes.dir}"/> <javac srcdir="${src.dir}" destdir="${classes.dir}" classpath="${ant.test.lib}"/> </target> ... <target name="junit" description="Runs the unit tests" depends="jar"> <delete dir="${junit.out.dir.xml}"/> <mkdir dir="${junit.out.dir.xml}"/> <junit printsummary="yes" haltonfailure="no"> <classpath refid="classpath.test"/> <formatter type="xml"/> <batchtest fork="yes" todir="${junit.out.dir.xml}"> <fileset dir="${src.dir}" includes="**/*Test.java"/> </batchtest> </junit> </target> <target name="junitreport" description="Create a report for the rest result"> <mkdir dir="${junit.out.dir.html}"/> <junitreport todir="${junit.out.dir.html}"> <fileset dir="${junit.out.dir.xml}"> <include name="*.xml"/> </fileset> <report format="frames" todir="${junit.out.dir.html}"/> </junitreport> </target> <target name="test" depends="junit,junitreport" description="Runs unit tests and creates a report"/> ... </project>
回到 src/HelloWorldTest.java。我們建立一個類別,其中有一個公共 BuildFileRule
欄位,並加上 JUnit 的 @Rule
註解。根據慣例的 JUnit4 測試,此類別不應有建構函式,也不應有預設的無引數建構函式,設定方法應加上 @Before
註解,終止方法加上 @After
註解,任何測試方法加上 @Test
註解。
import org.apache.tools.ant.BuildFileRule; import org.junit.Assert; import org.junit.Test; import org.junit.Before; import org.junit.Rule; import org.apache.tools.ant.AntAssert; import org.apache.tools.ant.BuildException; public class HelloWorldTest { @Rule public final BuildFileRule buildRule = new BuildFileRule(); @Before public void setUp() { // initialize Ant buildRule.configureProject("build.xml"); } @Test public void testWithout() { buildRule.executeTarget("use.without"); assertEquals("Message was logged but should not.", buildRule.getLog(), ""); } public void testMessage() { // execute target 'use.nestedText' and expect a message // 'attribute-text' in the log buildRule.executeTarget("use.message"); Assert.assertEquals("attribute-text", buildRule.getLog()); } @Test public void testFail() { // execute target 'use.fail' and expect a BuildException // with text 'Fail requested.' try { buildRule.executeTarget("use.fail"); fail("BuildException should have been thrown as task was set to fail"); } catch (BuildException ex) { Assert.assertEquals("fail requested", ex.getMessage()); } } @Test public void testNestedText() { buildRule.executeTarget("use.nestedText"); Assert.assertEquals("nested-text", buildRule.getLog()); } @Test public void testNestedElement() { buildRule.executeTarget("use.nestedElement"); AntAssert.assertContains("Nested Element 1", buildRule.getLog()); AntAssert.assertContains("Nested Element 2", buildRule.getLog()); } }
啟動 ant 時,我們會在 STDOUT 中收到簡短訊息和一份漂亮的 HTML 報告。
C:\tmp\anttests\MyFirstTask>ant Buildfile: build.xml compile: [mkdir] Created dir: C:\tmp\anttests\MyFirstTask\classes [javac] Compiling 2 source files to C:\tmp\anttests\MyFirstTask\classes jar: [jar] Building jar: C:\tmp\anttests\MyFirstTask\MyTask.jar junit: [mkdir] Created dir: C:\tmp\anttests\MyFirstTask\report\junit\xml [junit] Running HelloWorldTest [junit] Tests run: 5, Failures: 0, Errors: 0, Time elapsed: 2,334 sec junitreport: [mkdir] Created dir: C:\tmp\anttests\MyFirstTask\report\junit\html [junitreport] Using Xalan version: Xalan Java 2.4.1 [junitreport] Transform time: 661ms test: BUILD SUCCESSFUL Total time: 7 seconds C:\tmp\anttests\MyFirstTask>
嘗試使用旗標 -verbose 執行 Ant。如需更多資訊,請嘗試旗標 -debug。
對於更深入的問題,您可能需要在 Java 除錯程式中執行自訂任務程式碼。首先,取得 Ant 的原始碼,並使用除錯資訊建立它。
由於 Ant 是個大型專案,因此設定正確的中斷點可能會有點棘手。以下是版本 1.8 的兩個重要中斷點
main()
函式:com.apache.tools.ant.launch.Launcher.main()
com.apache.tools.ant.UnknownElement.execute()
如果您需要在設定任務屬性或文字時除錯,請從自訂任務的 execute()
方法開始除錯。然後在其他方法中設定中斷點。這將確保 JVM 已載入類別位元組碼。
此教學課程及其資源可透過 BugZilla [5] 取得。提供的 ZIP 檔包含
最新的來源和建置檔也可以在手冊中 在此 [6] 取得。
使用的連結