本文档提供了一个编写任务的逐步教程。
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>
要求目标目录存在;使用不存在的类目录调用 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
我们的类与 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 中,获取项目引用,提供文档字段,提供对日志记录功能的更轻松访问,并且(非常有用)为你提供在构建文件中使用此任务实例的确切位置。
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 类中的两个有用方法
String getProperty(String propertyName)
String replaceProperties(String value)
方法 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 方法的参数
int
、long
等java.lang.Integer
、java.lang.Long
等java.lang.String
java.io.File
;请参阅 手册“编写自己的任务” [3])在调用 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] 以了解其他方法。我们使用三种描述方法中的第一种方法。为此,需要执行以下几个步骤
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 自 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 版本的两个重要断点
main()
函数:com.apache.tools.ant.launch.Launcher.main()
com.apache.tools.ant.UnknownElement.execute()
如果你需要在设置任务属性或文本时进行调试,请先调试到自定义任务的 execute()
方法中。然后在其他方法中设置断点。这将确保类字节码已由 JVM 加载。
本教程及其资源可通过 BugZilla [5] 获取。那里提供的 ZIP 包含
最新的源代码和构建文件也可以在手册中的 这里 [6] 获取。
使用的链接