使用 Apache Ant 開發

撰寫自己的工作

撰寫自己的工作非常容易

  1. 建立一個 Java 類別,延伸 org.apache.tools.ant.Task另一個類別,其設計用於延伸。
  2. 針對每個屬性,撰寫一個設定器方法。設定器方法必須是一個 public void 方法,只有一個引數。方法名稱必須以 set 開頭,接著是屬性名稱,名稱的第一個字元為大寫,其餘為小寫*。也就是說,若要支援一個名為 file 的屬性,您會建立一個 setFile 方法。根據引數的類型,Ant 會為您執行一些轉換,請參閱 下方
  3. 如果您的工作應包含其他工作作為巢狀元素(例如 parallel),您的類別必須實作介面 org.apache.tools.ant.TaskContainer。如果您這樣做,您的工作無法支援任何其他巢狀元素。請參閱 下方
  4. 如果工作應支援字元資料(開始和結束標籤之間的巢狀文字),請撰寫一個 public void addText(String) 方法。請注意,Ant 不會擴充傳遞給工作的文字上的屬性。
  5. 針對每個巢狀元素,撰寫一個建立新增addConfigured 方法。建立方法必須是一個 public 方法,不帶引數,並傳回一個 Object 類型。建立方法的名稱必須以 create 開頭,接著是元素名稱。新增(或 addConfigured)方法必須是一個 public void 方法,只有一個引數,為 Object 類型,且具有無引數建構函式。新增(addConfigured)方法的名稱必須以 addaddConfigured)開頭,接著是元素名稱。如需更完整的討論,請參閱 下方
  6. 撰寫一個 public void execute() 方法,不帶引數,會擲回 BuildException。此方法實作工作本身。

* 實際上,第一個字元之後的字母大小寫對 Ant 來說並不重要,但使用全部小寫是一個好習慣。

工作的生命週期

  1. 包含與工作對應標籤的 xml 元素會在剖析時間轉換為 UnknownElement。此 UnknownElement 會放置在目標物件中的清單內,或遞迴放置在另一個 UnknownElement 內。
  2. 執行目標時,會使用 perform() 方法呼叫每個 UnknownElement。這會實例化工作。這表示工作只會在執行時間實例化。
  3. 工作會透過其繼承的 projectlocation 變數取得其專案和在建置檔中的位置的參考。
  4. 如果使用者指定此工作項的 id 屬性,專案會在執行階段註冊對此新建立工作項的參考。
  5. 工作項會透過其繼承的 target 變數取得其所屬目標的參考。
  6. init() 會在執行階段呼叫。
  7. 對應於此工作項的 XML 元素的所有子元素,會在執行階段透過此工作項的 createXXX() 方法建立,或透過其 addXXX() 方法實例化並新增至此工作項。對應於 addConfiguredXXX() 的子元素會在此時建立,但實際的 addConfigured 方法不會呼叫。
  8. 此工作項的所有屬性會在執行階段透過其對應的 setXXX() 方法設定。
  9. 對應於此工作項的 XML 元素內的內容字元資料區段會在執行階段透過其 addText() 方法新增至工作項。
  10. 所有子元素的所有屬性會在執行階段透過其對應的 setXXX() 方法設定。
  11. 如果已為 addConfiguredXXX() 方法建立對應於此工作項的 XML 元素的子元素,這些方法會在此時呼叫。
  12. execute() 會在執行階段呼叫。如果 target1target2 都依賴於 target3,則執行 ant target1 target2 會執行 target3 中的所有工作項兩次。

Ant 會對屬性執行的轉換

在將屬性的值傳遞給對應的設定器方法之前,Ant 會永遠先展開屬性。從 Ant 1.8 開始,可以 延伸 Ant 的屬性處理,讓非字串物件成為包含單一屬性參考的字串評估結果。這些會透過符合類型的設定器方法直接指派。由於啟用此行為需要一些超越基礎知識的介入,因此標記打算允許此使用範例的屬性會是個好主意。

撰寫屬性設定值最常見的方式是使用 java.lang.String 參數。在此情況下,Ant 會將文字值(在屬性擴充後)傳遞給您的工作。但還有更多!如果您的設定值方法的參數是

如果給定屬性存在多個 setter 方法,會發生什麼事?採用 String 參數的方法永遠會敗給更明確的方法。如果還有更多 setter 可供 Ant 選擇,只會呼叫其中一個,但我們不知道會呼叫哪一個,這取決於 Java 虛擬機的實作。

支援巢狀元素

假設你的工作需要支援名稱為 inner 的巢狀元素。首先,你需要一個類別來表示這個巢狀元素。通常你只需要使用 Ant 的類別之一,例如 org.apache.tools.ant.types.FileSet 來支援巢狀 fileset 元素。

