チュートリアル:タスクの作成

このドキュメントでは、タスクを作成するためのステップバイステップチュートリアルを提供します。

目次

ビルド環境のセットアップ

Apache Antはそれ自体をビルドします。私たちもAntを使用しています(そうでなければ、なぜタスクを作成するのでしょうか? :-) そのため、ビルドにはAntを使用する必要があります。

ルートディレクトリとしてディレクトリを選択します。特に断らない限り、すべての作業はここで行われます。このディレクトリをプロジェクトのルートディレクトリとして参照します。このルートディレクトリに、build.xmlという名前のテキストファイルを作成します。Antは何をすればよいでしょうか?

したがって、ビルドファイルには3つのターゲットが含まれています。
<?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]の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のTaskからの派生

わかりました、うまくいきました...しかし通常、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クラスの2つの便利なメソッドです。

replaceProperties()メソッドについては、ネストされたテキストセクションでさらに説明します。

属性

次に、メッセージのテキストを指定したいと思います(<echo/>タスクを書き換えているようです:-))。まず、属性を使用してそれを行います。非常に簡単です。各属性に対してpublic void setAttributename(Type 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をスローするため、これが必須です。(もちろん、messageをデフォルト文字列で初期化できます。)

その後、ビルドファイルを変更する必要があります。

    <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メソッドの引数として、次のデータ型をサポートします。

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;
    }
    ...
}

ただし、ここではプロパティは解決されません!プロパティを解決するには、プロパティ名を引数として受け取り、その値(または設定されていない場合は${propname})を返すProjectのreplaceProperties(String propname)メソッドを使用する必要があります。

したがって、ネストされたノードテキストのプロパティを置き換えるには、addText()メソッドを次のように記述できます。

    public void addText(String text) {
        message = getProject().replaceProperties(text);
    }

ネストされた要素

ネストされた要素を処理する機能を追加する方法はいくつかあります。詳細については、マニュアル[4]を参照してください。3つの記述方法のうち、最初の方法を使用します。そのためには、いくつかのステップがあります。

  1. ネストされた要素に含める必要があるすべての情報を収集するためのクラスを作成します。このクラスは、タスクの属性およびネストされた要素の場合と同じルール(setAttributename()メソッド)で作成されます。
  2. タスクは、このクラスの複数のインスタンスをリストに保持します。
  3. ファクトリメソッドはオブジェクトをインスタンス化し、参照をリストに保存して、Ant Coreに返します。
  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 1.7.0以降のバイナリ配布の一部であり、ant-testutil.jarの形式で提供されます。ソースディストリビューションからターゲット「test-jar」を使用してjarファイルを作成することもできます。

テストを実行してレポートを作成するには、オプションのタスク<junit><junitreport>が必要です。したがって、ビルドファイルに追加します。

<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に戻ります。JUnitの@Ruleアノテーションでアノテーションが付けられたパブリックBuildFileRuleフィールドを持つクラスを作成します。従来の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の2つの重要なブレークポイントです。

タスク属性またはテキストが設定されたときにデバッグする必要がある場合は、最初にカスタムタスクのexecute()メソッドをデバッグすることから始めます。次に、他のメソッドにブレークポイントを設定します。これにより、クラスのバイトコードがJVMによってロードされたことが保証されます。

リソース

このチュートリアルとそのリソースは、BugZilla [5]から入手できます。そこに提供されているZIPには、以下が含まれています。

最後のソースとビルドファイルは、マニュアル内のこちら[6]でも入手できます。

使用したリンク

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