Apache Ant学习笔记之一(转载)
Apache Ant学习笔记之一
©原作者:Matt Chapman
•Apache Ant源缘及目的
Apache Ant 是一个基于 Java 的生成(在软件开发中用来将源代码和其他输入文件转换为可执行文件的形式)工具。
最初的创始人 James Duncan Davidson
Ant=another neat tool
Ant 最初是 Tomcat 的一个内部组件
其第一个独立版本于 2000 年 7 月发布。
它是为了解决:
随着应用程序的生成过程变得更加复杂,确保在每次生成期间都使用精确相同的生成步骤,同时实现尽可能多的自动化,以便及时产生一致的生成版本,这就变得更加重要了。 |
•Ant工具概要
Ant 的生成文件是用 XML 编写的。每个生成文件由单个 project 元素组成,该元素又包含一个或多个 target 元素。
顶级 project 元素需要包含一个 default 属性,如果在 Ant 被调用时没有指定目标,这个属性将指定要执行的目标。
target是生成过程中已定义的一个步骤,它执行任意数量的操作。
如下是一个最简单的文件:
<?xml version="1.0"?> <target name="init" description="Initialize Argon database"> |
需要说明的是:
1.XML 声明指定了所使用的 XML 的版本,这不是当前的 Ant 所必需的
2.一次性打开和关闭一个元素是允许的,如:<target name="init"/>,当元素没有包含任何内容时,更简练的形式会更清晰。
3.XML 注释可以使用在整个生成文件中以提高清晰性。而且,Ant 定义了它自己的 description 元素和 description 属性,它们可用于提供更结构化的注释。
应该了解的一些基本概念
•属性
可以这样定义属性:
<property name="metal" value="beryllium"/> |
为了在生成文件的其他部分引用这个属性,要使用以下语法:
${metal} |
例如,为了使用这样一个值,它是另一个属性的值的组成部分,标签要写成下面这样:
<property name="metal-database" value="${metal}.db"/> |
属性分为预定义属性和自定义属性:
—预定义的属性:
运行 Ant 的所有系统属性,均可作为 Ant 属性使用,如:
${user.home}
—自定义属性:
${ant.version},这个属性包含 Ant 的版本;
${basedir},这个属性是项目目录的绝对路径(由包含生成文件的目录所定义,或者由 project 元素的可选 basedir 属性所定义)。
Ant 的 location 属性专门设计用于以平台无关的方式包含文件系统路径。您会像下面这样使用 location 来代替value:
<property name="database-file" location="archive/databases/${metal}.db"/> |
在可能的地方使用相对路径名称而不是绝对路径名称,这样还会更加灵活。
•定义依赖关系
使用 target 元素的 depends 属性来实现,每个目标的定义依据的是在它在能够执行之前必须完成的其他所有目标。如:
<target name="init"/> |
目标出现在生成文件中的顺序并不重要:执行顺序是由 depends 属性唯一确定的。
•使用命令行来运行Ant
如果你敲入Ant后回车,默认情况下Ant 寻找一个名为 build.xml 的文件来执行。
Ant 使用 -buildfile <file> 参数(-f <file> 是其简写形式)你可以指定要执行的生成文件。
-D选项用于设置随后可以在生成文件中使用的属性。
例如,为了将 name 属性设置为某个特定的值,您会使用一个类似下面这样的选项:-Dmetal=beryllium |
这个功能可用于覆盖生成文件中的初始属性设置。正如前面指出过的,属性的值一经设置就不能改变。-D 标志在读取生成文件中的任何信息之前设置某个属性;由于生成文件中的指派落在这个初始指派之后,因此它不会改变其值。
编写常用的target
Ant 的主要目标是生成 Java 应用程序,它能够内在地支持调用 javac 编译器以及其他 Java 相关任务。 |
•编译 Java 代码的任务的编写方式:
<javac srcdir="src"/> |
还支持的属性有:
destdir:将类文件放在一个单独的目标目录
classpath:等价于 javac 的 -classpath 选项。
debug="true":指示编译器应该带调试信息编译源文件。
javac 任务的一个重要特点在于,它仅编译那些它认为需要编译的源文件。如果某个类文件已经存在,并且对应的源文件自从该类文件生成以来还没有改变过,那么该源文件就不会被重新编译。
编写一个 clean 目标来从目标目录移除生成的任何类文件是个很好的习惯。如果想要确保所有源文件都已编译,就可以使用这个任务。
Ant 的 javac 任务的默认行为是调用运行 Ant 本身的任何 JVM 的标准编译器。然而,有时您可能想要单独地调用编译器 ―― 例如当你希望指定编译器的某些内存选项,或者需要使用一种不同级别的编译器的时候。为实现这个目的,只需将 javac 的 fork 属性设置为 true,比如像下面这样:
<javac srcdir="src" fork="true"/> |
如果想要指定一个不同的 javac 可执行文件,并向它传递一个最大内存设置,您可以像下面这样做:
<javac srcdir="src" fork="true" executable="d:\sdk141\bin\javac" memoryMaximumSize=" |
•创建 JAR 文件的任务的编写:
<jar destfile="package.jar" basedir="classes"/> |
这将创建一个名为 package.jar 的 JAR 文件,并把 classes 目录中的所有文件添加到其中(JAR 文件能够包含任意类型的文件,而不只是类文件)。此处没有指定清单文件,因此 Ant 将提供一个基本的清单文件。如:
<jar destfile="package.jar" basedir="classes"> |
•时间戳生成:
在生成环境中使用当前时间和日期,以某种方式标记某个生成任务的输出,以便记录它是何时生成的,这经常是可取的。这可能涉及编辑一个文件,以便插入一个字符串来指定日期和时间,或将这个信息合并到 JAR 或 zip 文件的文件名中。
这种需要是通过简单但是非常有用的 tstamp 任务来解决的。这个任务通常在某次生成过程开始时调用,比如在一个 init 目标中。这个任务不需要属性,许多情况下只需 <tstamp/> 就足够了。
tstamp 不产生任何输出;相反,它根据当前系统时间和日期设置 Ant 属性。下面是 tstamp 设置的一些属性、对每个属性的说明,以及这些属性可被设置到的值的例子:
属性 |
说明 |
例子 |
DSTAMP |
设置为当前日期,默认格式为yyyymmdd |
20031217 |
TSTAMP |
设置为当前时间,默认格式为 hhmm |
1603 |
TODAY |
设置为当前日期,带完整的月份 |
|
例如,在调用 tstamp 任务之后,我们能够根据日期命名该 JAR 文件,如下所示:
<jar destfile="package-${DSTAMP}.jar" basedir="classes"/> |
还可以配置 tstamp 任务来设置不同的属性,应用一个当前时间之前或之后的时间偏移,或以不同的方式格式化该字符串。所有这些都是使用一个嵌套的 format 元素来完成的,如下所示:
<tstamp> |
上面的清单将 OFFSET_TIME 属性设置为距离当前时间 10 分钟之后的小时数、分钟数和秒数。
用于定义格式字符串的字符与 java.text.SimpleDateFormat 类所定义的那些格式字符相同。
•创建和删除目录任务编写:
—创建目录:
<mkdir dir="archive/metals/zinc"/> |
首先要注意 / 被用作目录分隔符,这是 UNIX 和 Linux 的惯例。您可能认为这不是很平台无关的,但是 Ant 知道如何处理它,并针对它运行所在的平台做恰当的事情,这与我们在前面定义基于位置的属性时所看到的方式相同。我们能够同样容易地使用 \,而不管平台是什么 ―― Ant 能够处理任一种形式,甚至能够处理两种形式的混合。
mkdir 任务的另一个有用特性是它的如下能力:在父目录还不存在时创建它们。如果目标目录已经存在,mkdir 任务不会发出错误消息,而只是假设它的工作已经完成,从而什么也不做。
—删除目录同样也很容易:
<delete dir="archive/metals/zinc"/> |
这将删除指定的目录连同它包含的所有文件以及子目录。使用 file 属性而不是 dir 属性可以指定要删除的单个文件。
—复制和移动文件及目录:
<copy file="src/Test.java" tofile="src/TestCopy.java"/> |
—使用 move 来执行重命名操作而不是拷贝文件:
<move file="src/Test.java" tofile="src/TestCopy.java"/> |
—将文件复制或移动到另一个目录:
<copy file="src/Test.java" todir="archive"/> |
默认情况下,Ant 仅输出它执行的移动和复制操作的摘要,包括诸如已移动或复制的文件的数量等信息。如果想看到更详细的信息,包括涉及的文件名称等,您可以将 verbose 属性设置为true。
•创建和解压缩 zip 及 tar 文件:
<zip destfile="output.zip" basedir="output"/> |
相同的语法也可用于创建 tar 文件。 还可以使用 GZip 和 BZip 任务来压缩文件。例如:
<gzip src="/developerWorks/cn/education/java/j-apant/tutorial/output.tar" zipfile="output.tar.gz"/> |
解压缩和提取文件同样也很简单:
<unzip src="/developerWorks/cn/education/java/j-apant/tutorial/output.tar.gz" dest="extractDir"/> |
还可以包括 overwrite 属性来控制覆盖行为。默认设置是覆盖与正在被提取的归档文件中的条目相匹配的所有现有文件。相关的任务名称是 untar、unjar、gunzip 和 bunzip2。
•替换文件中的标记:
replace 任务,它执行文件中的查找和替换操作。token 属性指定要查找的字符串,value 属性指定一个新的字符串,查找到的标记字符串的所有实例都被替换为这个新的字符串。例如:
<replace file="input.txt" token="old" value="new"/> |
替换操作将在文件本身之内的适当位置进行。为了提供更详细的输出,可把 summary 属性设置为 true。这将导致该任务输出找到和替换的标记字符串实例的数目。
•模式匹配:
一次对一组文件执行那些操作经常是有用的 ―― 例如对给定目录中以 .java 结尾的所有文件执行操作。
这是使用通配符字符来完成的:*,它匹配零个或多个字符;以及 ?,它仅匹配一个字符。因而匹配以 .java 结尾的所有文件的模式不过就是 *.java。
也可以对目录执行模式匹配。例如,模式 src*/*.java 将匹配带 src 前缀的任何目录中的所有 Java 文件。 还有另一种模式结构:**,它匹配任意数量的目录。例如,模式 **/*.java 将匹配当前目录结构下的所有 Java 文件。
您能够以相当一致的方式对文件系统任务使用模式,比如嵌套的 fileset 元素。先前,我们使用这个任务来复制单个文件:
<copy file="src/Test.java" todir="archive"/> |
如果我们想要使用一个模式,可以将 file 属性替换为一个 fileset 元素,如下所示:
<copy todir="archive"> |
fileset 默认情况下包含指定 src 目录下的所有文件,因此为了仅选择 Java 文件,我们对模式使用一个 include 元素。类似地,我们可以对另一个模式添加一个 exclude 元素,从而潜在地排除include指定的匹配项。甚至可以指定多个include 和 exclude 元素;这样将得到一组文件和目录,它们包含 include 模式的所有匹配项的并集,但排除了 exclude 模式的所有匹配项。
注意还有一个通常很有用的文件集特性,但是对于没有意识到它的人来说,这个特性偶尔会产生混淆。这个特性称为 默认排除:即自动从文件集内容中排除的内置模式列表。该列表包括与名为 CVS 的目录相匹配的条目,以及以 ~ 字符结尾的文件,它们可能是备份文件。您通常不想在文件系统操作中包括这类文件和目录,因此排除这些文件是默认行为。然而,如果确实想无例外地选择 所有 文件和目录,可以将文件集的 defaultexcludes 属性设置为 no。
•使用选择器:
正如我们已经看到的,文件集用于指定一组文件,并且这个组的内容可以使用 include 和 exclude 模式来指定。也可以结合称为 选择器 的特殊元素使用include 和 exclude 来选择文件。下面是对 Ant 可用的核心选择器的列表:
—size:这个选择器用于根据文件的字节大小选择文件(除非使用 units 属性来指定了不同的单位)。when 属性用于设置比较的性质(less、more 或者 equal),value 属性定义每个文件将与之作比较的目标大小。
—contains:只有包含给定文本字符串(由text 属性指定)的文件才匹配这个选择器。默认情况下,查找操作是大小写敏感的;添加casesensitive="no" 可以改变默认设置。
—filename:name 属性指定文件名要与之匹配的模式。它本质上与 include 元素相同,以及与指定了negate="yes" 时的 exclude 元素相同。
—present:从当前目录结构中选择如下文件:它们与指定的 targetdir 目录中的文件具有相同的名称和相对目录结构。
—depend:这个选择器与 present 选择器具有相同的效果,只不过匹配的文件被限制到相对于 targetdir 位置中的对应文件来说,最近已修改过的那些文件。
—date:这个选择器基于其最后修改日期选择文件。when 属性指定作比较的性质是 before、after 还是 equal,datetime 属性指定与之作比较的日期和时间,这个日期和时间具有给定的固定格式 MM/DD/YYYY HH:MM AM_or_PM。注意 Windows 平台上有一个内置的 2 秒偏移,以允许底层文件系统的不精确性 ―― 这可能导致匹配的文件数量超过预期。允许的回旋时间量可以使用 granularity 属性来更改(以毫秒为单位来指定)。
—depth:这个选择器检查每个文件的目录结构层次数目。min 和/或 max 属性用于选择具有想要的目录层次数目的的文件。
还可以通过在一个选择器 容器 内嵌套一个或多个选择器来组合选择器。 最常用的选择器容器 and 仅选择它包含的所有选择器都选择了的文件。其他选择其容器包括 or、not、none 和 majority。
下面是一个文件集的例子,它仅选择那些大于 512 字节并且包含字符串“hello”的文件。
<fileset dir="dir"> |
•将生成文件链接起来:
有两种生成大型项目的不同方法。一种是让一个单一的生成文件做所有事情;另一种是让高级别的生成文件调用其它生成文件以执行特定任务,从而将生成过程划分为许多较小的部分。
使用 ant 任务来从一个 Ant 生成中调用另一个 Ant 生成是很容易的。在简单的情况下,您可以使用 antfile 属性,仅指定那些要使用的生成文件,Ant 将生成该生成文件中的默认目标。例如:
<ant antfile="sub-build.xml"/> |
在父生成文件中定义的任何属性默认将传递给子生成文件,虽然这可以通过指定 inheritAll="false"来避免。通过使用 property 元素来传入显式的属性也是可以做到的 ―― 即使将 inheritAll 设置为 false,这些属性也仍然适用于子生成文件。这个功能很适合用于给子生成文件传入参数。
让我们来考虑一个例子。下面是我们想要调用的一个生成文件:
<?xml version="1.0"?> |
(我们在前面还没有遇到过 echo 任务 ―― 它简单地输出给定的消息。)
下面是调用第一个生成文件的第二生成文件,它还给第一个生成文件传入 message 属性:
<?xml version="1.0"?> |
运行第二个生成文件所得到的输出如下:
Buildfile: build.xml |
•使用 CVS 知识库:
CVS 是 concurrent versions system(并发版本控制系统) 的缩写。它是一个源代码控制系统,设计用于跟踪许多不同开发人员做出的更改。它非常流行,在开放源代码项目中特别受欢迎。Ant 提供了与 CVS 的紧密集成。这对于自动化生成环境是非常有用的,因为单个生成文件也可以从源代码知识库中提取出一个或多个模块,生成项目,甚至基于自从前次执行生成以来所作的变更生成批处理文件。
注意,为了利用 Ant 中的 cvs 任务,您需要在机器上安装 cvs 命令,并使其从命令行可用。这个命令包括在大多数 Linux 发行套件中;它也以多种形式对 Windows 可用 ―― 例如作为宝贵的 Cygwin 环境的一部分。(请参阅参考资料以了解关于 Cygwin 的更多信息。)
下面是从 CVS 知识库提取模块的一个例子生成文件:
<?xml version="1.0"?> |
cvs 任务的主要属性是 cvsRoot,它是对 CVS 知识库的完整引用,包括连接方法和用户详细信息。这个参数的格式如下:
[:method:][[user][:password]@]hostname[:[port]]/path/to/repository |
在上面的例子中,我们作为匿名用户连接到 Eclipse 项目的中央知识库。然后其他属性指定了我们希望提取的模块以及放置提取文件的目的地。提取是 CVS 任务的默认操作;其他操作可通过使用 command 属性来指定。
请参阅参考资料以了解关于 CVS 的更多信息。
•创建自定义的任务:
为实现一个简单的自定义任务,我们所需做的就是扩展 org.apache.tools.ant.Task 类,并重写 execute() 方法。因此,作为这个文件排序自定义任务的框架,我们将编写如下代码:
import org.apache.tools.ant.BuildException; |
注意我们声明 execute() 方法抛出一个 BuildException 异常。如果这个任务出了任何错误,我们将抛出这个异常以便向 Ant 指出故障。
大多数任务,不管是核心任务还是自定义任务,都利用属性来控制它们的行为。对于这个简单任务,我们需要一个属性来指定要排序的文件,需要另一个属性来指定排序内容的输出。我们把这两个属性分别叫做 file 和 tofile。
Ant 使得支持自定义任务中的属性非常容易。为此,我们只需实现一个具有特别格式化的名称的方法,Ant 能够使用生成文件中指定的对应属性的值来调用这个方法。这个方法的名称需要是 set 加上属性的名称,因此在这个例子中,我们需要名为 setFile() 和 setTofile() 的方法。当 Ant 遇到生成文件中的一个属性设置时,它会寻找相关任务中具有适当名称的方法(称为 setter 方法)。
生成文件中的属性是作为字符串来指定的,因此我们的 setter 方法的参数可以是一个字符串。在这样的情况下,Ant 将在展开值所引用的任何属性之后,使用该属性的字符串值来调用我们的方法。但有时我们想把属性的值看作是一种不同的类型。这里的示例任务就是这种情况,其中的属性值引用文件系统上的文件,而不只是引用任意的字符串。可以通过将方法参数声明为 java.io.File 类型来容易地做到这点。Ant 将接受属性的字符串值,并把它解释为一个文件,然后传递给我们的方法。如果文件是使用相对路径名称来指定的,则会被转换为相对于项目基目录的绝对路径。Ant 能够对其他类型执行类似的转换,比如 boolean 和 int 类型。如果您提供具有相同名称但是具有不同参数的两个方法,Ant 将使用更明确的那一个方法,因此文件类型将优先于字符串类型。
这个自定义任务需要的两个 setter 方法类似如下:
// The setter for the "file" attribute |
现在我们能够完成这个简单的文件排序任务的实现:
import java.io.*; |
两个 setter 方法简单地对属性的值排序,以便这些值能够在 execute() 方法中使用。这里,输入文件被逐行地读入一个列表中,然后被排序并逐行地输出到输出文件。注意,为简单起见,我们很少执行错误检查 ―― 例如,我们甚至没有检查生成文件是否设置了必需的属性。不过我们的确至少捕捉了所执行的操作抛出的 I/O 异常,并将这些异常作为 BuildExceptions 重新抛出。
现在可以使用 javac 编译器或从某个 IDE 内编译这个自定义的任务。为了解决所使用的 Ant 类的引用问题,您需要把 ant.jar 文件的位置添加到 classpath 中。这个文件应该在 Ant 安装路径下的 lib 目录。
现在我们已经开发和编译了这个自定义的任务,下面可以从生成文件中利用它了。
在能够调用自定义的任务之前,我们需要给它指定一个名称来 定义 它,并告诉 Ant 关于实现这个任务的类文件的信息,以及定位该类文件所必需的任何 classpath 设置。这是使用 taskdef 任务来完成的,如下所示:
<taskdef name="filesorter" |
现在可以像使用 Ant 的核心任务一样使用这个自定义的任务了。下面是一个完整的生成文件,它显示了这个自定义任务的定义和用法:
<?xml version="1.0"?> |
现在在当前工作目录中创建一个 input.txt 文件来测试这个自定义的任务。例如:
Hello there |
下面是运行上面的生成文件之后产生的控制台输出:
Buildfile: build.xml |
注意 input.txt 的相对路径名称被转换成了当前目录中的一个绝对路径名称。这是因为我们将 setter 方法的参数指定为 java.io.File 类型而不是 java.lang.String 类型。
现在看一下这个任务实际是否能工作。这时应该已经在同一目录中创建了名为 output.txt 的文件,它包含以下内容:
And here is another one |
你可以尝试指定一个不存在的输入文件,以确定该任务是如何向 Ant 报告 “file not found”异常的。