了解 Ant 1.6 的新特性以及它们如何影响您组织编译过程的方式。
虽然 Ant 版本的 1.5.x 系列在任务级方面有很大的改善,但它没有改变人们使用 Ant 的方式。而 Ant 1.6 却有所不同。它增加了几个新特性,以支持大型或非常复杂的编译情况。但是,要充分利用它们的功能,用户可能需要稍微调整它们的编译过程。
本文重点介绍了其中的三种新特性 — <macrodef>、<import>、<subant> 任务,表明使用它们可以有什么收获,以及它们如何影响您组织编译设置的方式。
宏
大多数编译工程师迟早会面临必须执行相同的任务组合但在几个地方配置稍微有点不同的情况。一个常见的例子是创建一个web 应用程序存档,对于开发系统、测试系统和生产系统有着不同的配置。
让我们假设 web 应用程序拥有依赖于目标系统的不同的 web 部署描述符,并为开发环境使用了一个不同的 JSP 集合以及一个不同的资料库集合。配置信息将放在属性中,创建 web 存档的任务看起来将类似于
<target name="war" depends="jar"> <war destfile="${war.name}" webxml="${web.xml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> </war> </target>
其中 support-libraries 是引用一个在其它位置定义的 <fileset> ,该引用指向您的应用程序所需的附加资料库的一个公共集合。
如果您只想一次创建一个 web 存档,那么您只需要正确地设置属性。比如说,您可以从一个您的目标专有的属性文件中加载它们。
利用 Ant 1.5 创建存档
现在,假定您想为测试系统和生产系统同时创建存档,以确保您真正为两个系统打包了相同的应用程序。利用 Ant 1.5,您可能使用 <antcall> 来调用拥有不同属性设置的 "war" 目标,类似:
<target name="production-wars"> <antcall target="war"> <param name="war.name" value="${staging.war.name}"/> <param name="web.xml" value="${staging.web.xml}"/> </antcall> <antcall target="war"> <param name="war.name" value="${production.war.name}"/> <param name="web.xml" value="${production.web.xml}"/> </antcall> </target>
当然,这假定两个目标系统都将使用相同的 jar 和 JSP。
但这种方法有一个主要缺点 — 就是速度慢。 <antcall> 重新分析编译文件,并为每一次调用重新运行调用的目标所依赖的所有目标。在上面的例子中,"jar" 目标将被运行两次。我们希望这对第二次调用没有影响,因为 "war" 目标依赖于它。
利用 Ant 1.6 创建存档
使用 Ant 1.6,您可以忘掉用 <antcall> 来实现宏的方法,相反您可以通过参数化现有的任务来创建一个新的任务。因而上面的例子将变为:
<macrodef name="makewar"> <attribute name="webxml"/> <attribute name="destfile"/> <sequential> <war destfile="@{destfile}" webxml="@{webxml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> </war> </sequential> </macrodef>
这定义了一个名称为 makewar 的任务,该任务可以和任何其它的任务一样使用。该任务有两个必需的属性,webxml 和 destfile。要使属性可选,我们必需在任务定义中提供一个默认值。这个示例假定 ${jar.name} 和 ${jsps} 在编译期间为常量,从而它们仍然作为属性指定。注意,属性在使用任务时展开而不是在定义宏的地方展开。
所用任务的特性几乎完全和属性一样,它们通过 @{} 而不是 ${} 展开。与属性不同,它们是可变的,也就是说,它们的值可以(并将)随着每一次调用而改变。它们也只在您的宏定义程序块内部可用。这意味着如果您的宏定义还包含了另一个定义了宏的任务,那么您内部的宏将看不到包含的宏的属性。
于是新的 production-wars 目标将类似于:
<target name="production-wars"> <makewar destfile="${staging.war.name}" webxml="${staging.web.xml}"/> <makewar destfile="${production.war.name}" webxml="${production.web.xml}"/> </target>
这个新的代码段不仅执行得快一些,而且也更易读,因为属性名称提供了更多的信息。
宏任务还可以定义嵌套的元素。<makewar> 定义中的 <war> 任务的嵌套 <fileset> 可以是这种嵌套元素的一种。可能开发目标需要一些额外的文件或想从不同的位置中挑选 JSP 或资源。以下代码段将一个可选的嵌套 <morefiles> 元素添加到了 <makewar> 任务中
<macrodef name="makewar"> <attribute name="webxml"/> <attribute name="destfile"/> <element name="morefiles" optional="true"/> <sequential> <war destfile="@{destfile}" webxml="@{webxml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> <morefiles/> </war> </sequential> </macrodef>
调用将类似于:
<makewar destfile="${development.war.name}" webxml="${development.web.xml}"> <morefiles> <fileset dir="${development.resources}"/> <lib refid="development-support-libraries"/> </morefiles> </makewar>
这就像 <morefiles> 的嵌套元素直接在 <war> 任务内部使用的效果一样。
即使迄今为止的示例仅显示了包装单个任务的 <macrodef>,但它不限于此。
下面的宏不仅将创建 web 存档,还将确保包含最终存档的目录在试图写入之前存在。在一个实际的编译文件中,您可能在调用任务之前使用一个设置目标来完成这个操作。
<macrodef name="makewar"> <attribute name="webxml"/> <attribute name="destfile"/> <element name="morefiles" optional="true"/> <sequential> <dirname property="@{destfile}.parent" file="@{destfile}"/> <mkdir dir="${@{destfile}.parent}"/> <war destfile="@{destfile}" webxml="@{webxml}"> <lib refid="support-libraries"/> <lib file="${jar.name}"/> <fileset dir="${jsps}"/> <morefiles/> </war> </sequential> </macrodef>
这里注意两件事情:
首先,特性在属性展开之前展开,因此结构 ${@{destfile}.parent} 将展开一个名称包含了 destfile 特性的值和 ".parent" 后缀的属性。这意味着您可以将特性展开嵌入到属性展开中,而不是将属性展开嵌入特性展开中。
其次,这个宏定义了属性,该属性的名称基于一个特性的值,因为 Ant 中的属性是全局的并且不可改变。第一次尝试使用
<dirname property="parent" file="@{destfile}"/>
相反将不会在 "production-wars" 目标中的第二次 <makewar> 调用产生期望的结果。第一次调用将定义一个新的名称为 parent 的属性,该属性指向父目录 ${staging.war.name}。第二次调用将查看这个属性但不会修改它的值。
预期 Ant 未来的版本将支持某些类型的限定范围的属性,这种属性只在宏执行期间定义。在此之前,使用特性的名称来构建属性名称是一种变通办法,潜在的副作用是要创建大量的属性。
提示: 如果您查看您的编译文件时发现使用了 <antcall> 代替宏,那么强烈建议您考虑使用 macrodef 将其转换成真正的宏。性能影响可能非常显著,并且还可能产生更易读和更易于维护的编译文件。 |
将一个编译文件分成多个文件有几个原因。
共享公用功能/在 Ant 1.6 之前包含文件
在 Ant 1.6 之前,您唯一的选择是实体包含的 XML 方法,类似于:
<!DOCTYPE project [ <!ENTITY common SYSTEM "file:./common.xml"> ]> <project name="test" default="test" basedir="."> <target name="setup"> ... </target> &common; ... </project>
摘自 Ant 常见问题解答。
这种方法有两个主要的缺点。您不能使用 Ant 属性指向您想包含的文件,因此被迫在您的编译文件中对位置进行硬编码。您想包含的文件只是一个 XML 文件的一部分,它可能没有一个根元素,因而使用支持 XML 的工具进行维护更加困难。
共享公用功能/使用 Ant 1.6 包含文件
Ant 1.6 自带了一个名称为 import 的新任务,您现在可以使用它。上面的示例将变为
<project name="test" default="test" basedir="."> <target name="setup"> ... </target> <import file="common.xml"/> ... </project>
因为它是一个任务,因此您可以使用 Ant 所有的特性来指定文件位置。主要的差异是被导入的文件本身必须是一个有效的 Ant 编译文件,因而必须有一个名称为 project 的根元素。如果您想从实体包含转换到导入,那么您必须在导入的文件的内容首尾放上 <project> 标记;然后 Ant 将在读取文件时再次划分它们。
注意文件名称由 Ant 任务根据编译文件的位置(而不是指定的基本目录)确定。如果您没有设置项目的 basedir 属性或将其设为 ".",那么您将不会注意到任何差异。如果您需要根据基本目录解析一个文件,那么您可以使用一个属性作为变通办法,类似于:
<property name="common.location" location="common.xml"/> <import file="${common.location}"/>
属性 common.location 将包含文件 common.xml 的绝对路径,并已根据导入项目的基本目录解析。
使用 Ant 1.6,所有的任务都可能放在目标之外或之内,除了两个例外。 <import> 一定不能嵌入到目标中, <antcall> 一定不能在目标外使用(否则它将创建一个无限循环)。
而 <import> 可做的不仅仅是导入另一个文件。
首先,它定义了名称为 ant.file.NAME 的特殊属性,其中 NAME 替换为每一个导入文件的 <project> 标记的名称属性。这个属性包含了导入文件的绝对路径,导入文件可用来根据它自己的位置(而不是导入文件的基本目录)定位文件和资源。
这意味着 <project> 的名称属性在 <import> 任务环境中变得更加重要。它还用来为在被导入的编译文件中定义的目标提供别名。如果导入了以下文件
<project name="share"> <target name="setup"> <mkdir dir="${dest}"/> </target> </project>
导入编译文件可以查看作为 "setup" 或 "share.setup" 的目标。后者在目标覆盖的上下文中变得非常重要。
让我们假定有一个包含了多个独立的组件(每个组件拥有它自己的编译文件)的编译系统。这些编译文件几乎相同,因此我们决定将公用功能转移到一个共享和已导入的文件中。为了简单起见,我们只介绍 Java 文件的编译和创建结果的一个 JAR 存档。共享的文件将类似于
<project name="share"> <target name="setup" depends="set-properties"> <mkdir dir="${dest}/classes"/> <mkdir dir="${dest}/lib"/> </target> <target name="compile" depends="setup"> <javac srcdir="${src}" destdir="${dest}/classes"> <classpath refid="compile-classpath"/> </javac> </target> <target name="jar" depends="compile"> <jar destfile="${dest}/lib/${jar.name}" basedir="${dest}/classes"/> </target> </project>
这个文件不会作为一个独立的 Ant 编译文件进行工作,因为它没有定义 "setup" 所依赖的 "set-properties" 目标。
组件 A 的编译文件可能类似于
<project name="A" default="jar"> <target name="set-properties"> <property name="dest" location="../dest/A"/> <property name="src" location="src"/> <property name="jar.name" value="module-A.jar"/> <path id="compile-classpath"/> </target> <import file="../share.xml"/> </project>
它仅设置适当的环境,然后将全部的编译逻辑交给被导入的文件负责。注意该编译文件创建了一个空的路径作为编译 CLASSPATH,因为它是自包含的。模块 B 依赖于 A,它的编译文件将类似于
<project name="B" default="jar"> <target name="set-properties"> <property name="dest" location="../dest/B"/> <property name="src" location="src"/> <property name="jar.name" value="module-B.jar"/> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> </target> <import file="../share.xml"/> </project>
您将注意到该编译文件与 A 的编译文件几乎一样,因此似乎有可能将大多数的 set-properties 目标也推送到 shared.xml 中。实际上,我们可以假定有一个对 dest 和 src 目标一致的命名惯例,以实现这一目的。
<project name="share"> <target name="set-properties"> <property name="dest" location="../dest/${ant.project.name}"/> <property name="src" location="src"/> <property name="jar.name" value="module-${ant.project.name}.jar"/> </target> ... contents of first example above ... </project>
ant.project.name 是一个内置的属性,它包含了最外面的 <project> 标记的名称属性的值。因此,如果模块 A 的编译文件导入了 share.xml,那么它将拥有值 A。
注意,所有的文件都与导入编译文件的基本目录相关,因此 scr 属性的实际值依赖于导入文件。
为此,A 的编译文件将简单地变为
<project name="A" default="jar"> <path id="compile-classpath"/> <import file="../share.xml"/> </project>
B 的编译文件将变为
<project name="B" default="jar"> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> <import file="../share.xml"/> </project>
现在假定 B 增加了一些 RMI 接口,需要在编译类之后但在创建 jar 之前运行 <rmic>。这就是目标覆盖能派上用场的地方。如果我们在导入编译文件中定义了一个目标,该目标与被导入的编译文件中的一个目标名称相同, 那么将使用导入编译文件中的目标。例如,B 可以使用:
<project name="B" default="jar"> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> <import file="../share.xml"/> <target name="compile" depends="setup"> <javac srcdir="${src}" destdir="${dest}/classes"> <classpath refid="compile-classpath"/> </javac> <rmic base="${dest}/classes" includes="**/Remote*.class"/> </target> </project>
在上面的示例中将使用 "compile" 目标,而不是 share.xml 中的目标;然而,不幸的是,这只是从共享那里复制 <javac> 任务。一种更好的解决方案是:
<project name="B" default="jar"> <path id="compile-classpath"> <pathelement location="../dest/A/module-A.jar"/> </path> <import file="../share.xml"/> <target name="compile" depends="share.compile"> <rmic base="${dest}/classes" includes="**/Remote*.class"/> </target> </project>
这只是使 B 的 "compile" 在原来的 "compile" 目标使用之后运行 <rmic>。
如果我们想在编译之前生成一些 Java 源代码(例如通过 XDoclet),我们可以使用类似下面的方法:
<import file="../share.xml"/> <target name="compile" depends="setup,xdoclet,share.compile"/> <target name="xdoclet"> .. details of XDoclet invocation omitted .. </target>
因此您可以完全覆盖一个目标或通过在原始目标之前或之后运行任务来增强它。
这里要注意一个危险。目标覆盖机制使导入编译文件依赖于在导入文件中使用的名称属性。如果任何人修改了导入文件的名称属性,那么导入编译文件将被破坏。Ant 开发社区目前正在讨论在 Ant 的一个未来的版本中为此提供一个解决方案。
提示: 如果您在编译文件中发现了非常常见的结构,那么值得尝试将文件重构为一个(一些)共享文件,并在必要时使用目标覆盖。这可以使您的编译系统更加一致,并让您能够重用编译逻辑。 |
在某种意义上,subant 是两种任务合二为一,因为它了解操作的两种模式。
如果您使用 <subant> 的 genericantfile 属性,那么它的工作方式和 <antcall> 一样,调用包含任务的同一个编译文件中的目标。与 <antcall> 不同,<subant> 获取目录的列表或集合,并将为每一个目录调用一次目标,以设定项目的基本目录。如果您想在任意数量的目录中执行完全一样的操作,那么这非常有用。
第二种模式不使用 genericantfile 属性,而获取一个编译文件的列表和集合进行迭代,以在每一个编译文件中调用目标。这种工作方式类似于在一个循环中使用 <ant> 任务。
第二种形式的典型情景是几个能够独立编译的模块的一个编译系统,但是该系统需要一个主编译文件来一次性编译所有的模块。
接下来的步骤 使用以下资源了解关于 Ant 的更多信息,并开始编译和部署 Java 项目。 Ant 业界趋势 阅读关于 JDeveloper 中的 Ant 集成的更多信息 下载 Oracle JDeveloper 10g 测试驱动:将 Ant 用于编译 Ant 入门第 1 部分 Ant 入门第 2 部分 在 Linux 上创建 Java 应用程序的命令行方法 阅读关于 Ant 的更多信息 相关文章与下载 |
在 Ant 1.6 之前构建主编译文件
在导入部分中讨论的例子使用了这样一个主编译文件。
<target name="build-all"> <ant dir="module-A" target="jar"/> <ant dir="module-B" target="jar"/> </target>
在 Ant 1.6 之前的 Ant 中。
使用 Ant 1.6 构建主编译文件
在 Ant 1.6 中使用 <subant>,这可以重写为
<target name="build-all"> <subant target="jar"> <filelist dir="."> <file name="module-A/build.xml"/> <file name="module-B/build.xml"/> </filelist> </subant> </target>
这看起来并没有很大的改善,因为您仍然必须单独指定每一个子编译文件。相反如果您转用 <fileset>,那么情况将有所改观。
<target name="build-all"> <subant target="jar"> <fileset dir="." includes="module-*/build.xml"/> </subant> </target>
这将自动发现所有模块的编译文件。如果您增加了一个模块 C,主编译文件中的目标不需要修改。
但小心。与 <filelist> 或 <path>(也被 <subant> 支持)不同,<fileset> 是无序的。在我们的例子中,模块 B 依赖于模块 A,因此我们需要确保首先编译模块 A,而使用 <fileset> 没有办法这么做。
如果编译完全彼此独立或者它们对于一个给定的操作彼此独立,那么 <fileset> 仍然有用。模块 B 的文档目标可能完全不依赖于模块 A,同样还有从您的 SCM 系统中更新源代码的目标。
如果您想将编译文件的自动发现与根据编译的相互依赖性对编译进行排序结合在一起,那么您将必须编写一个定 制的 Ant 任务。基本的想法是编写一个使用 <fileset> 的任务(让我们目前称之为 <buildlist>),确定依赖关系并计算 <subant> 必须使用的顺序。然后它创建一个以正确的顺序包含编译文件的 <path>,然后将对这个路径的一个引用放到项目中。调用将类似于
<target name="build-all"> <buildlist reference="my-build-path"> <fileset dir="." includes="module-*/build.xml"/> </buildlist> <subant target="jar"> <buildpath refid="my-build-path"/> </subant> </target>
这个假想的 buildlist 任务已经在 Ant 用户邮件列表和 bug 跟踪系统中进行了讨论。很有可能 Ant 的一个将来的版本中将包含这样的一个任务。
在 Ant 1.6 中已经增加了大量的新特性。这些新功能中的许多功能使得编译模板易于创建、构造和定制。特别是 <import> 和 <target> 进行了覆盖。<import>、<macrodef> 和 <subant> 特性很有可能使得 Ant 编译可高度重用。<scriptdef>(本文中未讨论)对于需要一些脚本但不想用 Java 编写定制任务的人而言可能非常有吸引力。
(转)http://www.oracle.com/technology/global/cn/pub/articles/bodewig_ant1.6.html