Ant 任务开发与扩展全解析
1. 支持任意命名的元素和属性
在 Ant 1.5 之前,任务在运行时无法动态添加新的属性或元素。例如,若不修改 Ant 源代码,就无法在
<ejbjar>
中添加新的
<condition>
测试或其他元素。需要动态扩展的任务通常采用不太优雅的方式实现,如支持用户定义参数的任务在构建文件中可能如下指定:
<paramtask>
<param name="username" value="erik"/>
<param name="hostname" value="localhost"/>
</paramtask>
对应的 Java 代码如下:
package org.example.antbook.tasks;
import org.apache.tools.ant.Task;
import java.util.Vector;
import java.util.Iterator;
public class ParamTask extends Task {
private Vector params = new Vector();
public Param createParam() {
Param p = new Param();
params.add(p);
return p;
}
public void execute() {
Iterator iter = params.iterator();
while (iter.hasNext()) {
Param p = (Param) iter.next();
log(p.getName() + " = " + p.getValue());
}
}
public static class Param {
private String name;
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
将用户定义的参数指定为任务的属性会更简洁,如下所示:
<dynatask username="erik" hostname="localhost"/>
Ant 1.5 引入了动态配置机制,借助
DynamicConfigurator
接口,任务可以支持新的属性和元素。示例代码如下:
package org.example.antbook.tasks;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DynamicConfigurator;
import org.apache.tools.ant.Task;
import java.util.Enumeration;
import java.util.Properties;
public class DynaTask extends Task implements DynamicConfigurator {
private Properties params = new Properties();
public void setDynamicAttribute(String name, String value)
throws BuildException {
params.setProperty(name, value);
}
public Object createDynamicElement(String name)
throws BuildException {
throw new
BuildException("Element " + name + " is not supported");
}
public void execute() {
Enumeration enum = params.keys();
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement();
log(name + " = " + params.get(name));
}
}
}
此示例中,
DynaTask
仅支持动态属性,通过
createDynamicElement
方法抛出
BuildException
拒绝动态元素。不过,它可以采用工厂式设计模式,根据元素名称查找并实例化类实例。
2. 构建任务库
构建可重用任务库能让构建文件编写者更轻松地使用任务。将任务整合到库中,只需构建一个 JAR 文件。建议在库中包含一个属性文件,用于快速将构建文件任务名称映射到实际实现的 Java 类名。示例属性文件内容如下:
simpletask=org.example.antbook.tasks.SimpleTask
message=org.example.antbook.tasks.MessageTask
message2=org.example.antbook.tasks.MessageTask2
enum=org.example.antbook.tasks.EnumTask
fileproc=org.example.antbook.tasks.FileProcTask
nested=org.example.antbook.tasks.NestedTask
run=org.example.antbook.tasks.RunTask
构建 JAR 文件的目标如下:
<target name="jar" depends="compile">
<copy file="${meta.dir}/taskdef.properties"
todir="${build.dir}"/>
<jar destfile="${dist.dir}/tasks.jar"
basedir="${build.dir}"/>
</target>
构建文件可轻松使用任务库,无需了解每个任务的类名,示例如下:
<?xml version="1.0" ?>
<project name="library" default="main">
<property name="tasks.jar" location="dist/tasks.jar"/>
<taskdef resource="taskdef.properties" classpath="${tasks.jar}"/>
<target name="usetasks">
<simpletask/>
<property name="the.message" value="blue scooter"/>
<message message="${the.message}"/>
<property name="another.message" value="light up ahead"/>
<message2>${another.message}</message2>
<enum version="2.3"/>
<fileproc dir="${basedir}">
<include name="**/*.java"/>
</fileproc>
<nested>
<fileset dir="images">
<include name="**/*.gif"/>
</fileset>
<fileset refid="non.java.files"/>
</nested>
<run flag="off">
<fileset dir="." excludes="**/*.class"/>
</run>
</target>
<target name="main" depends="usetasks"/>
</project>
这里仅使用一个
<taskdef>
就定义了
taskdef.properties
中的所有任务,并在
usetasks
目标中使用了部分任务。
3. 支持多个版本的 Ant
为旧版本 Ant 编写的任务通常能在未来的 Ant 1.x 版本(可能包括 Ant 2.0)中正常工作,但反之未必。Ant 的 API 会不断发展,新版本的特性在旧版本中可能不被支持。为确保任务能在不同版本的 Ant 中兼容,需避免使用较新的 API 功能,如
Project.setNewProperty
和
DynamicConfigurator
。最佳做法是使用要支持的最旧版本的 Ant 编译任务,并在每个版本上进行测试。
4. Ant 的其他扩展方式
Ant 的可扩展性不仅局限于自定义 Java 任务,还可通过以下多种方式进行扩展:
- 使用
<script>
任务,借助多种流行脚本语言编写临时任务。
- 使用自定义构建监听器和记录器监控或记录构建过程。
- 实现自定义
<mapper>
。
- 使用专门的选择器进行强大的文件集过滤。
- 使用过滤器转换文本流。
5. 在 Ant 中进行脚本编写
<script>
任务支持多种脚本语言命令,可用于调用任务和操作其他 Ant 对象。脚本可使用 JavaScript(确切地说是 Mozilla 的 Rhino 实现)或其他语言编写,是在构建文件内部扩展 Ant 的简便方法,无需分发额外库或添加源文件。
<script>
任务基于 IBM 的 Bean Scripting Framework,需下载
bsf.jar
并放入
ANT_HOME/lib
目录。同时,还需提供要使用的脚本语言实现。以生成随机数并赋值给属性为例,示例代码如下:
<project name="script_example" default="test-random">
<description>
Use a script task to generate a random number, then
print it
</description>
<target name="random">
<script language="javascript"><![CDATA[
//NB: an unqualified Math is the JavaScript object
var r=java.lang.Math.random();
var num = Math.round(r*10);
project.setNewProperty("random", num);
self.log("Generated random number " + num, project.MSG_DEBUG);
]]>
</script>
</target>
<target name="test-random" depends="random">
<echo>Random number is ${random}</echo>
</target>
</project>
运行
test-random
目标将输出 0 到 10 之间的随机数。
6.
<script>
提供的隐式对象
<script>
任务为脚本上下文提供两个固定名称的隐式对象:
project
和
self
。
project
对象是
org.apache.tools.ant.Project
实例的引用,方便通过
Project
对象 API 设置和访问属性;
self
对象是
org.apache.tools.ant.taskdefs.optional.Script
实例的引用,用于记录消息。
除
self
和
project
外,
<script>
任务还提供 Ant 属性、目标和引用到脚本上下文。属性和目标按名称提供,引用按 ID 或名称提供。但需注意,名称不是有效 Java 标识符的隐式对象会被忽略。示例如下:
<?xml version="1.0" ?>
<project name="script_context" default="main">
<property name="legalName" value="accessible"/>
<property name="illegal name" value="inaccessible?"/>
<target name="main">
<script language="javascript"><![CDATA[
self.log("legalName = " + legalName);
self.log("illegal name = " + project.getProperty("illegal name"));
self.log("test = " + test);
self.log("echo_task = " + echo_task);
self.log("script_context = " + script_context);
self.log("project = " + project);
echo_task.setMessage("invoked via <script>");
echo_task.execute();
]]>
</script>
</target>
<target name="test">
<echo id="echo_task"/>
</target>
</project>
输出结果如下:
main:
[script] legalName = accessible
[script] illegal name = inaccessible?
[script] test = test
[script] echo_task = org.apache.tools.ant.taskdefs.Echo@7a8a02
[script] script_context = org.apache.tools.ant.Project@7ebe1
[script] project = org.apache.tools.ant.Project@7ebe1
[echo] invoked via <script>
legalName
属性可直接访问,而
illegal name
因包含空格,只能通过
project.getProperty
访问。
7. 脚本编写总结
脚本框架功能强大,可通过完整包名使用任何 Java 类,通过名称或 ID 引用构建文件中的对象。但过多使用
<script>
任务会使构建文件变得复杂,限制代码复用。若项目使用脚本任务,建议将脚本提取到单独文件中,使用
<script file="random.js">
引用。此外,脚本框架在出错时信息不够明确,JavaScript 与 Java 相似但有差异,容易造成误导。
8. 监听器和记录器
Ant 提供了监控其执行进度的功能,涉及两个紧密相关的概念:监听器和记录器。
8.1 监听器
附加的
BuildListener
在构建生命周期中会收到七个事件通知,包括构建开始/结束、目标开始/结束、任务开始/结束和消息记录。每个事件都会传递一个
BuildEvent
对象,该对象根据事件类型封装不同信息,具体如下表所示:
| BuildListener 方法 | BuildEvent 内容 |
| — | — |
| buildStarted 和 buildFinished | 通过
getProject()
获取项目,通过
getException()
获取异常 |
| targetStarted 和 targetFinished | 通过
getTarget()
获取目标,通过
getException()
获取异常,也可访问项目对象 |
| taskStarted 和 taskFinished | 通过
getTask()
获取任务,通过
getException()
获取异常,也可访问项目和目标对象 |
| messageLogged | 通过
getMessage()
获取消息,通过
getPriority()
获取消息优先级,根据消息记录位置,还可使用
getTask
、
getTarget
或
getProject
|
使用监听器时,需在命令行使用
-listener
开关指定,例如:
ant -listener org.apache.tools.ant.listener.CommonsLoggingListener
8.2 记录器
BuildLogger
接口在
BuildListener
基础上增加了对输出和错误打印流的访问,还提供了设置 emacs 模式和消息输出级别的方法。Ant 项目只能关联一个
BuildLogger
,可通过命令行
-logger
开关指定。
-emacs
启用 emacs 模式,
-quiet
、
-verbose
和
-debug
开关指定日志级别。默认输出级别为信息级别。
以下是一个自定义监听器的示例,它是 Jakarta Commons Logging API 的包装器:
package org.apache.tools.ant.listener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogConfigurationException;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
/**
* Jakarta Commons Logging listener.
* Note: do not use the SimpleLog as your logger implementation as it
* causes an infinite loop since it writes to System.err, which Ant traps
* and reroutes to the logger/listener layer.
*
* @author Erik Hatcher
* @since Ant 1.5
*/
public class CommonsLoggingListener implements BuildListener {
private boolean initialized = false;
private LogFactory logFactory;
public CommonsLoggingListener() {
try {
logFactory = LogFactory.getFactory();
} catch (LogConfigurationException e) {
e.printStackTrace(System.err);
return;
}
initialized = true;
}
public void buildStarted(BuildEvent event) {
if (initialized) {
Log log = logFactory.getInstance(Project.class);
log.info("Build started.");
}
}
public void buildFinished(BuildEvent event) {
if (initialized) {
Log log = logFactory.getInstance(Project.class);
if (event.getException() == null) {
log.info("Build finished.");
} else {
log.error("Build finished with error.", event.getException());
}
}
}
public void targetStarted(BuildEvent event) {
if (initialized) {
Log log = logFactory.getInstance(Target.class);
log.info("Target \"" + event.getTarget().getName() +
"\" started.");
}
}
public void targetFinished(BuildEvent event) {
if (initialized) {
String targetName = event.getTarget().getName();
Log log = logFactory.getInstance(Target.class);
if (event.getException() == null) {
log.info("Target \"" + targetName + "\" finished.");
} else {
log.error("Target \"" + targetName
+ "\" finished with error.", event.getException());
}
}
}
public void taskStarted(BuildEvent event) {
if (initialized) {
Task task = event.getTask();
Log log = logFactory.getInstance(task.getClass().getName());
log.info("Task \"" + task.getTaskName() + "\" started.");
}
}
public void taskFinished(BuildEvent event) {
if (initialized) {
Task task = event.getTask();
Log log = logFactory.getInstance(task.getClass().getName());
if (event.getException() == null) {
log.info("Task \"" + task.getTaskName() + "\" finished.");
} else {
log.error("Task \"" + task.getTaskName()
+ "\" finished with error.", event.getException());
}
}
}
public void messageLogged(BuildEvent event) {
if (initialized) {
Object categoryObject = event.getTask();
if (categoryObject == null) {
categoryObject = event.getTarget();
if (categoryObject == null) {
categoryObject = event.getProject();
}
}
Log log = logFactory.getInstance(
categoryObject.getClass().getName());
switch (event.getPriority()) {
case Project.MSG_ERR:
log.error(event.getMessage());
break;
case Project.MSG_WARN:
log.warn(event.getMessage());
break;
case Project.MSG_INFO:
log.info(event.getMessage());
break;
case Project.MSG_VERBOSE:
// 原文档此处未完成,可根据实际情况补充
break;
}
}
}
}
通过以上内容,我们全面了解了 Ant 任务开发、扩展的多种方式,包括支持任意命名元素和属性、构建任务库、脚本编写、监听器和记录器的使用等,这些功能能帮助我们更灵活、高效地进行项目构建。
Ant 任务开发与扩展全解析
9. 自定义映射器开发
自定义
<mapper>
可以根据特定需求对文件进行映射转换。虽然文中未详细给出具体示例代码,但我们知道它是 Ant 可扩展性的一部分。在实际应用中,自定义映射器可以实现一些特殊的文件路径转换逻辑。例如,将文件的扩展名进行替换,或者根据一定规则修改文件的目录结构等。要开发自定义映射器,通常需要实现相应的接口或继承相关的基类,然后在 Ant 构建文件中使用自定义的映射器。以下是一个简单的开发步骤示例:
1.
定义映射逻辑类
:创建一个 Java 类,实现映射所需的逻辑。
2.
实现接口或继承基类
:根据 Ant 的要求,实现特定的映射器接口或继承相关基类。
3.
在构建文件中使用
:在 Ant 构建文件中引用自定义的映射器。
10. 创建自定义选择器
自定义选择器可以用于强大的文件集过滤。通过自定义选择器,我们可以根据自己的规则筛选出符合条件的文件。例如,根据文件的修改时间、文件大小、文件名的特定格式等进行筛选。以下是创建自定义选择器的一般步骤:
1.
创建选择器类
:创建一个 Java 类,实现选择器的逻辑。
2.
实现选择器接口
:实现 Ant 提供的选择器接口,重写相关方法。
3.
在构建文件中使用
:在 Ant 构建文件的
<fileset>
中使用自定义选择器。
以下是一个简单的自定义选择器示例,用于筛选出文件名以
.txt
结尾的文件:
import org.apache.tools.ant.types.selectors.FileSelector;
import java.io.File;
public class TxtFileSelector implements FileSelector {
@Override
public boolean isSelected(File basedir, String filename, File file) {
return filename.endsWith(".txt");
}
}
在 Ant 构建文件中使用该选择器:
<project name="customSelectorExample" default="main">
<target name="main">
<fileset dir="." includes="**/*">
<customselector classname="TxtFileSelector"/>
</fileset>
</target>
</project>
11. 实现自定义过滤器
自定义过滤器可以用于转换文本流。例如,对文件内容进行替换、添加前缀或后缀等操作。实现自定义过滤器的步骤如下:
1.
创建过滤器类
:创建一个 Java 类,实现过滤器的逻辑。
2.
实现过滤器接口
:实现 Ant 提供的过滤器接口,重写相关方法。
3.
在构建文件中使用
:在 Ant 构建文件中使用自定义过滤器。
以下是一个简单的自定义过滤器示例,用于在文件内容前添加一行注释:
import org.apache.tools.ant.filters.BaseFilterReader;
import java.io.IOException;
import java.io.Reader;
public class AddCommentFilter extends BaseFilterReader {
private boolean addedComment = false;
public AddCommentFilter(Reader in) {
super(in);
}
@Override
public int read() throws IOException {
if (!addedComment) {
addedComment = true;
return "# This is a comment\n".codePointAt(0);
}
return super.read();
}
}
在 Ant 构建文件中使用该过滤器:
<project name="customFilterExample" default="main">
<target name="main">
<copy file="input.txt" tofile="output.txt">
<filterchain>
<filterreader classname="AddCommentFilter"/>
</filterchain>
</copy>
</target>
</project>
12. 总结
Ant 具有丰富的可扩展性,为开发者提供了多种扩展方式,以满足不同的项目需求。以下是对这些扩展方式的总结:
-
自定义 Java 任务
:通过编写自定义 Java 任务,可以实现复杂的构建逻辑。
-
脚本编写
:使用
<script>
任务可以方便地在构建文件中使用脚本语言进行扩展,但要注意避免过度使用导致构建文件复杂。
-
监听器和记录器
:监听器和记录器可以帮助监控和记录构建过程,方便调试和问题排查。
-
自定义映射器、选择器和过滤器
:这些扩展方式可以根据特定需求对文件进行处理和转换。
为了更清晰地展示 Ant 扩展方式的关系,以下是一个 mermaid 流程图:
graph LR
A[Ant 扩展方式] --> B[自定义 Java 任务]
A --> C[脚本编写]
A --> D[监听器和记录器]
A --> E[自定义映射器]
A --> F[自定义选择器]
A --> G[自定义过滤器]
在实际项目中,我们可以根据具体需求选择合适的扩展方式,灵活运用这些功能,提高项目构建的效率和灵活性。同时,要注意遵循兼容性原则,确保任务在不同版本的 Ant 中正常工作。通过不断学习和实践,我们可以更好地掌握 Ant 的扩展能力,为项目开发提供有力支持。
超级会员免费看
792

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



