编写 Ant 任务全攻略
1. 数据类型引用
可重复使用的数据类型是 Ant 的一大优势。作为嵌套元素使用的数据类型,无需额外编码就能隐式支持重用。但如果任务需要接受先前定义的数据类型的 id 作为属性,可使用
org.apache.tools.ant.types.Reference
类型。这其实是字符串参数构造函数功能的一种特殊情况。若要接受路径数据类型作为
classpathref
属性的引用,可实现
setClasspathRef
方法:
public void setClasspathRef(Reference r) {
createClasspath().setRefid(r);
}
2. 支持嵌套元素
Ant 的许多任务会使用嵌套元素,为包含它们的任务提供丰富且层次化的数据。例如,典型的
<javac>
任务会包含一个嵌套的
<classpath>
元素:
<javac destdir="${build.classes.dir}"
debug="${build.debug}"
srcdir="${src.dir}">
<classpath refid="compile.classpath"/>
</javac>
支持嵌套元素的代码并不复杂,Ant 内部有一套成熟的机制来实现这一点。与属性类似,当 Ant 遇到嵌套元素时,会查找任务中特定命名的方法并调用。Ant 使用特定命名的方法处理三种不同的子元素场景,具体如下表所示:
| 场景 | 方法 |
| — | — |
| Ant 可以使用无参构造函数构造对象,且无需预填充 |
public void addElementName(ObjectType obj)
|
| Ant 可以使用无参构造函数构造对象,但需要预填充 |
public void addConfiguredElementName(ObjectType obj)
|
| 任务需要自己构造对象 |
public ObjectType createElementName()
|
以下是一个支持嵌套文件集的任务示例:
package org.example.antbook.tasks;
import java.util.Vector;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
public class NestedTask extends Task {
private Vector filesets = new Vector();
public void addFileset(FileSet fileset) {
filesets.add(fileset);
}
public void execute() {
log("# filesets = " + filesets.size());
}
}
在构建文件中使用该任务的示例如下:
<taskdef name="nested"
classname="org.example.antbook.tasks.NestedTask"
classpath="${build.dir}"
/>
<fileset dir="src" excludes="**/*.java" id="non.java.files"/>
<nested>
<fileset dir="images">
<include name="**/*.gif"/>
</fileset>
<fileset refid="non.java.files"/>
</nested>
对于嵌套元素,建议使用
addXXX
或
addConfiguredXXX
方法,主要原因是这样可以支持类型的多态性。
addConfiguredXXX
方法在任务需要立即获取一个完全填充的对象时很有用,但实际中很少用到。当任务需要自己构造对象时,可使用
createXXX
方法,比如对象没有无参构造函数,或者
addXXX
方法无法满足需求时。
3. 支持数据类型
从任务实现的角度来看,支持嵌套数据类型的任务和支持嵌套自定义类的任务没有区别。Ant 的内省机制对它们的处理方式相同,而且数据类型支持使用
id/refid
属性实现重用。嵌套数据类型隐式支持使用
refid
进行引用,因此任务代码无需显式添加此支持。需要注意的是,如果要导入自定义数据类型的引用,任务和数据类型必须由同一个类加载器加载。将 JAR 文件放入 Ant 的
lib
目录可自动实现这一点。如果在
<taskdef>
和
<datatype>
声明中指定了类加载器,则必须在所有声明中设置
loaderref
属性为相同的类加载器引用。
4. 允许自由格式的主体文本
对于某些任务,XML 属性和元素结构的约束过于严格。要求任务的用户处理属性值中字符转义的问题可能会很麻烦。例如,使用
<echo>
任务的
message
属性显示 “6 < 9” 需要使用实体引用:
<echo message="6 < 9"/>
若不使用实体引用,代码
<echo message="6 < 9"/>
会导致 XML 错误。为了避免这种情况,可以允许任务访问元素文本。在任务中添加
addText
方法,可让 Ant 允许元素直接包含文本主体或在 CDATA 部分包含文本:
<echo><![CDATA[
6 < 9
]]></echo>
需要注意的是,在调用
addText
方法之前,Ant 属性不会自动展开。
5. 创建基本的 Ant 任务子类
后续的任务将继承自
org.apache.tools.ant.Task
类。继承该类的主要原因是可以访问 Ant 的内部 API,
Task
类提供了以下功能:
- 访问包含的目标
- 访问当前项目
- 日志记录功能
不继承
Task
类的类也可以通过实现
setProject
方法来访问项目实例和日志记录功能:
public void setProject (org.apache.tools.ant.Project project)
不继承
Task
类的主要原因是避免类对 Ant 的依赖,以及保持自己的继承层次结构。但如果实现了
setProject
方法,实际上已经创建了对 Ant 的依赖。对于保持自己的继承层次结构,建议将其他 Java 类封装在
Task
子类中,这样可以在不改变任务和构建文件接口的情况下修改封装代码的内部实现。
6. 为任务添加属性
以下是一个继承自
Task
的基本 Ant 任务示例,它演示了一个可选属性的使用:
package org.example.antbook.tasks;
import org.apache.tools.ant.Task;
public class MessageTask extends Task {
private String message = "";
public void setMessage(String message) {
this.message = message;
}
public void execute() {
log(message);
}
}
在构建文件中使用该任务的示例如下:
<target name="messagetask" depends="compile">
<taskdef name="message"
classname="org.example.antbook.tasks.MessageTask"
classpath="${build.dir}"
/>
<property name="the.message" value="blue scooter"/>
<message message="${the.message}"/>
</target>
该任务类似于
<echo>
任务,只是将
message
属性的值记录下来(日志级别为
MSG_INFO
)。需要注意的是,Ant 会自动处理 XML 属性中的属性扩展。
7. 处理元素文本
元素文本通过
addText
方法传递给包含它的对象,通常是任务本身或嵌套元素。需要注意的是,文本会原样提供,属性引用不会自动展开。以下是
MessageTask
的一个变体示例,它将消息文本作为元素数据而不是属性:
package org.example.antbook.tasks;
import org.apache.tools.ant.Task;
public class MessageTask2 extends Task {
private String message = "";
public void addText(String message) {
this.message = message;
}
public void execute() {
log(message);
}
}
在构建文件中使用该任务的示例如下:
<property name="another.message" value="light up ahead"/>
<message2>${another.message}</message2>
输出结果为
[message2] ${another.message}
。如果需要解析属性引用,可在
execute
方法中调用
Project
的
replaceProperties
方法:
public void execute() {
log(getProject().replaceProperties(message));
}
修改后,输出结果为
[message2] light up ahead
。
8. 操作文件集
Ant 的数据类型使得编写处理常见 Java 构建领域对象(如路径和文件集)的任务变得更加简单。如果要编写一个处理单个目录树中文件的任务,并且希望该任务作为一个隐式文件集,可以使用
org.apache.tools.ant.taskdefs.MatchingTask
基类,这样可以节省很多工作。以下是一个处理文件集的任务示例:
package org.example.antbook.tasks;
import java.io.File;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;
public class FileProcTask extends MatchingTask {
private File dir;
public void setDir (File dir) {
this.dir = dir;
}
public void execute() throws BuildException {
if (dir == null) {
throw new BuildException("dir must be specified");
}
log("dir = " + dir, Project.MSG_DEBUG);
DirectoryScanner ds = getDirectoryScanner(dir);
String[] files = ds.getIncludedFiles();
for (int i = 0; i < files.length; i++) {
log("file: " + files[i]);
}
dir = null;
}
}
使用
MatchingTask
可以获得一些与
<fileset>
数据类型类似的功能:
-
includes/excludes
属性
-
defaultexcludes
属性
-
<include>/<exclude> / <includesfile> / <excludesfile>
元素
-
<patternset>
元素
需要在自己的代码中提供隐式文件集使用的目录。在上述示例中,实现了
setDir
方法,并在
execute
方法中检查
dir
属性是否指定,如果未指定则抛出
BuildException
。
MatchingTask
基类提供了
getDirectoryScanner(File baseDir)
方法,用于获取一个
DirectoryScanner
实例,该实例会考虑所有指定的包含和排除规则。
虽然许多 Ant 任务都继承自
MatchingTask
,但目前的趋势是逐渐远离这个任务,因为显式文件集已被证明更加灵活。如果要编写自己的任务,使用
MatchingTask
作为基类仍然很有吸引力。继承自
MatchingTask
的任务应该只处理单个隐式文件集,需要支持多个嵌套文件集的任务应该继承自
Task
。
9. 错误处理
作为任务开发者,需要决定如何处理任务配置或执行过程中可能出现的异常情况。Ant 会捕获任务方法抛出的异常,并在此时使构建失败。当希望构建因任何原因失败时,可抛出 Ant 的
org.apache.tools.ant.BuildException
异常,它是
RuntimeException
的子类。
可以添加一个
failonerror
属性,让构建文件控制构建是否失败,就像许多 Ant 核心任务(如
<java>
任务)一样。以下是一个简单的
Task
类示例:
package org.example.antbook.tasks;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
public class ConditionalFailTask extends Task {
private boolean failOnError = true;
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
public void execute() throws BuildException {
if (failOnError) {
throw new BuildException("oops!");
}
log("success");
}
}
建议默认设置为在出错时失败,这样构建文件的编写者如果希望关闭构建失败,就需要显式地进行设置。这与大多数内置 Ant 任务的设计一致,但也有一些例外情况。
10. 测试 Ant 任务
Ant 代码库不仅包含 Ant 核心和可选任务的源代码,还包含越来越多的 JUnit 测试用例,这些测试用例有助于验证代码更改是否会破坏预期的功能。目前,Ant 二进制发行版不包含基本测试用例类或测试基础设施,但可以通过 Ant 的 CVS 仓库免费获取。
由于建议将现有功能封装在 Ant 任务外观中,因此针对被封装的底层 API 编写单元测试会更加简单直接。但如果需要对复杂的 Ant 任务进行单元测试,最好的方法是访问 Ant 的 CVS 仓库,并使用
org.apache.tools.ant.BuildFileTest
基类。Ant 自己的构建文件中有
<junit>
任务用于执行测试用例。可以编写测试用例来断言某些消息是否被记录、属性是否具有预期的值,或者在预期时是否抛出
BuildException
。
11. 执行外部程序
编写 Ant 任务的一个常见原因是封装本地程序,使其功能支持更复杂的能力,如遍历文件集和进行依赖检查。在编写自定义任务来封装构建过程中需要调用的可执行程序之前,先研究一下内置的
<apply>
任务,看它是否能满足需求。
如果确定
<apply>
或
<exec>
任务无法满足需求,就需要更深入地研究 Ant 的 API。在跨平台的情况下,从 Ant 成功启动另一个程序并不容易,Ant 为此做了很多工作来处理这些问题。以下是一个示例任务,它会为每个嵌套
<fileset>
元素指定的文件执行一次
myprog
程序:
package org.example.antbook.tasks;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
public class RunTask extends Task {
private Vector filesets = new Vector();
private boolean flag = true;
public void addFileset(FileSet fileset) {
filesets.add(fileset);
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void execute() {
int fileCount = 0;
int successCount = 0;
Enumeration enum = filesets.elements();
while (enum.hasMoreElements()) {
FileSet fileset = (FileSet) enum.nextElement();
DirectoryScanner ds =
fileset.getDirectoryScanner(getProject());
String[] files = ds.getIncludedFiles();
for (int i = 0; i < files.length; i++) {
fileCount++;
File f = new File(fileset.getDir(getProject()), files[i]);
if (process(f)) {
successCount++;
}
}
}
log(successCount + " out of " +
fileCount + " files processed successfully");
}
protected boolean process(File file) {
Commandline cmdline = new Commandline();
cmdline.setExecutable("myprog");
if (flag) {
cmdline.createArgument().setValue("-flag");
}
cmdline.createArgument().setValue(file.toString());
LogStreamHandler streamHandler =
new LogStreamHandler(this, Project.MSG_INFO,
Project.MSG_WARN);
Execute runner = new Execute(streamHandler, null);
runner.setAntRun(project);
runner.setCommandline(cmdline.getCommandline());
int retVal = 0;
try {
retVal = runner.execute();
}
catch (IOException e) {
log(e.getMessage(), Project.MSG_DEBUG);
return false;
}
return true;
}
}
RunTask
类使用了 Ant 提供的几个类。
RunTask
通过
addFileset
方法收集任意数量的文件集。
execute
方法会遍历每个文件集,对于每个文件集,使用
DirectoryScanner
获取包含的文件列表。
getIncludedFiles
方法返回的
String[]
中的值不是完整路径,而是相对于文件集根目录的相对路径。通过使用文件集的根目录作为父目录来构造
File
对象,可得到文件的绝对路径。
在
process
方法中,使用 Ant 的
Commandline
对象构造完整的命令行,通过
flag
属性启用或禁用条件开关。Ant 的
Execute
类处理了许多启动外部进程时的复杂问题,确保支持各种 JDK 和平台。在适当的情况下,会使用
ANT_HOME/bin
目录中的启动脚本,如
antRun.bat
、
antRun
和
antRun.pl
。
处理进程输出
提供给
Execute
实例的
LogStreamHandler
用于将标准输出和错误定向到所需的 Ant 日志级别。如果任务需要捕获执行过程的输出,可以使用
PumpStreamHandler
并提供自己的输出流。
本地执行总结
Ant 的许多 API 使得从构建文件中启动本地可执行文件和脚本变得更加容易。在编写自定义 Ant 任务来封装本地执行之前,确保
<apply>
和
<exec>
任务无法满足需求。如果在
RunTask
示例中不需要
flag
属性,就可以使用
<apply>
任务。
12. 在任务中执行 Java 程序
执行 Java 程序可以像执行本地程序一样,在新的 JVM 中运行,但这样会有启动开销。也可以在 Ant 自己的 JVM 中调用 Java 程序,这样能大大提高性能。
<java>
任务会根据
fork
属性的值选择使用哪种方法。封装 Java 执行的主要原因是无法控制要封装的程序的源代码。如果能控制源代码,最好编写一个任务来直接封装 API,而不是运行
main
方法(或可执行 JAR)。
如果
<java>
任务无法满足需求,想要构建一个包装任务来执行 Java 程序,有两种不错的方法:
- 创建一个
Task
扩展类,封装
<java>
任务并直接控制它。
- 创建
<java>
任务的扩展类,继承其所有内置功能,让构建文件的编写者可以控制类路径、分叉、环境等参数。
这两种方法都能快速实现执行 Java 程序的功能,但建议使用封装的方法,这样可以根据需要暴露
<java>
任务的部分或全部功能。无论使用哪种方法,都需要使用
org.apache.tools.ant.taskdefs.Java
类,它是
<java>
任务对应的类。
执行分叉 Java 程序的示例任务
假设没有搜索引擎命令行工具程序的源代码,该工具的命令行格式为:
java org.example.antbook.tasks.Searcher index query
其中
index
是 Lucene 索引的目录路径,
query
是搜索查询。我们希望创建一个任务包装器来替代使用
<java>
任务。使用
<java>
任务调用该程序的示例如下:
<java classname="org.example.antbook.tasks.Searcher"
fork="true"
classpathref="task.classpath">
<arg file="${index.dir}"/>
<arg value="${query}"/>
</java>
将
Searcher
封装在自定义任务中是有必要的。如果没有提供正确数量的命令行参数,程序会执行
System.exit(-1)
。如果不进行分叉,用户不小心遗漏参数时,Ant 会立即终止,甚至不会显示
BUILD FAILED
消息。虽然设置
fork="true"
可以在参数不正确时让 Ant 保持运行,但这存在风险,因为需要构建文件的编写者了解这些情况。更好的做法是让构建文件看起来像这样:
<searcher classpathref="task.classpath"
index="${index.dir}"
query="${query}"
/>
这样可以消除风险,因为
<searcher>
任务内部总是启用分叉模式。此外,还能提高可读性,并增强实际运行的程序与 Ant 任务之间的耦合性。具体来说,
index
和
query
属性可以按任意顺序指定,用户也无需记住类名。
以下是实现该功能的示例代码:
package org.example.antbook.tasks;
import java.io.File;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
public class SearcherTask extends Task {
private Path classpath;
private File indexDir;
private String query;
public void setClasspath(Path classpath) {
this.classpath = classpath;
}
public void setClasspathRef(Reference ref) {
createClasspath().setRefid(ref);
}
public Path createClasspath() {
if (classpath == null) {
classpath = new Path(this.getProject());
}
return classpath.createPath();
}
public void setIndex(File indexDir) {
this.indexDir = indexDir;
}
public void setQuery(String query) {
this.query = query;
}
public void execute() throws BuildException {
Java javaTask = (Java) getProject().createTask("java");
javaTask.setTaskName(getTaskName());
javaTask.setClassname("org.example.antbook.tasks.Searcher");
javaTask.setClasspath(classpath);
javaTask.createArg().setFile(indexDir);
javaTask.createArg().setValue(query);
javaTask.setFork(true);
if (javaTask.executeJava() != 0) {
throw new BuildException("error");
}
}
}
通过调用
setTaskName
方法并传入当前任务名称,输出会以自定义任务名称作为前缀,而不是
[java]
。在这个例子中,
Searcher
的输出会以
[searcher]
作为前缀。
SearcherTask
展示了几个在任务外观中包装 Java 程序的关键技术。最重要的是,它在指定类路径方面提供了灵活性,允许使用
classpath
或
classpathref
属性,或者嵌套的
<classpath>
元素。虽然这些在任务代码中需要一些处理,但由于 Ant 的 API,工作量很小。
SearcherTask
的主要技巧是在内部使用
<java>
任务,虽然这种方式有点像“黑客”做法,但它是处理分叉、类路径、命令行参数和 JVM 参数复杂性的最简单方法。通过了解 Ant 如何为任务填充数据,就可以明白它是如何工作的。只需像 Ant 在构建文件中使用
<java>
任务那样调用设置器和其他特殊方法,如以
create/add
开头的方法。
编写 Ant 任务全攻略(续)
13. 总结与最佳实践
在编写 Ant 任务时,有许多关键要点和最佳实践需要牢记,以确保任务的高效性、可维护性和灵活性。以下是对本文内容的总结和一些实用的最佳实践建议。
数据类型与嵌套元素
-
数据类型引用
:可重复使用的数据类型是 Ant 的强大特性。使用
org.apache.tools.ant.types.Reference类型来接受先前定义的数据类型的 id 作为属性,能增强任务的复用性。 -
嵌套元素支持
:使用
addXXX或addConfiguredXXX方法处理嵌套元素,优先考虑这两种方法以支持类型的多态性。仅在任务需要自己构造对象时使用createXXX方法。
任务属性与文本处理
- 属性添加 :为任务添加属性时,遵循 Ant 的属性内省和填充机制,Ant 会自动处理 XML 属性中的属性扩展。
-
元素文本处理
:使用
addText方法处理元素文本,若需要解析属性引用,调用Project的replaceProperties方法。
文件集操作
-
使用
MatchingTask:对于处理单个目录树中文件的任务,使用org.apache.tools.ant.taskdefs.MatchingTask基类可节省大量工作,但目前趋势是更倾向于显式文件集。
错误处理与测试
-
错误处理
:在任务开发中,使用
org.apache.tools.ant.BuildException处理异常情况,默认设置出错时失败,让构建文件编写者显式关闭失败设置。 -
任务测试
:针对被封装的底层 API 编写单元测试,对于复杂任务,可使用
org.apache.tools.ant.BuildFileTest基类进行测试。
外部程序与 Java 程序执行
-
外部程序执行
:在编写自定义任务封装本地执行前,确保
<apply>和<exec>任务无法满足需求。使用 Ant 的Commandline和Execute类处理外部程序的执行。 -
Java 程序执行
:若
<java>任务无法满足需求,可通过封装或扩展<java>任务来执行 Java 程序,优先考虑封装方法以控制功能暴露。
14. 流程图总结
下面是一个 mermaid 格式的流程图,总结了编写 Ant 任务的主要步骤:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始编写 Ant 任务]):::startend --> B{任务需求分析}:::decision
B -->|处理数据类型| C(支持数据类型引用):::process
B -->|使用嵌套元素| D(支持嵌套元素):::process
B -->|添加属性| E(为任务添加属性):::process
B -->|处理文本| F(处理元素文本):::process
B -->|操作文件集| G(使用 MatchingTask 操作文件集):::process
B -->|执行外部程序| H(封装外部程序执行):::process
B -->|执行 Java 程序| I(封装 Java 程序执行):::process
C --> J(考虑 id/refid 引用):::process
D --> K(选择合适的方法处理嵌套元素):::process
E --> L(利用 Ant 属性扩展):::process
F --> M(使用 addText 方法):::process
M --> N{是否需要解析属性引用}:::decision
N -->|是| O(调用 replaceProperties 方法):::process
N -->|否| P(直接处理文本):::process
G --> Q{是否使用显式文件集}:::decision
Q -->|是| R(使用显式文件集):::process
Q -->|否| S(使用 MatchingTask):::process
H --> T(使用 Commandline 和 Execute 类):::process
I --> U{选择封装或扩展方式}:::decision
U -->|封装| V(封装 <java> 任务):::process
U -->|扩展| W(扩展 <java> 任务):::process
J --> X(确保类加载器一致):::process
K --> Y(优先使用 addXXX 或 addConfiguredXXX 方法):::process
R --> Z(灵活配置文件集):::process
S --> AA(提供隐式文件集目录):::process
T --> AB(处理进程输出):::process
V --> AC(控制 <java> 任务功能暴露):::process
W --> AD(继承 <java> 任务功能):::process
X --> AE([完成任务编写]):::startend
Y --> AE
L --> AE
P --> AE
O --> AE
Z --> AE
AA --> AE
AB --> AE
AC --> AE
AD --> AE
这个流程图展示了从任务需求分析到最终完成任务编写的整个过程,涵盖了数据类型处理、嵌套元素使用、属性添加、文本处理、文件集操作、外部程序和 Java 程序执行等关键步骤。
15. 未来展望
随着软件开发的不断发展,Ant 作为一个强大的构建工具,其任务编写也将面临新的挑战和机遇。未来,可能会有更多的自动化和智能化需求,例如根据项目的不同环境自动调整任务配置,或者通过机器学习算法优化任务执行顺序以提高构建效率。
同时,与其他现代构建工具和框架的集成也将变得更加重要。例如,将 Ant 任务与持续集成/持续部署(CI/CD)工具集成,实现更高效的软件开发流程。此外,随着 Java 语言和相关技术的不断演进,Ant 任务也需要不断适应新的特性和规范。
总之,编写 Ant 任务是一个不断学习和实践的过程。通过掌握本文介绍的知识和最佳实践,开发者可以编写出高质量、灵活且易于维护的 Ant 任务,为软件开发项目提供有力的支持。
希望这些内容能帮助开发者更好地理解和应用 Ant 任务编写的技巧,在实际项目中发挥更大的作用。如果你在编写 Ant 任务过程中遇到任何问题,欢迎在评论区留言讨论。
超级会员免费看
3655

被折叠的 条评论
为什么被折叠?



