56、Ant 任务开发与扩展全解析

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 的扩展能力,为项目开发提供有力支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值