巢狀元素或其巢狀子元素的屬性將使用與工作相同的機制處理(也就是屬性的 setter 方法、巢狀文字的 addText(),以及子元素的 create/add/addConfigured 方法)。

現在你有一個類別 NestedElement,假設要使用在你的巢狀 <inner> 元素,你有三個選項

  1. public NestedElement createInner()
  2. public void addInner(NestedElement anInner)
  3. public void addConfiguredInner(NestedElement anInner)

差異是什麼?

選項 1 讓工作建立 NestedElement 的執行個體,類型沒有限制。對於選項 2 和 3,Ant 必須先建立 NestedInner 的執行個體,才能傳遞給工作,這表示 NestedInner 必須有一個 public 無參數建構函式或一個 public 單參數建構函式,採用 Project 類別作為參數。這是選項 1 和 2 之間唯一的差異。

選項 2 和 3 之間的差異在於 Ant 在將物件傳遞給方法之前對物件執行的動作。在呼叫建構函式後,addInner() 會直接收到物件,而 addConfiguredInner() 則是在處理這個新物件的屬性和巢狀子項後才取得物件。

如果你使用多個選項,會發生什麼事?只會呼叫其中一個方法,但我們不知道會呼叫哪一個,這取決於你的 JVM 實作。

巢狀類型

如果你的工作需要巢狀使用已使用 <typedef> 定義的任意類型,你有兩個選項。

  1. public void add(Type 類型)
  2. public void addConfigured(Type 類型)

1 和 2 之間的差異與前一節中 2 和 3 之間的差異相同。

例如,假設某人想要處理 org.apache.tools.ant.taskdefs.condition.Condition 類型的物件,他可能有一個類別

public class MyTask extends Task {
    private List conditions = new ArrayList();
    public void add(Condition c) {
        conditions.add(c);
    }
    public void execute() {
     // iterator over the conditions
    }
}

可以這樣定義和使用這個類別

<taskdef name="mytask" classname="MyTask" classpath="classes"/>
<typedef name="condition.equals"
         classname="org.apache.tools.ant.taskdefs.conditions.Equals"/>
<mytask>
    <condition.equals arg1="${debug}" arg2="true"/>
</mytask>

以下是更複雜的範例

public class Sample {
    public static class MyFileSelector implements FileSelector {
         public void setAttrA(int a) {}
         public void setAttrB(int b) {}
         public void add(Path path) {}
         public boolean isSelected(File basedir, String filename, File file) {
             return true;
         }
     }

    interface MyInterface {
        void setVerbose(boolean val);
    }

    public static class BuildPath extends Path {
        public BuildPath(Project project) {
            super(project);
        }

        public void add(MyInterface inter) {}
        public void setUrl(String url) {}
    }

    public static class XInterface implements MyInterface {
        public void setVerbose(boolean x) {}
        public void setCount(int c) {}
    }
}

此類別定義了許多靜態類別,這些類別實作/延伸 PathMyFileSelectorMyInterface。可以如下定義和使用這些類別

<typedef name="myfileselector" classname="Sample$MyFileSelector"
         classpath="classes" loaderref="classes"/>
<typedef name="buildpath" classname="Sample$BuildPath"
         classpath="classes" loaderref="classes"/>
<typedef name="xinterface" classname="Sample$XInterface"
         classpath="classes" loaderref="classes"/>

<copy todir="copy-classes">
   <fileset dir="classes">
      <myfileselector attra="10" attrB="-10">
         <buildpath path="." url="abc">
            <xinterface count="4"/>
         </buildpath>
      </myfileselector>
   </fileset>
</copy>

TaskContainer

TaskContainer 包含一個單一方法 addTask,基本上與巢狀元素的 add 方法 相同。當您的任務的 execute 方法被呼叫時,任務執行個體將會被設定 (其屬性和巢狀元素已處理),但在此之前不會被設定。

當我們 execute 會被呼叫時,我們說謊了 ;-). 事實上,Ant 會在 org.apache.tools.ant.Task 中呼叫 perform 方法,而該方法會反過來呼叫 execute。此方法可確保會觸發 建置事件。如果您執行巢狀在您的任務中的任務執行個體,您也應該對這些執行個體呼叫 perform,而不是 execute

範例

讓我們撰寫自己的任務,在 System.out 串流上列印訊息。此任務有一個屬性,稱為 message

package com.mydomain;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

public class MyVeryOwnTask extends Task {
    private String msg;

    // The method executing the task
    public void execute() throws BuildException {
        System.out.println(msg);
    }

    // The setter for the "message" attribute
    public void setMessage(String msg) {
        this.msg = msg;
    }
}

它真的很簡單 ;-)

將您的任務新增到系統也很簡單

  1. 在啟動 Ant 時,請確保實作您的任務的類別在類別路徑中。
  2. <taskdef> 元素新增到您的專案。這實際上會將您的任務新增到系統中。
  3. 在建置檔的其餘部分使用您的任務。

