教程:编写任务

本文档提供了一个编写任务的逐步教程。

内容

设置构建环境

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> 要求目标目录存在;使用不存在的类目录调用 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,并且通过它的 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 中,获取项目引用,提供文档字段,提供对日志记录功能的更轻松访问,并且(非常有用)为你提供在构建文件中使用此任务实例的确切位置。

Oki-doki——让我们使用其中的一些

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

但是这里不会解析属性!为了解析属性,我们必须使用 Project 的 replaceProperties(String propname) 方法,该方法以属性名称作为参数,并返回其值(如果未设置,则返回 ${propname})。

因此,要替换嵌套节点文本中的属性,我们的方法 addText() 可以写成

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

嵌套元素

有几种方法可以插入处理嵌套元素的能力。请参阅 手册 [4] 以了解其他方法。我们使用三种描述方法中的第一种方法。为此,需要执行以下几个步骤

  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 版本的两个重要断点

如果你需要在设置任务属性或文本时进行调试,请先调试到自定义任务的 execute() 方法中。然后在其他方法中设置断点。这将确保类字节码已由 JVM 加载。

资源

本教程及其资源可通过 BugZilla [5] 获取。那里提供的 ZIP 包含

最新的源代码和构建文件也可以在手册中的 这里 [6] 获取。

使用的链接

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