四、 生成一个简单的 JAVA 项目
1. 简介
现在已经清楚了 Ant 生成文件的格式,并了解了如何定义属性和依赖关系以及如何运行 Ant ,下面可以开始为一个基本的 JAVA 项目构建一个生成环境了。这将包括学习用于编译源代码和组合 JAR 文件的 Ant 任务。
2. 编译源代码
由于 Ant 的主要目标是生成 Java 应用程序,它能够内在地、出色地支持调用 javac 编译器以及其他 Java 相关任务就毫不奇怪了。下面是编译 Java 代码的任务的编写方式:
<javac srcdir=”src”/>
这个标签需找 src 目录中以 .java 为扩展名的所有文件,并对它们调用 javac 编译器,从而在相同的目录中生成类文件。当然,将类文件放在一个单独的目录结构中通常会更清晰;可以通过添加 destdir 属性来让 Ant 做到这点。其它有用的属性包括:
l classpath :等价于 javac 的 -classpath 选项。
l debug= “ true “:指示编译器应该带调试信息编译源文件。
javac 任务的一个重要特点在于,它仅编译那些它认为需要编译的源文件。如果某个类文件已经存在,并且对应的源文件自从该类文件生成以来还没有改变过,那么该源文件就不会被重新编译。 Javac 任务的输出显示了实际被编译的源文件的数目。编写一个 clean 目标来从目标目录移除生成的任何类文件是个很好的习惯。如果想要确保所有源文件都已编译,就可以使用这个任务。这种行为刻画了 Ant 的许多任务的特点:如果某个任务能够确定所请求的操作不需要执行,那么该操作就会被跳过。
像 Ant 一样, javac 编译器本身也是用 Java 语言实现的。这对 Ant 中的 javac 任务的使用来说非常有利,因为它通常调用 Ant 运行所在的相同 Java 虚拟机 (JVM) 中的编译器类。在每次需要编译 Java 代码时,其它生成工具通常需要运行一个新的 javac 进程,从而需要一个新的 JVM 实例。但是在使用 Ant 的情况下,只需要单个 JVM 实例,它既用于运行 Ant 本身,也用于执行所有必需的编译任务 ( 以及其它相关任务,比如处理 JAR 文件 ) 。这是一种高效的多的资源使用方式,能够极大地缩短项目生成时间。
3. 编译器选项
正如前一小节看到的, Ant 的 javac 任务的默认行为是调用运行 Ant 本身的任何 JVM 的标准编译器。然而,有时可能想要单独地调用编译器 ---- 例如希望指定编译器的某些内存选项,或者需要使用一种不同级别的编译器的时候。为实现这个目的,只需将 javac 的 fork 属性设置为 true ,比如向下面这样:
<javac srcdir="src" fork="true"/>
如果想要指定一个不同的 javac 可执行文件,并向它传递一个最大内存设置,可以按照以下设置:
<javac srcdir="src" fork="true" executable="d:\sdk141\bin\javac"
memoryMaximumSize="128m"/>
甚至可以将 Ant 配置为使用某种不同的编译器。受支持的编译器包括开放源代码的 Jike 编译器和来自 GNU 编译器集 (GNU Compiler Collection,GCC) 的 GCT 编译器。可以通过两种方式指定这些编译器:可以设置 build.compiler 属性,这将应用于使用 javac 任务的所有场合;或根据需要设置每个 javac 任务中的 compiler 属性。
javac 任务还支持其他许多选项。请参考 Ant 手册以了解更多细节(请参阅参考资料)。
4. 创建 JAR 文件
在创建 Java 源文件之后,结果类文件通常被打包到一个 JAR 文件中,这个文件类似 zip 归档文件。每个 JAR 文件都包含一个清单文件,它可以指定该 JAR 文件的属性。
下面是 Ant 中 jar 任务的一个简单使用例子:
<jar destfile="package.jar" basedir="classes"/>
这将创建一个名为 package.jar 的 JAR 文件,并把 classes 目录中的所有文件添加到其中 (JAR 文件能够包含任意类型的文件,而不只是类文件 ) 。此处没有指定清单文件,因此 Ant 将提供一个基本的清单文件。
manifest 属性允许指定一个用作该 JAR 文件的清单的文件。清单文件的内容还可以使用 manifest 任务在生成文件中指定。这个任务能够像文件系统写入一个清单文件,或者能够实际嵌套在 jar 之内,以便一次性地创建清单文件和 JAR 文件。例如:
<jar destfile="package.jar" basedir="classes">
<manifest>
<attribute name="Built-By" value="${user.name}"/>
<attribute name="Main-class" value="package.Main"/>
</manifest>
</jar>
5. 时间戳生成
在生成环境中使用当前时间和日期,以某种方式标记某个生成任务的输出,以便记录它是何时生成的,这经常是可取的。这可能涉及编辑一个文件,以便插入一个字符串来指定日期和时间,或将这个信息合并到 JAR 或 zip 文件的文件名中。
这种需要是通过简单但是非常有用的 tstamp 任务来解决的。这个任务通常在某处生成过程开始时调用,比如在一个 init 目标中。这个任务不需要属性,许多情况下只需 <tstamp/> 就足够了。
Tstamp 不产生任何输出;相反,它根据当前系统时间和日期设置 Ant 属性。下面是 tstamp 设置的一些属性、对每个属性的说明,以及这些属性可被设置到的值的例子:
属性 | 说明 | 例子 |
DSTAMP | 设置为当前日期,默认格式为 yyyymmdd | 20100606 |
TSTAMP | 设置为当前时间,默认格式为 hhmm | 1600 |
TODAY | 设置为当前日期,带完整的月份 | 2010 年 6 月 6 日 |
例如,在前一小节中,我们按如下方式创建了一个 JAR 文件:
<jar destfile="package.jar" basedir="classes"/>
在调用 tstamp 任务之后,我们能够根据日期命名该 JAR 文件,如下所示:
<jar destfile="package-${DSTAMP}.jar" basedir="classes"/>
因此,如果这个任务在 2010 年 6 月 6 日调用,该 JAR 文件将被命名为 package-20100606.jar 。
还可以配置 tstamp 任务来设置不同的属性,应用一个当前时间之前或之后的时间偏移,或以不同的方式格式化该字符串。所有这些都是使用一个嵌套的 format 元素来完成的,如下所示:
<tstamp>
<format property="OFFSET_TIME"
pattern="HH:mm:ss"
offset="10" unit="minute"/>
</tstamp>
上面的清单将 OFFSET_TIME 属性设置为距离当前时间 10 分钟之后的小时数、分钟数和秒数。用于定义格式字符串的字符与 java.text.SimpleDateFormat 类所定义的那些格式字符相同。
6. 综合
前面几小节为我们提供了生成简单 Java 项目所需的足够知识。下面将把这些代码片段组合成一个完整的生成文件,它将编译 src 目录下的所有源代码,将结果类文件放在 build 目录下,然后把所有类文件打包到 dist 目录中的一个 JAR 文件中。要自己试验这个生成文件,所需要的就是包括一个或者多个 Java 源代码文件的 src 目录 --- 这个目录可以包含从简单的“ Hello world ”程序到来自某个现有项目的大量源文件的任何内容。如果需要向 Java classpath 添加 JAR 文件或其他任何内容,以便成功地编译元代码,只需在 javac 任务中为其添加一个 classpath 属性。
该生成文件看起来如下:
<?xml version="1.0"?>
<project default="dist" name="Project Argon">
<description>A simple Java project</description>
<property name="srcDir" location="src"/>
<property name="buildDir" location="build"/>
<property name="distDir" location="dist"/>
<target name="init">
<tstamp/>
<mkdir dir="${buildDir}"/>
<mkdir dir="${distDir}"/>
</target>
<target name="compile" depends="init">
<javac srcdir="${srcDir}" destdir="${buildDir}"/>
</target>
<target name="dist" depends="compile">
<jar destfile="${distDir}/package-${DSTAMP}.jar" basedir="${buildDir}">
<manifest>
<attribute name="Built-By" value="${user.name}"/>
<attribute name="Main-Class" value="package.Main"/>
</manifest>
</jar>
<jar destfile="${distDir}/package-src-${DSTAMP}.jar" basedir="${srcDir}"/>
</target>
<target name="clean">
<delete dir="${buildDir}"/>
<delete dir="${distDir}"/>
</target>
</project>
五、 文件系统的操作
1. 简介
我们了解了关于 Ant 的足够多的知识,现在能够生成一个基本的 Java 项目了,不过现实中的项目当然很少像我们的例子那么简单。在下面几节中,我们将考察 Ant 的许多附加功能中的一部分,以及能够使用它们的场合。
在本节中,我们将考察如何执行常见文件操作,比如创建目录和解压缩文件。 Ant 的优秀特性之一在于,执行这些操作的任务一般在所有的平台上都是相同的。
2. 创建和删除目录
最基本的文件系统操作之一就是创建目录或文件夹。做这项工作的任务名为 mkdir ,毫不奇怪,它非常类似于具有相同名称的 Windows 和 UNIX/Linux 命令。
<mkdir dir=”archive/metals/zinc”/>
首先要注意 / 被用作目录分隔符,这是 UNIX 和 Linux 的惯例。你可能认为这不是很平台无关的,但是 Ant 知道如何处理它,并针对它运行的平台做恰当的事情,这与我们在前面定义基于位置的属性时所看到的方式相同。我们能够同样容易地使用 \, 而不管平台是什么 ----Ant 能够处理任一种形式,甚至能够处理两种形式的混合。
Mkdir 任务的另一个有用特性是它的如下能力:在父目录还不存在时创建它们。考虑一下上面的清单,设想 archive 目录存在,但是 metals 目录不存在。如果使用底层平台的 mkdir 命令,你需要首先显式地创建 metals 目录,然后第二次调用 mkdir 命令创建 zinc 目录。但是 Ant 任务比这更加智能,它能够一次性创建这两个目录。类似地,如果目标目录已经存在, mkdir 任务不会发出错误消息,而只是假设它的工作已经完成,从而什么也不做。
删除目录同样也很容易:
<delete dir="archive/metals/zinc"/>
这将删除指定的目录连同它包含的所有文件以及子目录。使用 file 属性而不是 dir 属性可以指定要删除的单个文件。
3. 复制和移动文件及目录
在 Ant 中制作文件的一份拷贝很简单。例如:
<copy file="src/Test.java" tofile="src/TestCopy.java"/>
还可以使用 move 来执行重命名操作而不是拷贝文件:
<move file="src/Test.java" tofile="src/TestCopy.java"/>
另一个常用的文件系统操作是将文件复制或移动到另一个目录。做这项工作的 Ant 语法同样也很简单:
<copy file="src/Test.java" todir="archive"/>
<move file="src/Test.java" todir="archive"/>
默认情况下, Ant 仅输出它执行的移动和复制操作的摘要,包括诸如已移动或复制的文件的数量等信息。如果想看到更详细的信息,包括涉及的文件名称等,可以将 verbose 属性设置为 true 。
4. 创建和解压缩 zip 及 tar 文件
在前一节中,我们看到了如何创建 JAR 文件。创建其它归档文件的过程几乎完全相同。下面是创建 zip 文件的 Ant 任务。
<zip destfile="output.zip" basedir="output"/>
相同的语法也可用于创建 tar 文件。 还可以使用 GZip 和 BZip 任务来压缩文件。例如:
<gzip src="output.tar" zipfile="output.tar.gz"/>
解压缩和提取文件同样也很简单:
<unzip src="output.tar.gz" dest="extractDir"/>
还可以包括 overwrite 属性来控制覆盖行为。默认设置是覆盖与正在被提取的归档文件中的条目相匹配的所有现有文件。相关的任务名称是 untar 、 unjar 、 gunzip 和 bunzip2 。
5. 替换文件中的标记
我们将在本节考察的最后一个文件系统操作是 replace 任务,它执行文件中的查找和替换操作。 Token 属性指定要查找的字符串, value 属性指定一个新的字符串,查找到的标记字符串的所有实例都被替换为这个新的字符串。例如:
<replace file="input.txt" token="old" value="new"/>
替换操作将在文件本身之内的适当位置进行。为了提供更详细的输出,可把 summary 属性设置为 true 。这将导致该任务输出找到和替换的标记字符串实例的数目。
六、 其它有用的任务和技术
1. 简介
在考察自定义的任务之前,我们首先介绍一些还没遇到过的有用功能。 Ant 标准地附带了大量的功能,因此这里仅经挑选其中几个最有用的功能。模式匹配和文件选择器是功能强大的机制,它们极大地增强了我们已看到过的一些任务的功能;将生成任务链接起来以及与 cvs 知识库协同工作,是已发现的这些机制的两个主要实际应用领域。
2. 模式匹配
在前面考察文件系统任务时,我们仅使用了单独地命名的文件和目录然而,一次对一组文件执行那些操作经常是有用的 --- 例如对给定目录中以 .java 结尾的所有文件执行操作。正如等价的 DOS 和 UNIX 命令提供了这样的功能一样, Ant 也提供了这样的功能。这是使用通配符字符来完成的: * ,它匹配零个或多个字符;以及?,它及匹配一个字符。因此匹配以 .java 结尾的所有文件的模式不过就是 *.java 。
也可以对目录执行模式匹配。例如,模式 src*/*.java 将匹配带 src 前缀的任何目录中的所有 Java 文件。还有另一种模式结构: **, 它匹配任意数量的目录。例如,模式 **/*.java 将匹配当前目录结构下的所有 Java 文件。
你能够以相当一致的方式对文件系统任务使用模式,比如嵌套的 fileset 元素。先前,我们使用这个任务来复制单个文件:
<copy file="src/Test.java" todir="archive"/>
如果我们想要使用一个模式,可以将 file 属性替换为一个 fileset 元素,如下所示:
<copy todir="archive">
<fileset dir="src">
<include name="*.java"/>
</fileset>
</copy>
Fileset 默认情况下包含指定 src 目录下的所有文件,因此为了仅选择 Java 文件,我们对模式使用一个 include 元素。类似地,我们可以对另一个模式添加一个 exclude 元素,从而潜在地排除 include 指定的匹配项。甚至可以指定多个 include 和 exclude 元素;这样将得到一组文件和目录,它们包含 include 模式的所有匹配项的并集,但排除了 exclude 模式的所有的匹配项。
3. 使用选择器
正如我们已经看到的,文件集用于指定一组文件,并且这个组的内容可以使用 include 和 exclude 模式来指定。也可以结合称为选择器的特殊元素使用 include 和 exclude 来选择文件。下面是对 Ant 可用的核心选择器的列表:
l size :这个选择器用于根据文件的字节大小选择文件(除非使用 units 属性来指定了不同的单位)。 When 属性用于设置比较的性质 (less 、 more 或者 equal) , value 属性定义每个文件将与之作比较的目标大小。
l contains :只有包含给定文本字符串 ( 由 text 属性指定 ) 的文件才匹配这个选择器。默认情况下,查找操作是大小写敏感;添加 casesensitive=”no” 可以改变默认设置。
l filename : name 属性指定文件名要与之匹配的模式。它实质上与 include 元素相同,以及与指定了 nagate=”yes” 时的 exclude 元素相同。
l present :从当前目录结构中选择如下文件:它们与指定的 targetdir 目录中的文件具有相同的名称和相对目录结构
l depend :这个选择器与 present 选择器具有相同的效果,只不过匹配的文件被限制到相对