教學:撰寫任務

本文件提供撰寫任務的分步教學。

內容

設定建置環境

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>
此建置檔案經常使用相同的值(srcclassesMyTask.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

與 TaskAdapter 整合

我們的類別與 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'.

衍生自 Ant 的工作

好的,這有效...但通常您會延伸 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 類別中的兩個有用方法

方法 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 方法的引數

在呼叫設定方法之前,所有屬性都會解析。因此,如果屬性 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] 以了解其他方式。我們使用所述三種方式中的第一種方式。這有幾個步驟

  1. 我們建立一個類別來收集巢狀元素應該包含的所有資訊。這個類別是由與工作相同的屬性和巢狀元素規則建立的(setAttributename() 方法)。
  2. 工作會在清單中保留這個類別的多個執行個體。
  3. 工廠方法會實例化一個物件,將參考儲存在清單中,並將其傳回 Ant 核心。
  4. 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 的兩個重要中斷點

如果您需要在設定任務屬性或文字時除錯,請從自訂任務的 execute() 方法開始除錯。然後在其他方法中設定中斷點。這將確保 JVM 已載入類別位元組碼。

資源

此教學課程及其資源可透過 BugZilla [5] 取得。提供的 ZIP 檔包含

最新的來源和建置檔也可以在手冊中 在此 [6] 取得。

使用的連結

  1. https://ant.dev.org.tw/manual/properties.html#built-in-props
  2. https://ant.dev.org.tw/manual/Tasks/taskdef.html
  3. https://ant.dev.org.tw/manual/develop.html#set-magic
  4. https://ant.dev.org.tw/manual/develop.html#nested-elements
  5. https://issues.apache.org/bugzilla/show_bug.cgi?id=22570
  6. tutorial-writing-tasks-src.zip