範例

<?xml version="1.0"?>

<project name="OwnTaskExample" default="main" basedir=".">
  <taskdef name="mytask" classname="com.mydomain.MyVeryOwnTask"/>

  <target name="main">
    <mytask message="Hello World! MyVeryOwnTask works!"/>
  </target>
</project>

範例 2

若要直接從建立它的建置檔使用任務,請將 <taskdef> 宣告放在目標中,在編譯之後。使用 <taskdef>classpath 屬性指向程式碼剛剛編譯的位置。

<?xml version="1.0"?>

<project name="OwnTaskExample2" default="main" basedir=".">

  <target name="build" >
    <mkdir dir="build"/>
    <javac srcdir="source" destdir="build"/>
  </target>

  <target name="declare" depends="build">
    <taskdef name="mytask"
        classname="com.mydomain.MyVeryOwnTask"
        classpath="build"/>
  </target>

  <target name="main" depends="declare">
    <mytask message="Hello World! MyVeryOwnTask works!"/>
  </target>
</project>

新增任務的另一種方式 (更永久的方式) 是將任務名稱和實作類別名稱新增到 org.apache.tools.ant.taskdefs 套件中的 default.properties 檔案。然後您可以使用它,就像它是內建任務一樣。


建置事件

Ant 能在執行建置專案所需任務時產生建置事件。可以將監聽器附加到 Ant 以接收這些事件。例如,此功能可用於將 Ant 連線到 GUI 或將 Ant 整合到 IDE 中。

若要使用建置事件,您需要建立一個 ant Project 物件。然後,您可以呼叫 addBuildListener 方法將監聽器新增到專案中。您的監聽器必須實作 org.apache.tools.antBuildListener 介面。監聽器將收到下列事件的 BuildEvents

如果建置檔案透過 <ant><subant> 呼叫另一個建置檔案,或使用 <antcall>,您將建立一個新的 Ant「專案」,它會傳送自己的目標和任務層級事件,但絕不會傳送建置開始/完成事件。自 Ant 1.6.2 起BuildListener 介面有一個名為 SubBuildListener 的延伸,它會收到兩個新事件,如下所示

如果您有興趣了解這些事件,您只需實作新介面,而不是 BuildListener(當然,還要註冊監聽器)。

如果您希望從命令列附加監聽器,可以使用 -listener 選項。例如

ant -listener org.apache.tools.ant.XmlLogger

將使用監聽器執行 Ant,該監聽器會產生建置進度的 XML 表示。此監聽器包含在 Ant 中,預設監聽器也是如此,它會將記錄產生到標準輸出。

注意:監聽器不得直接存取 System.outSystem.err,因為 Ant 的核心會將這些串流上的輸出重新導向到建置事件系統。存取這些串流可能會導致 Ant 中出現無限迴圈。根據 Ant 的版本,這將導致建置終止或 JVM 用完堆疊空間。記錄器也不能直接存取 System.outSystem.err。它必須使用已設定的串流。

注意:BuildListener 的所有方法(「建置已開始」和「建置已完成」事件除外)都可能同時在多個執行緒上發生,例如當 Ant 正在執行 <parallel> 任務時。

範例

撰寫轉接器到您最愛的記錄程式庫非常容易。只要實作 BuildListener 介面,實例化您的記錄器,然後將訊息委派給該實例即可。

在開始建置時,將您的轉接器類別和記錄程式庫提供給建置類別路徑,並透過 -listener 選項啟用您的記錄器,如上所述。

public class MyLogAdapter implements BuildListener {

    private MyLogger getLogger() {
        final MyLogger log = MyLoggerFactory.getLogger(Project.class.getName());
        return log;
    }

    @Override
    public void buildStarted(final BuildEvent event) {
        final MyLogger log = getLogger();
        log.info("Build started.");
    }

    @Override
    public void buildFinished(final BuildEvent event) {
        final MyLogger logger = getLogger();
        MyLogLevelEnum loglevel = ... // map event.getPriority() to enum via Project.MSG_* constants
        boolean allOK = event.getException() == null;
        String logmessage = ... // create log message using data of the event and the message invoked
        logger.log(loglevel, logmessage);
    }

    // implement all methods in that way
}

原始碼整合

透過 Java 延伸 Ant 的另一種方式是變更現有的任務,這絕對受到鼓勵。對現有來源和新任務的變更都可以納入 Ant 程式碼庫中,這對所有使用者都有利,並能分散維護負擔。

請參閱 Apache 網站上的 參與 頁面,以取得有關如何取得最新來源和如何提交變更以重新併入來源樹的詳細資訊。

Ant 也有提供一些 任務指南,為開發和測試任務的人員提供一些建議。即使您打算將任務保留給自己,您仍然應該閱讀這份指南,因為它應該具有參考價值。