开发人员:J2EE 和开放源代码 适用于任务编写者的 Ant 1.6 利用 Ant 1.6 内部结构更改编写一个任务,甚至编写一个任务库。 在上一篇文章中,我重点介绍了如何使用 Ant 1.6 的某些新功能帮助您更好地控制或重用您的编译设置。本文将向您介绍 Ant 1.6 进行的内部更改,及您如何在编写任务甚至任务库时利用这些更改。 Ant 1.6 中的主要改进 Ant 1.6 中的主要更改之一是 Ant 已经支持 XML 名称空间。这不会对您的编译文件或任务的编写方式造成太大的影响,除非您在任务名称中一直使用冒号 — 即使在 XML 名称空间产生之前,在任务名称中使用冒号也是一个不良习惯,并受到禁止。Ant 的名称空间用法有时可能会使人感到困惑,因此本文的第一部分将尝试指出某些难点。 与 XML 名称空间支持紧密相关是 Ant 库(简称 antlib)的概念。您可以将自定义任务和类型划分到单个库中,并将其置于它们自己的 XML 名称空间中。您不必再为任务选择唯一的名称而感到担心。如果根据稍后介绍的命名惯例选择名称空间 URI,您甚至可以使 Ant 自动发现您的 antlib,您只需在编译文件中声明它们的名称空间。 如果要编写一个选择器或过滤器阅读器以便在 Ant 的任务中使用,则必须在编译文件中使用一个自定义格式,这样做将使您的选择器或阅读器在使用性方面不如 Ant 任务的内置选择器或阅读器方便。由于添加新条件需要更改 Ant 的某个核心类,因此编写自定义条件根本无法实现。 从 1.6 版开始,Ant 支持一种新的在任务中指定嵌套元素的方式。早期的 TaskContainer 接口会告诉 Ant,您的任务将支持任何的 Ant 任务,把它作为嵌套元素,同样,您现在可以使任务支持公共基类的任何子类或接口实现。为支持这种新的方法,许多现有的 Ant 任务已经过改进,因此您现在可以轻松地向 Ant 中添加条件。 例如:rsync 任务 作为运行示例,我们将编写一个任务,该任务在 rsync 命令行接口的顶部提供一个瘦层。该实现只用作示例,实际的任务肯定更为复杂并且对命令行参数、错误检查条件等对象提供更多的控制。 该任务将使用 rsync 将一个主目录分发到未知数目的从目录。由于不同的任务可能需要连接到相同的从目录,因此将从目录作为一个可以通过 Ant 的 id/refid 系统实现的数据类型。 一个简单的 slave 类可能类似于下面这样
public class Slave extends ProjectComponent { private String refid; private String hostAndDir; public Slave() {} public void setRefid(String id) { if (hostAndDir != null) { throw new BuildException("Can't mix hostanddir with refid"); } refid = id; } public void setHostanddir(String had) { if (refid != null) { throw new BuildException("Can't mix hostanddir with refid"); } hostAndDir = had; } public String getHostAndDir() { if (refid != null) { Slave s = (Slave) getProject().getReference(refid); return s.getHostAndDir(); } return hostAndDir; } } 一个非常简单的 rsync 任务可能为
public class Rsync extends Task { private Commandline cmd = new Commandline(); private File master; private ArrayList slaves = new ArrayList(); public Rsync() { cmd.setExecutable("rsync"); cmd.createArgument().setValue("-vauCz"); cmd.createArgument().setValue("--delete"); } public void setMaster(File f) { cmd.createArgument().setFile(f); } public void addSlave(Slave s) { slaves.add(s); } public void execute() { Iterator iter = slaves.iterator(); while (iter.hasNext()) { Slave s = (Slave) iter.next(); Commandline c = (Commandline) cmd.clone(); c.createArgument().setValue(s.getHostAndDir()); Execute exe = new Execute(new LogStreamHandler(this, Project.MSG_INFO, Project.MSG_WARN), null); exe.setCommandline(c.getCommandline()); try { exe.execute(); } catch (IOException e) { throw new BuildException(e, getLocation()); } } } } 要使该任务和类型对 Ant 可见,应在编译文件中使用
<typedef name="slave" classname="org.example.Slave"/> <taskdef name="deploy" classname="org.example.Rsync"/> 并确保 Ant 能够找到您的类。 如果要动态提供类路径,则必须确保 Ant 重用相同的类加载器,即使用类似如下所示的 type/taskdef 的 loaderref 属性
<typedef name="slave" classname="org.example.Slave" loaderref="deploy"> <classpath> <pathelement location="path-to-my.jar"/> </classpath> </typedef> <taskdef name="deploy" classname="org.example.Rsync" loaderref="deploy"/> 如果不采用此方法,Ant 将使用不同的类加载程序加载 Slave 类两次 — 这将导致稍后引发 ClassCastExceptions。 随后,您可以使用以下所示的任务
<slave hostanddir="slave1:/www" id="slave1"/> <slave hostanddir="slave2:/www" id="slave2"/> <deploy master="some-dir"> <slave refid="slave1"/> <slave hostanddir="slave3:/www"/> </deploy> 用户实际上并不需要知道该任务在暗中使用了 rsync,因此将其命名为 <deploy> 而不是 <rsync>。 XML 名称空间 现在,假设应用服务器提供者提供了一个 <deploy> 任务,用于要不重新启动服务器进程时将应用程序重新部署到应用服务器。 在 Ant 1.6 之前,如果要在同一编译文件内部使用两个任务,则必须将其中的某个任务重命名。从 Ant 1.6 开始,您可以按照 XML 名称空间[1] 限定任务和类型。在用于 Ant 时,XML 名称空间只由标识它的 URI 以及在 XML 文件中使用的前缀组成。 只需选择一个前缀和 URI,并进行如下设置:
<project ... xmlns:mylib="org.example"> <typedef name="slave" classname="org.example.Slave" uri="org.example"/> <taskdef name="deploy" classname="org.example.Rsync" uri="org.example"/> <mylib:slave hostanddir="slave1:/www" id="slave1"/> <mylib:slave hostanddir="slave2:/www" id="slave2"/> <mylib:deploy master="some-dir"> <mylib:slave refid="slave1"/> <mylib:slave hostanddir="slave3:/www"/> </mylib:deploy> </project> 此处,"mylib" 是前缀,"org.example" 是 URI。 您可以分别在每个元素的前缀与 URI 之间建立映射,但我觉得最方便的方法是在 <project> 元素中声明它们。唯一的例外是 Ant 库,它在同一编译进程的内部装配和使用(如下所示)。 您甚至可以在所需的任何级别更改默认名称空间(不带前缀的名称空间)以节省某些键入操作,例如:
<deploy master="some-dir" xmlns="org.example"> <slave refid="slave1"/> <slave hostanddir="slave3:/www"/> </deploy> 使用该方法时必须多加小心。如果过于频繁地更改默认名称空间,将使编译文件无法被读取。 有些事项需要加以注意:
Ant 的名称空间支持存在一些应引起注意的常见难点。 DynamicConfigurator 是一个接口,使任务编写者能够接受任意的嵌套元素和属性,而不必编写所需签名的方法。由于向后兼容性方面的原因,该接口无法更改以支持名称空间,Ant 将始终把限定名称(前缀加上元素名称)传递给元素创建方法。为满足更高级的需要,Ant 1.6.2 将包含一个支持名称空间的 DynamicConfiguratorNS 接口。 通过 Ant 的反映规则发现的所有元素均被 Ant 视为是父元素名称空间的一部分。正是由于此原因,需要您为
<mylib:deploy master="some-dir"> <mylib:slave refid="slave1"/> </mylib:deploy> 中的 slave 元素提供一个 "mylib" 前缀。但假设我们需要支持一个嵌套的 <dirset> 以便替换 <mylib:deploy> 中的 master 属性。我们将向该任务中添加
public void addDirset(org.apache.tools.ant.types.DirSet ds) { ... } 。由于 Ant 通过反映发现嵌套的 "dirset" 元素,并将其视为是该任务使用的同一名称空间的一部分,因此必须编写
<mylib:deploy master="some-dir"> <mylib:dirset dir="some-dir"/> </mylib:deploy> ,即使 <dirset> 已被定义为默认名称空间中的数据类型。更糟糕的是,Ant 添加了一些新反映规则(如下所示),使任务写入器使用
public void add(org.apache.tools.ant.types.DirSet ds) { ... } 代替 addDirset。该替换方法使该任务支持作为 DirSet 的子集的任何命名类型,将其作为嵌套元素。此处使用 <mylib:dirset> 将无效,这是因为 "org.example" 名称空间中的 "dirset" 没有 <typedef>,只允许使用了 Ant 的核心名称空间的 <dirset>。 为了更清楚了解该情况,Ant 1.6.2 允许您在使用 addDirSet 或 addConfiguredDirSet 时使用 <mylib:dirset> 或 <dirset>,这样任务编写者可以推荐使用非限定格式,并不必将任务使用的 add 方法机制公开给编译文件编写者。 仅当元素属性与元素位于同一名称空间(或没有任何前缀)时,Ant 才设置元素属性。Ant 会忽略不同名称空间的所有属性。这意味着,与元素不同,您可以在编译文件内部使用来自名称空间并且对 Ant 没有特殊含义的属性。 Ant 库 到目前为止,必须对每个 <deploy> 任务枚举所有 slave。即使您可以使用对其他位置定义的 <slave> 引用,这仍然是一个手动并且容易出错的任务。一个更好的方法是 slave 集合类型,该类型可用于在单个位置定义一个集合并在需要时使用对该集合的引用。 由于稍后要扩展该类型,因此我们使用接口来描述抽象 slave 集合类型
public interface SlaveCollection { Collection getSlaves(); } 并提供一个简单的列表实现
public class SlaveList extends ProjectComponent implements SlaveCollection { private String refid; private ArrayList slaves = new ArrayList(); public SlaveList() {} public void setRefid(String id) { if (slaves.size() > 0) { throw new BuildException("Can't mix nested slaves with refid"); } refid = id; } public void addSlave(Slave s) { if (refid != null) { throw new BuildException("Can't mix nested slaves with refid"); } slaves.add(s); } public Collection getSlaves() { if (refid != null) { SlaveCollection sc = (SlaveCollection) getProject().getReference(refid); return sc.getSlaves(); } return slaves; } } 然后,我们更改 Rsync 以包含以下代码:
public void addConfiguredSlaveList(SlaveList sc) { slaves.addAll(sc.getSlaves()); } 由于我们需要该列表在此刻完全起作用(否则 getSlaves 将始终返回一个空列表),因此我们用 "addConfigured" 代替 "add"。 将以上代码组合在一起
<project ... xmlns:mylib="org.example"> <typedef name="slave" classname="org.example.Slave" uri="org.example"/> <typedef name="slavelist" classname="org.example.SlaveList" uri="org.example"/> <taskdef name="deploy" classname="org.example.Rsync" uri="org.example"/> <mylib:slave hostanddir="slave1:/www" id="slave1"/> <mylib:slave hostanddir="slave2:/www" id="slave2"/> <mylib:slavelist id="some-dir-slaves"> <mylib:slave refid="slave1"/> <mylib:slave hostanddir="slave3:/www"/> </mylib:slavelist> <mylib:deploy master="some-dir"> <mylib:slavelist refid="some-dir-slaves"/> </mylib:deploy> </project> 并在顶部包含三个以上或三个以下的相同 type/taskdef。如果我们向 type/taskdef 的混合中添加 loaderref,则使 type/taskdef 脱离 sync 的可能性将更大。添加到 Ant 1.4 中的 type/taskdef 的资源或文件属性将使我们能够借助于属性文件使用单个 <typedef> 定义多个类型(在本示例中为 slave 和 slavelist),但我们仍必须将 <taskdef> 和 <typedef> 保留在 sync 中。 Ant 库提供了一个将相关任务和类型划分到库中的机制。Ant 库(或 antlib)使用一个简单的 XML 描述符描述它们的内容。根元素是 <antlib>,可以在它的内部使用几个 Ant 任务,最重要的是 <typedef> 和 <taskdef>。该示例将使用
<antlib> <typedef name="slave" classname="org.example.Slave"/> <typedef name="slavelist" classname="org.example.SlaveList"/> <taskdef name="deploy" classname="org.example.Rsync"/> </antlib> 作为描述符,并使用
<typedef file="our-descriptor.xml" uri="org.example"> <classpath> <pathelement="path-to-my.jar"/> </classpath> </typedef> 在编译文件内部的单个步骤中定义所有三个元素。这还将确保它们将最终将位于同一名称空间中,并由同一类加载程序加载。 对于 Ant 启动时可供 Ant 使用的类,我们不需要嵌套的 <classpath> 元素。对于这些类,如果我们对名称空间 URI 和描述符文件名遵循一个简单的命名惯例,则甚至可以完全忽略 <typedef>。 当 Ant 为以 "antlib:" 开头的名称空间 URI 找到一个名称空间声明时,它将把 URI 的余下部分作为 Java 程序包名处理并尝试从该程序包中加载一个名为 antlib.xml 的描述符作为资源,即对于名称空间 URI "antlib:org.example" ,Ant 将尝试从加载了 Ant 的类加载程序中加载 org/example/antlib.xml。 因此,将 rsync 任务和两个类型绑定到单个 jar 文件中时,我们将以上所示的描述符置于一个名为 antlib.xml 的文件中,并将其添加到 jar 文件(位于 org/example 目录的内部)中。这样,该编译文件将减小为
<project ... xmlns:mylib="antlib:org.example"> <mylib:slave hostanddir="slave1:/www" id="slave1"/> <mylib:slave hostanddir="slave2:/www" id="slave2"/> <mylib:slavelist id="some-dir-slaves"> <mylib:slave refid="slave1"/> <mylib:slave hostanddir="slave3:/www"/> </mylib:slavelist> <mylib:deploy master="some-dir"> <mylib:slavelist refid="some-dir-slaves"/> </mylib:deploy> </project> ,且该编译文件内部没有 <typedef>。 这意味着 Ant 库可以作为自包含的 jar 文件提供。任务编写者随后通知用户将其提供给 Ant(将它置于 Unix 上的 ANT_HOME/lib 或 $HOME/.ant/lib 中或置于 Windows 上的 %userprofile%\.ant\lib 中是最常见的选择),并只需简单地声明匹配名称空间即可开始使用它。 Ant 自身的内置任务和类型是 "antlib:org.apache.tools.ant" 名称空间的一部分,并象任何其他 antlib 一样被处理 - 唯一的差别是您不必声名该名称空间,Ant 将自动加载该描述符。 Ant 的可选任务证明:无法加载任务或类型其实并不重要。用户可能没有运行特殊可选任务所必须的库,但只要他从未尝试使用该任务,就不会出现问题。为在第三方 antlib 中支持这样的可选任务,向 <typedef> 中添加了一个新属性 onerror,该属性可用于在 Ant 无法加载特定类型或任务时通知 Ant 应继续运行。 除 <taskdef> 和 <typedef> 以外,在 antlib 描述符内部还可以使用 <macrodef> 和 <presetdef>。这意味着您可以将任务定义为宏或现有任务的变体而不必使用户看到该设计决策。在任务扩展了 Ant 中的给定基类的情况下,您甚至可以编写您自己的任务以便在描述符中使用。 如果编译了一个 antlib 并要在同一编译文件中使用它,则不应使用 Ant 的自动发现,因为这可能会使 Ant 加载一个旧版本的库并可能在以后导致类加载程序问题。这种情况下,不要在项目元素中声名名称空间映射,而是使用一个显式 <typedef> 加载一个可以使用的 antlib。 新反映规则 Ant 1.5 中的多态性 如果研究 SlaveList 示例,将发现 Ant 1.5 支持有限形式的多态性。SlaveList 中的 getSlaves 方法希望将项目引用作为 SlaveCollection 的实现并且不假设它是 SlaveList。如果添加其他类型的 <slavesfile>,以便从文件中读取所需的 slave 配置(忽略详细信息),则可以通过引用使用它:
<mylib:slavesfile file="some/file" id="from-file"/> <mylib:deploy master="some-dir"> <mylib:slavelist refid="from-file"/> </mylib:deploy> 此处,Ant 创建了一个只用于代理到 SlaveCollection 的不同实现的 slavelist 实例。如果要使用 Ant 的内置 <classfileset>,则必须采用同一方法,可以通过 <fileset> "调整" 它。 Ant 1.6 中的多态性 为了向多态性元素添加真正的支持,Ant 向其反映逻辑中添加了两个新的方法。
public void add(X); public void addConfigured(X); X 的任何子类(或实现,如果 X 是接口)可以用作嵌套元素,只要通过 <typedef> 或隐式加载的 antlib 将其定义为一个 Ant 类型。在某种意义上而言,这是 Ant 1.4 的 TaskContainer 接口的扩展,它已经普遍作为任意类型。 如果用以下代码替换 Rsync 中的 addSlave 和 addConfiguredSlaveList 方法
public void add(Slave s) { slaves.add(s); } public void addConfigured(SlaveCollection sc) { slaves.addAll(sc.getSlaves()); }
以上的所有示例仍可以正常工作 — 这是因为 <mylib:slave> 和 <mylib:slavelist> 定义为类型 — 只是 <mylib:deploy> 现在将接受 Slave 的任意命名的子类和 SlaveCollection 的实现作为嵌套元素。您现在可以直接编写以下代码
<mylib:deploy master="some-dir"> <mylib:slavesfile file="some/file" id="from-file"/> </mylib:deploy> 。尤其是,任何人可以编写他自己的 SlaveCollection 实现,对其进行 <typedef> 设置并将其嵌套到 <mylib:deploy> 中,而不必对 <deploy> 任务进行任何更改。 这不仅对于要提供其自身的插件点的新任务有用,而且还适用于 Ant 的核心。<condition> 任务的实现现在包含 signature add(Condition) 的方法,因此它立即支持可插入条件。支持嵌套 <selector> 或 <filterchain> 及其对可插入 Selector 或 FilterReader 丝线的支持也存在该情况。 确定当前月相是否是满月的自定义条件可能类似于如下所示
package org.example; import java.util.Calendar; import org.apache.tools.ant.util.DateUtils; import org.apache.tools.ant.taskdefs.condition.Condition; public class FullMoon implements Condition { public boolean eval() { return DateUtils.getPhaseOfMoon(Calendar.getInstance()) == 4; } } 和
<typedef name="fullmoon" classname="org.example.FullMoon"/> <condition property="full-moon" value="true"> <fullmoon/> </condition> <property name="full-moon" value="false"/> <echo>Today is full moon:${full-moon}</echo> 将通知您有关月相的信息。 与新反映规则紧密相关是 <typedef> 的 adapter 和 adaptto 属性。就像 TaskAdapter 支持将任意类用作 Ant 任务一样,只要它们提供相应签名的执行方法,您便可以为其他 Ant 接口或类型定义 adapter 类。如果要使类从不同于方法签名所需的基类进行扩展,或尽可能远地从 Ant 解除您自己实现的偶合,则使用该方法很有用。 例如,我们需要将条件用作文件选择器,我们可能包含某些只针对满月的文件 — 或更正式地说,依赖于运行 Ant 的操作系统。以下类将将任意 Condition 实现改为 FileSelectors:
public class ConditionToSelector implements TypeAdapter, FileSelector { private Project p; private Object realThing; public void setProject(Project p) { this.p = p; } public Project getProject() { return p; } public void setProxy(Object o) { realThing = o; } public Object getProxy() { return realThing; } public void checkProxyClass(Class proxyClass) { if (!Condition.class.isAssignableFrom(proxyClass)) { throw new BuildException(proxyClass + " is not a condition"); } } public boolean isSelected(File basedir, String filename, File file) { return ((Condition) getProxy()).eval(); } } 使用了
<typedef name="os-selector" classname="org.apache.tools.ant.taskdefs.condition.Os" adapter="org.example.ConditionToSelector"/> 然后,可以使用
<fileset dir="scripts"> <or> <and> <os-selector family="dos"/> <filename name="**/*.bat"/> </and> <and> <os-selector family="unix"/> <filename name="**/*.sh"/> </and> </or> </fileset> 来选择批处理文件或 shell 脚本,具体情况取决于当前操作系统。 结论 如果有一组相关任务和类型,则应考虑将其绑定在 Ant 库中。使用 XML 名称空间避免命名冲突并允许自动发现您的库。 如果要编写可能提供方便扩展点的任务,则使用新反映规则代替旧方法来定义嵌套元素。 脚注: Stefan Bodewig ( stefan.bodewig@freenet.de) 自 2000 年起一直研究 Ant,并且他是 Apache Ant、Gump 和 Jakarta 项目的项目管理委员会成员。在实际生活中他是德国科隆 BoST interactive 的一位高级软件开发人员。 |