Ant开发:动态配置、任务库构建与扩展方法
1. 支持任意命名的元素和属性
1.1 Ant 1.5 之前的情况
在 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;
}
}
}
1.2 Ant 1.5 的动态配置机制
Ant 1.5 引入了动态配置机制,允许将用户定义的参数指定为任务的属性,例如:
<dynatask username="erik" hostname="localhost"/>
通过实现
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
来拒绝动态元素。Ant 的内省机制会特殊处理实现了
DynamicConfigurator
接口的类,将属性名和值传递给
setDynamicAttribute
方法,将元素名传递给
createDynamicElement
方法。
1.3 未来应用展望
虽然 Ant 1.5 引入了
DynamicConfigurator
能力,但 Ant 自身的任务尚未使用该功能。未来,像
<ejbjar>
和
<serverdeploy>
这样的容器任务可能会利用这种动态能力,实现更灵活的供应商扩展,而无需修改 Ant 的核心任务。例如,XDoclet 已经在使用
DynamicConfigurator
动态查询支持的子任务的实现细节。
2. 构建任务库
2.1 任务库的构建方法
构建可重用任务库能让构建文件编写者更方便地使用任务。将任务整合到库中很简单,只需将它们打包成 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>
2.2 使用任务库
构建文件可以轻松使用任务库,而无需知道每个任务的类名。示例构建文件如下:
<?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 中工作的任务,例如 Ant 1.4.1 和 Ant 1.5,必须避免使用较新的 API 功能,如
Project.setNewProperty
和
DynamicConfigurator
。确保兼容性的最佳方法是使用要支持的最旧版本的 Ant 来编译任务,并在每个版本上进行测试。
4. Ant 的其他扩展方式
4.1 脚本编写
4.1.1
<script>
任务概述
<script>
任务支持多种脚本语言命令,可用于调用任务和操作其他 Ant 对象。脚本可以用 JavaScript(具体是 Mozilla 的 Rhino 实现)或其他多种语言编写,是在构建文件内部扩展 Ant 的简单方法,无需分发额外的库或添加新的源文件。
4.1.2 脚本任务的基础框架
<script>
任务基于 IBM 的 Bean Scripting Framework,需要下载
bsf.jar
并放入
ANT_HOME/lib
目录。同时,还需要提供所使用脚本语言的实现。例如,Mozilla Rhino 版本的 JavaScript 是常用选择,Python 等也是可行的。这些库的下载 URL 在 Ant 文档的库依赖部分有列出。
4.1.3 示例:生成随机数并赋值给属性
<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
目标会打印出
random
属性的值,该值在 0 到 10 之间(包含 0 和 10)。
4.1.4 隐式对象
<script>
任务为脚本上下文提供了两个固定名称的隐式对象:
project
和
self
。
project
对象是
org.apache.tools.ant.Project
实例的引用,可用于通过
Project
对象的 API 设置和访问属性;
self
对象是
org.apache.tools.ant.taskdefs.optional.Script
实例的引用,可用于使用
Task
提供的日志方法记录消息。
除了
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>
4.1.5 脚本编写总结
脚本框架功能强大,可以通过完整的包名使用任何 Java 类,通过名称或 id 值引用构建文件中的对象。但通常不建议在 Ant 文件中大量使用
<script>
任务,因为它会使构建文件变得复杂,限制代码的复用性。如果项目确实需要使用脚本任务,最好将脚本提取到单独的文件中,使用
<script file="random.js">
选项直接引用文件,这样可以将脚本与构建文件分离,便于复用。此外,脚本框架在出现问题时可能无法提供详细的错误信息,例如在将浮点数随机数转换为整数时使用
(int)
进行强制类型转换可能会导致错误。
4.2 监听器和记录器
4.2.1 监听器和记录器概述
Ant 提供了在执行过程中监控其进度的功能,涉及两个紧密相关的概念:监听器和记录器。为了开发自定义监听器和记录器,需要了解 Ant 使用的底层架构。
4.2.2 BuildListener 接口
附加的
BuildListener
会在构建生命周期中的七个事件发生时收到通知,这些事件包括:构建开始/结束、目标开始/结束、任务开始/结束以及消息记录。任何数量的构建监听器都可以附加到项目上,Ant 内部也会附加一些监听器来通知自己事件的发生,特别是构建结束事件,用于清理操作。每个事件都会传递一个
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
4.2.3 BuildLogger 接口
BuildLogger
接口在其父接口
BuildListener
的基础上,增加了对输出和错误打印流的访问。此外,还提供了设置 emacs 模式和消息输出级别的方法。
DefaultLogger
使用 emacs 模式为 IDE 集成提供格式化输出,因为大多数 IDE 可以解析 emacs 表示的文件错误位置。消息输出级别用于根据日志级别过滤输出。
一个 Ant 项目只能有一个关联的
BuildLogger
,因为记录器可以访问错误和输出打印流,所以只附加一个记录器是合理的。可以通过命令行的
-logger
开关指定
BuildLogger
,使用
-emacs
启用 emacs 模式,
-quiet
、
-verbose
和
-debug
开关指定日志级别。默认的输出级别是信息级别,介于安静和详细选项之间。
BuildLogger
会作为监听器内部附加到项目上,因此它会像其他附加的监听器一样接收事件。
需要注意的是,Ant 在记录器或监听器介入之前会有一些控制台输出,这些输出无法通过它们捕获。如果在项目范围之外发生错误(例如缺少
build.xml
),这些输出只能在控制台或日志文件中看到。
4.2.4 编写自定义监听器
以下是一个自定义监听器的示例,它是 Jakarta Commons Logging API 的包装器,可通过多种流行的日志记录 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;
}
}
}
}
4.3 其他扩展方式
除了脚本编写、监听器和记录器,Ant 还支持以下扩展方式:
- 自定义
<mapper>
实现
- 使用专门的选择器进行强大的文件集过滤
- 使用过滤器转换文本流
了解这些扩展方式在遇到继承的构建或需要特定功能时会很有用。
综上所述,Ant 提供了丰富的扩展机制,包括动态配置、任务库构建、脚本编写、监听器和记录器等,开发者可以根据具体需求选择合适的扩展方式,以实现更灵活、高效的项目构建。
5. 自定义映射器开发
5.1 自定义映射器概述
自定义
<mapper>
实现允许开发者根据特定需求对文件进行映射转换。在 Ant 构建过程中,可能需要对文件的名称、路径等进行修改,自定义映射器可以满足这些个性化的需求。
5.2 开发步骤
虽然文档未给出具体示例代码,但开发自定义映射器的一般步骤如下:
1.
定义映射逻辑
:确定需要对文件进行何种映射转换,例如修改文件名的前缀、后缀,改变文件路径等。
2.
实现映射接口
:创建一个类实现 Ant 提供的映射接口,并重写相应的映射方法。
3.
在构建文件中使用
:将自定义映射器应用到需要的任务中,如
<copy>
、
<move>
等任务。
以下是一个简单的流程说明:
graph LR
A[定义映射逻辑] --> B[实现映射接口]
B --> C[在构建文件中使用]
6. 创建自定义选择器
6.1 自定义选择器的作用
自定义选择器用于对文件集进行强大的过滤。在 Ant 构建中,可能需要根据特定的条件筛选文件,例如文件的修改时间、文件大小、文件类型等,自定义选择器可以实现这些复杂的筛选逻辑。
6.2 开发步骤
创建自定义选择器的步骤如下:
1.
确定筛选条件
:明确需要根据哪些条件筛选文件。
2.
实现选择器接口
:创建一个类实现 Ant 提供的选择器接口,并重写选择方法,根据筛选条件判断文件是否符合要求。
3.
在构建文件中使用
:将自定义选择器应用到
<fileset>
中,实现文件的筛选。
示例代码虽然未在文档中给出,但以下是一个简单的流程说明:
graph LR
A[确定筛选条件] --> B[实现选择器接口]
B --> C[在构建文件中使用]
7. 实现自定义过滤器
7.1 自定义过滤器的功能
自定义过滤器用于转换文本流。在 Ant 构建过程中,可能需要对文件的内容进行修改,例如替换某些字符串、添加特定的文本等,自定义过滤器可以实现这些文本转换功能。
7.2 开发步骤
实现自定义过滤器的步骤如下:
1.
确定转换规则
:明确需要对文本进行何种转换,例如替换字符串、添加前缀后缀等。
2.
实现过滤器接口
:创建一个类实现 Ant 提供的过滤器接口,并重写过滤方法,根据转换规则对文本进行处理。
3.
在构建文件中使用
:将自定义过滤器应用到需要的任务中,如
<copy>
、
<move>
等任务。
以下是一个简单的流程说明:
graph LR
A[确定转换规则] --> B[实现过滤器接口]
B --> C[在构建文件中使用]
8. 总结
Ant 具有丰富的可扩展性,为开发者提供了多种扩展和定制的方式:
-
脚本编写
:通过
<script>
任务支持多种脚本语言,可在构建文件内部扩展 Ant,但使用时需注意代码的复用性和错误处理。
-
监听器和记录器
:可监控和记录 Ant 的构建过程,开发者可以开发自定义监听器和记录器满足特定的监控和日志需求。
-
自定义映射器
:允许根据特定需求对文件进行映射转换。
-
自定义选择器
:实现强大的文件集过滤功能,根据复杂条件筛选文件。
-
自定义过滤器
:用于转换文本流,对文件内容进行修改。
开发者可以根据具体的项目需求,灵活运用这些扩展方式,实现更高效、灵活的项目构建。同时,在使用过程中要注意遵循 Ant 的规范和最佳实践,确保构建的稳定性和可维护性。
以下是 Ant 扩展方式的总结表格:
| 扩展方式 | 作用 | 使用场景 |
| — | — | — |
| 脚本编写 | 在构建文件内部扩展 Ant,调用任务和操作对象 | 临时需求、快速实现功能 |
| 监听器和记录器 | 监控和记录构建过程 | 调试、记录构建信息 |
| 自定义映射器 | 对文件进行映射转换 | 修改文件名、路径等 |
| 自定义选择器 | 筛选文件集 | 根据复杂条件筛选文件 |
| 自定义过滤器 | 转换文本流 | 修改文件内容 |
通过合理利用这些扩展方式,开发者可以充分发挥 Ant 的优势,提高项目构建的效率和质量。
超级会员免费看
793

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



