内容


用 Gant 构建软件

结合 Groovy 和 Ant 更加轻松地实现灵活构建

Comments

开始之前

关于本教程

在本教程中,您将了解构建框架 Gant,它利用 Groovy 和 Apache Ant 产生了一个高度通用的中介物(medium),该中介物允许您在重用所有 Ant 功能的同时实现编程逻辑。

目标

本教程引导您逐步了解 Gant 的基础知识。您将学习如何:

  • 利用 Gant 灵活的特定于领域的语言(DSL)在构建中定义行为
  • 重用 Ant
  • 在构建中定义支持重用甚至主动的函数

学习完本教程后,您将理解使用 Gant 构建软件的好处,并开始在日常的 Java™ 开发中使用 Gant。

先决条件

为了充分利用本教程,您必须熟悉 Java 语法和在 Java 平台上进行面向对象开发的基本概念。此外,您还要熟悉 Ant。另外,我们特别推荐您阅读 Andrew Glover 编写的另一篇教程 “精通 Groovy”。

系统要求

要实践本教程中的代码,您需要成功安装以下两个程序之一:

您还需要安装 Groovy 和 Gant。本教程提供的文本包含有这些安装的详细说明。

本教程的推荐配置是:

  • 支持 Sun JDK 1.5.0_09(或更新的版本)或 IBM JDK 1.5.0 SR3,并带有 500MB 以上主内存的系统
  • 至少需要 20MB 磁盘空间,以安装所需的软件组件和示例

本教程中的说明和示例基于 Microsoft® Windows® 操作系统。 本教程所涉及的所有工具也适用于 Linux® 和 UNIX 系统。

Ant 面临的挑战

Ant 实际上是 Java 平台的标准构建解决方案。尽管可以选择 Maven 和 Maven 2 等解决方案,但是 Ant 拥有广泛的开发人员基础。JUnit、PMD、Tomcat 和许多其他开源甚至商业性工具都提供了 Ant 任务。然而,Ant 本身的缺陷可能会招来一些挑战。虽然 Ant 在做什么 方面表现得很出色,但在如何 做方面却表现得很一般,挑战正是源于此处。本节解释了 Ant 在哪些方面缺乏灵活性(将在下一节提到),而设计 Gant 正是为了提供这些灵活性。

缺乏灵活性

使用 Ant 处理简单的任务很容易,比如在一系列目录中删除文件。清单 1 中的简单目标在调用时循环地删除了一系列文件和目录:

清单 1. 简单的 Ant delete 目标
<target name="clean" description="deletes recursively through generated directories">
  <delete dir="${default.target.dir}" />
</target>

但是,您可能希望更加灵活地删除目录和文件。例如,您只需要删除存在时间超过 3 个小时的文件或目录。虽然 Ant 在使用过滤器方面提供了便利,可以帮助缩小特定文件类型的范围(如所有的 .class 文件),但对于不是基于文件的决策逻辑,它不一定能够恰当处理。

如果使用 Java 代码解决问题,实现这个目标只需要一些相当容易实现的逻辑。例如,清单 2 定义了一个删除任何超过规定时间的文件的方法:

清单 2. 删除超过规定时间的文件的 Java 方法
public static void deleteFiles(File directory, Date time){
 File[] files = directory.listFiles();
 for(int x = 0; x < files.length; x++){
  File ifl = files[x];
   if((ifl.isFile()) && (ifl.lastModified() < time.getTime())){
   ifl.delete();
  }
 }
}

在清单 2 中可以看到,只要理解 Java File API,这并不是特别难以实现。

但要将这个逻辑转换为 Ant 则不是一件容易的事情。好消息是 Ant 提供了几个选项,以实现更加类似于命令式的逻辑。至少有 3 个可用的高级选项,可以用来解决根据时间戳删除特定文件的问题:

  • 将逻辑直接添加到构建文件
  • 使用 Java 语言编写一个定制任务
  • 使用脚本语言在构建文件内部编写需要的逻辑

坏消息是每个选项都有一些缺点。

尝试将构建文件转变为 XML

将 Ant 的构建文件实现为 XML 文档。XML 能够很好地描述做什么,即声明式编程。但它不适合用于描述如何 做,即通常所说的命令式编程。不过在使用命令式编程时,许多编程人员都试图提供一些便捷的库,以帮助克服 XML 的缺陷。

如果您希望在 XML 中直接应对文件删除的挑战,可以尝试通过 Ant-Contrib 项目的 if 任务(参见 参考资料)向 Ant 直接添加条件逻辑,如清单 3 所示:

清单 3. 用 Ant-Contrib 添加条件逻辑 if
<if>
 <equals arg1="${file.name}" arg2="temp.txt" />
 <then>
   <delete file="${file.name}"/>
 </then>
 <else>
   <echo message="Doing nothing" />
 </else>
</if>

尽管这一特定任务(和相关的 XML)为条件逻辑提供了便利,但它非常冗长,而且不能解决问题的要点 — 轻松地确定文件的时间戳。并且循环占据了非常大的分量。事实上 Ant 并不支持开箱即用的循环。因此,必须使用另一个名为 foreach 的 Ant-Contrib 任务,如清单 4 所示:

清单 4. 用 Ant-Contrib 添加循环 foreach
<foreach param="file_var" target="delete_file">
 <path>
  <fileset dir="${src.dir}">
   <include name="**/*.class"/>
  </fileset>
 </path>
</foreach>

其实这也不算简单。

通过继承进行定制

使 Ant 更加灵活的另一种选择:通常可以使用 Java 代码创建一个定制 Ant 任务。这就是说,通过实现 execute 方法,可以从 Ant 的 Task 对象和代码扩展到模板模式。

虽然这个方法行得通(实际上也很有意思),但需要大量的工作,并且从测试到部署的整个生命周期非常耗时。如果想要进行某些更改,则很难在 Ant 的构建文件中实现;您必须修改 Java 源代码,然后进行重新编译和测试。开发社区已经在缩短生命周期方面取得了很大的成果,但仍然需要努力,因为定制(能够轻松地应对我前面提到的挑战)在另一种中介物中(Java 语言)。

动态的 Ant

最后这点也很重要。通常可以选择在 Ant 构建文件中实现删除逻辑,但必须借助各种便于使用脚本语言的任务,比如 Ruby、Jython 或 Groovy。

例如,使用 Groovy 提供的 groovy Ant 任务,可以将清单 5 中的代码添加到 Ant 构建文件:

清单 5. 使用 groovy Ant 任务
<target name="delete-files" depends="groovy-init">
 <groovy classpathref="build.classpath" >
 new File("${properties.delete_dir}").eachFileRecurse{ ifl ->
  if((ifl.isFile()) && (ifl.lastModified() < 
     Long.parseLong("${properties.delete_dir_time}"))){
	ifl.delete()
  }
 }
 </groovy>
</target>

这种方法是可行的,但有点累赘。不仅 Ant 构建文件包含了大量的 XML,现在(通过 groovy 任务)一种不同的中介物 — Groovy 代码 — 也添加到这个混合代码中。注意,处理围绕命令式代码的属性也会有点麻烦(获取解析 long 值所需要的日期 — 它实际上是一个 String,因为它在 Ant 中是一个属性)。

现在一切都很明白了:Ant 需要 灵活性。它希望能够轻松地实现条件逻辑、循环和扩展性,不过没能如愿。但是可以推断,问题不在 Ant 本身,而出在 XML 上。目前缺乏的是灵活地引入一个命令式编程范例 — 这就是说要以正常代码的方式利用 Ant 以任务的形式提供的所有东西,而非 XML。

Gant 可以提供帮助。

Gant:一个巨大的飞跃

Gant 是一个构建框架。它利用 Groovy 和 Ant 产生了高度通用的中介物,该中介物能够在重用 Ant 的所有功能 的同时使用编程逻辑。因此,您可以使用 Gant 轻松实现在软件组装期间执行有趣任务的逻辑。尽管 Gant 基本上是 Groovy,它很容易上手,前提是您需要稍微了解 Ant。实际上,Gant 使用非常类似于 Ant 目标的领域特定语言(Domain Specific Language,DSL)。这一节介绍您需要执行的安装,以在您的系统上使用 Gant。

安装 Groovy

要使用 Gant,必须安装 Groovy。按照下面的简单步骤安装 Groovy:

  1. 从 Groovy 的站点下载最新版本(撰写本该教程时为 1.5.4)的 Groovy(参见 参考资料)。
  2. 将归档文件解压到系统的任何位置(可以使用任何一种操作系统;Groovy 不能识别平台,因为它只是 Java 代码)。
  3. 将 Groovy 的 bin 目录添加到您的路径。
  4. 创建一个环境变量 GROOVY_HOME,它将指向 Groovy 所在的目录。

准备安装 Gant。

安装 Gant

下载最新版本的 Gant(撰写本教程时为 1.1.1)并解压到系统(参见 参考资料)。

在已创建的目录内有两个子文件夹:bin 和 lib。lib 文件夹包含一些 JAR 文件。其中一个是 gant JAR,它包含了 Gant 的核心内容。还有一个 ivy JAR 和一个 maven-ant JAR,它们包含了便捷的实用程序,这些实用程序分别利用 Ivy 进行依赖项管理,并使您能够在 Ant 中使用各种 Maven 特性(参见 参考资料)。

bin 目录至少包含 3 个脚本。最重要的脚本是 install.groovy。

现在运行 install.groovy 脚本。它会将 lib 目录里面的 3 个 JAR 文件和 2 个脚本复制到 Groovy 的安装目录下。现在 Groovy 的 bin 目录包含 2 个 Gant 脚本,而 Groovy 的 lib 目录包含有 gant、ivy 和 maven-ant JAR。

打开

Gant 现在已经是 Groovy 安装的一部分。打开一个 Groovy 控制台并在命令行输入 gant。这时您会看到一个类似于 Cannot open file build.gant 的错误消息。

开始使用 Gant

当然,要用 Gant 做比较有用的事情,必须构建一些 Java 代码。本节将通过一个简单的项目帮助您开始体验 Gant,该项目包含一些源文件和测试文件。

项目安装

在简单项目(本教程后面还会用到)上运行 JUnit 测试,并使用 PMD 进行源代码分析(参见 参考资料)。该项目的根项目目录下有一个 src 目录(用于存储源文件)和一个测试目录(用于存储测试文件)。lib 目录包含有一系列的 JAR 文件,这些文件在编译 JUnit 测试和运行 PMD 时要用到。

该项目不能自动编译源文件和测试文件、运行 JUnit 测试以及通过 PMD 分析代码库。尽管可以使用 Ant 实现这些操作的自动化,但必须结合 Gant 一起使用。在这个过程中,您将发现 Gant 使这些本来就容易的任务变得更加容易和灵活。

开始 下载 并解压归档文件。

Gant 的 DSL

一个 Gant 构建脚本就是一个 Groovy 脚本。通常将 Gant 构建文件命名为 build.gant(就像通常将 Ant 构建文件命名为 build.xml 一样)。

在项目的根目录创建一个名为 build.gant 的空文件。组装项目所需要的任务将添加到这里。但在进行这个操作之前,需要先花点时间帮助您理解 Gant。

从更高的层次来说,Gant 文件是一系列类似于清单 6 的 target 定义(虽然不局限于 target):

清单 6. Gant 的 target 语法
target(name:"description"){
 //do groovy code
}

每个 target 可以有一个 name 属性(比如 compileclean),其后是一个 description 属性。这个模式非常匹配 Ant 的 target 元素,该元素也有一个 name 属性和可以选择的 description 属性。例如,Ant 的模型(重温一下)如清单 7 所示:

清单 7. Ant <target/> 语法
<target name="compile" description="compiles source code">
   <!-- do ant stuff -->
</target>

可以看到,Gant 将分隔符 <> 替换为 {}。当然,最关键的是分隔符之间的内容。

能够在 Ant 构建文件中使用的所有 任务都可以用于 Gant 文件。您只需要通过 Ant. 调用引用任务,并确保该任务的代码(如果它是定制的,比如 PMD 任务)在类路径中。

例如,如果选择在 Gant 中使用 Ant 的 echo 任务,则能够轻松地调用该任务:

Ant.echo(message:"hello world")

注意模式是如何产生的,在 Gant 和 Ant 之间进行比较。Ant 的 XML 元素变成调用 Ant 对象的方法,而 XML 元素属性变成名称-值对。例如,Ant 的 echo 调用类似于:

<echo message="hello world"/>

比较 Ant 的 XML DSL 和 Gant 的 Groovy DSL 将得出一个简单的模式。如果以前用过 Groovy Builders(参见 参考资料),您马上会意识到它是处理 Gant 的核心。因此,如果使用的是嵌套 Ant 任务(一会儿就会看到),顶级 Gant 任务将创建遵循同一模型的闭包。

进行测试

现在已经进行了简单的 Ant 到 Gant 的任务转换,接下来要简单介绍一下 Gant 的用法。打开您最喜欢的编辑器(我最喜欢 Eclipse;参见 参考资料)。在 build.gant 文件,添加一个称为 echotarget,并在 target 的主体内添加一个 echo 调用:

target(echo:"my first gant target"){
 Ant.echo(message:"hello world")
}

保存文件,然后回到命令行并输入 gant echo。如果一切进展顺利,您会在命令行上看到一个简单的消息,类似:

[echo] hello world

虽然这称不上激动人心,但一旦理解这个模式,在 Gant 中使用各种 Ant 任务将变得十分简单。

运行中的 Gant

现在您已经掌握了关于 Gant 的基本知识,本节将把您带到当前项目软件构建的实质部分。首先要做的是为源代码和测试代码分别添加一个编译目标。

项目设置

为了用 Gant 编译 Java 代码,必须使用 Ant 的 javac 任务。此外,还要指定存储所编译的代码的位置。通常将生成的代码存储在项目的根目录下的临时目录。在本教程中,您将把所有的已编译代码放在一个称为 target 的目录下。您还将根据功能对该目录进行细分:把已编译代码(.class 文件)放到 target 目录下的 class 子目录。

我刚才描述的过程需要两个步骤。首先,必须创建 target/classes 目录,然后调用 javac。如果您曾处理过 Ant 构建文件,那么至少已经完成过一次了。为了增强您的记忆并提供一个模型,清单 8 展示了应该如何在 Ant 的 XML 文件中实现成功的编译步骤(注意,这并不代表代码本身被成功编译):

清单 8. 在 Ant 中编译任务
<target name="compile">
 <mkdir dir="target/classes" />
 <javac srcdir="src" destdir="target/classes" />
</target>

清单 8 中的 XML 使用了两个核心 Ant 任务:mkdirjavac。因此,要将这转换为 Gant,必须两次调用 Ant 对象。记得遵循在 开始使用 Gant 中学习的元素-属性到名称-值模式。

采用清单 8 中的 XML 并将其转换为 Gant 将产生如清单 9 所示的代码:

清单 9. Gant 编译步骤
target(compile:"compiles source code"){
 Ant.mkdir(dir:"target/classes")
 Ant.javac(srcdir:"src", destdir:"target/classes")
}

一切进展顺利 — 到目前为止还不是很复杂。但是,还有重构的空间吗?还可以改善这些代码吗?记住,您现在看到的不再是 XML — 它现在是代码。我敢打赌,如果您在普通的 Java 文件中看到这些内容,您先想到的可能是创建一个值为 target/classesString 常量。

项目属性

在 Ant 中,如果要创建一个可以在整个构建文件中引用的通用值,就必须使用 Ant 的 property 机制。Ant property 定义(重温一下)如下所示:

<property name="default.target.dir" value="target" />
<property name="classes.dir" value="${default.target.dir}/classes" />

例如,在本代码中,target 值现在可以引用为 default.target.dir。如果值被引用两次以上并可以更改,这将很方便,因为更改只能在一个位置发生。

Ant 属性也有不可修改 的一面:值一旦设置将不可再更改。这是一个有用的特性,使用 Gant 时要想到它。如果您善于观察的话,您可能会想到由于 Gant 文件是 Groovy 脚本,因此不需要再使用 Ant 的 property 机制,只使用脚本内的普通旧变量。这是对的!但记住,如果希望这些值保持不可更改的话,要将它们变为 final就像在 Java 代码中一样

相应地,您可以在 Gant 中将 target/classes 字符串替换为我称为 clzzdir 的常量:

清单 10. 将目录字符串替换为一个常量
final clzzdir = "target/classes"

target(compile:"compiles source code"){
 Ant.mkdir(dir:clzzdir)
 Ant.javac(srcdir:"src", destdir:clzzdir)
}

注意,清单 10 使 clzzdir 成为变量 final。记住,在 Gant 中的命名模式是不同的。例如,前一个 Ant property 定义中的 Ant 属性使用一个 . 名称模式(比如 default.target.dir)。在 Ant 使用这一模式是一种很有用的方法,它可以快速确定 XML 正文中的字符串实际上是一个属性。但 Gant 属于 Groovy,并且 Groovy 本身只是 Java 代码,因此 . 模式不起作用。但是您可以随意采用自己的模式(比如带下划线的变量:_clzzdir 等等)。

链接目标

Gant 可以使用的一个强大 Ant 属性是任务链接。任务链接是指在 Ant 中为任务之间创建依赖项 — 例如,在代码编译和测试编译之间创建依赖项。测试类在源代码上有一个隐式的依赖项。JUnit 测试通常在测试时导入代码,因此在编译任何 JUnit 测试之前必须先在测试中编译代码。

在 Ant 中,这通常通过使用一个 targetdepends 属性来完成;但在 Gant 中,可以用稍微不同的方式来实现。在 Gant 中,您可以通过 target 主体内部的 depends 调用来指定依赖项,而不是增加 target 调用本身。

使用 depends 子句非常容易。只需要在 target 主体内部引用它,并导入在执行其余内容之前必须调用的一个到多个(用逗号隔开)其他 target

target(name:"description"){
 depends(another, other)
 //do groovy code
}

就像在 Ant 中一样,当在 target 内部指定依赖项时,该依赖项为幂等(idempotent):即使被引用一次以上,该依赖项也只调用一次

但这是一个 Groovy 文件,而 Groovy 属于 Java,Java 代码又是一种编程语言(不是 XML)。这意味着有很大的灵活性:您也可以直接调用依赖项 — 即您可以根据需要直接调用其他 target。但要记住,直接调用 target 会取消幂等关系。

例如,清单 11 中的两个 target 通过一个直接调用链接起来:

清单 11. 链接 target
target(echo:"my first gant target"){
 echoAgain()
 Ant.echo(message:"hello world")
}

target(echoAgain:"my first gant target again"){
 Ant.echo(message:"hello world first!!")
}

注意 echo target 中的 echoAgain 调用。这意味着在 hello world 输出之前,hello world first!! 已经输出。自己试试看!

记住,构建脚本内的幂等性非常有用。我强烈推荐坚持使用 depends 子句,除非您有十足理由可以不使用它。

默认目标

Ant 支持默认目标— 当 Ant 调用时,如果在命令行上没有指定的目标,默认目标将被调用。如果 Ant 构建内最常用的目标为 test 目标,那么将其作为默认目标意味着不需要再通过命令行传入 test 标识符。

Gant 支持两个用于指定默认目标的机制。在创建目标时,您可以在 Gant 中使用 default 关键字,也可以在 Gant 脚本的正文内部使用 setDefaultTarget 调用。

在 Gant 中使用 default 关键字很简单。只需要创建一个名为 defaulttarget。然后,在该 target 的主体部分使用 depends 调用,从而强制产生默认 target。例如,该代码将 echo target 变为默认 target

target(default:"echo"){
 depends(echo)
}

另外,您也可以使用 setDefaultTarget 调用指定一个默认 target

setDefaultTarget(echo)

两个机制中可任选一个。但需要注意的是,使用 setDefaultTarget 时要求它在定义 target 之后出现。使用 default 关键字则没有这个限制。

链接测试编译

现在我有点过多地讨论了 Gant 的特性,接下来应该链接编译测试代码(依赖于 JUnit)与源代码之间的隐式依赖项。但在编译 JUnit 测试之前,记得将 JUnit JAR 文件放到类路径中。Ant 通过 path 元素能够很好地支持构建路径。因此,要使用 Gant,必须创建一个包含有 JUnit JAR(在 lib 目录内)的 path 元素。在后面,将针对 PMD 在该目录中使用其他 JAR,因此创建一个包含该目录所有内容的 path,如清单 12 所示:

清单 12. 创建一个 path
Ant.path(id:"build.cp"){ 
 fileset(dir:"lib", includes:"**/*.jar") 
}

接下来将要编译测试目录里面的代码。因为该代码使用 JUnit 并且导入了源代码目录里面的代码(您可能还记得,它被编译到 target/classes 目录),所有在测试代码编译期间两个逻辑单元必须在类路径上:来自编译步骤的 JUnit JAR 文件和类文件。

测试编译步骤首先需要依赖 compile 目标。然后,在 javac Ant 调用中,需要引用两个 path。即,清单 13 中的 build.cp 引用来自清单 12 的 path 以及已经生成的类文件的位置:

清单 13. 引用 path 和类文件位置
target("compile-test":"compiles test source code"){
 depends(compile)
 Ant.mkdir(dir:"target/test-classes")
 Ant.javac(srcdir:"test", destdir:"target/test-classes"){
  classpath(){
   path(refid:"build.cp")
   path(location:clzzdir)
  }
 }
}

注意,在本示例中目标的名称是一个 String,而不是一个字母。这是因为 - 字符的原因。在 Groovy 中,Map 里面的名称不能够包含 - 字符;然而,如果希望使用 -,可以通过把名称放置在 "" 之间,从而强制将它变成一个 String

同时还需要注意如何嵌入 javac Ant 调用:一个 classpath 调用后面跟着两个 path 引用。这是运行中的 Groovy Builders,它们工作得相当好。

当然,在这一点上,还有重构的空间。进一步将 target/test-classes String 抽取出来并将它替换成一个常变量。

向函数添加行为

因为(我不止一次提到)Gant 脚本属于 Groovy 脚本,并且 Groovy 是基本的 Java 代码,Gant 用户并不只限于在 target 内部定义行为。可以根据需要随意定义方法(或独立的函数)。这一节向您展示如何实现上述功能。

通过函数进行重构

由于 Gant 构建在高度动态和灵活的 Groovy 语言之上,这使它能在 Gant 构建文件中提供更多的选项,这是前所未有的。您可能很想知道是不是可以进行更多的重构,比之前我已经展示的还要多。您已经将通用的 String 分解为变量,对于一个普通的 Java 类,这很正常。但能否实现更多的重构?

再看一看编译 target,这时您会注意到它们使用的东西基本相同:它们都使用 Ant 的 javac 来编译代码。为何不将该代码转变为一个函数呢?— 如果这是一个普通的 Java 类您很可能会这样做。

当然,这些编译 target 之间有些不同。编译测试代码(依赖于 JUnit 和所产生的编译源代码)需要路径信息,比如查找 JUnit JAR 文件以及已编译的源代码类的位置。这就是编译代码添加 Ant classpath 结构的原因。因此,我们应该可以在需要的时候添加一些逻辑以使用路径结构。

在编写编译函数之前,必须先完成两件事情。第一,重命名初始的 path id,使它更能反映其所代表的内容 — 一个库集合。该代码将 id 更改为 libs

Ant.path(id:"libs"){
 fileset(dir:"lib", includes:"**/*.jar")
}

第二,创建另一个 path 对象,让它把库和在 target/classes 目录里面的已编译源代码(也称为 clzzdir,还记得吗?)结合起来:

Ant.path(id:"build.cp"){
 path(refid:"libs")
 path(location:clzzdir)
}

定义两个不同的 path 结构可以提供可观的灵活性。例如,当处理 PMD 时,就没有必要处理已生成的类 — 即库。因此,可以引用更加精确的 path 对象,它更能反映实际所需要的东西(而不像默认条件下考虑所有的东西)。

创建 Gant 函数

Groovy 允许在一个脚本文件内创建独立的函数。同样,在 Gant 文件内也可以这样做。创建一个允许重用的函数,这与我们开始进行面向对象编程时所学习到的东西保持一致。因为构建文件两次使用 Ant 的 javac,所以创建一个函数(称为 javac),它包含有:

  • 一个包含源文件的目录,作为第一参数。
  • 一个针对生成的类文件的目标目录,作为第二参数。
  • path 引用,作为可选参数。

注意,Groovy 允许可选参数。这就是说,您可以为参数指定一个默认的内联值,如清单 14 所示:

清单 14. 指定默认参数值
def javac(basedir, destdir, cpref=null){
 Ant.javac(srcdir:basedir, destdir:destdir){
  if(cpref != null){
   classpath(refid:cpref)
  }
 }
}

如果调用方不提供一个值,将使用默认值。如清单 14 看到的一样,如果 cpref 不为 null,它将被使用。

重构 target

现在 Gant 构建文件中已经有一个函数,这时可以重构 compile-testcompile 目标,以引用最新定义的 javac

清单 15. 指定默认参数值
target("compile-test":"compiles test source code"){
 depends(compile)
 Ant.mkdir(dir:tstclzzdir)
 javac("test", tstclzzdir, "build.cp")
}

记住,编译测试需要一个类路径;相应地,必须将引用传入到包含有库和已编译源文件的 path 结构。最后,编译源文件并不需要(在此处)任何库,因此不需要最后一个参数:

target(compile:"compiles source code"){
 Ant.mkdir(dir:clzzdir)
 javac("src", clzzdir)
}

您可以看到,在构建脚本内定义函数的能力是非常有用的。如果曾经深入研究过普通的 Ant,您就会了解到 Ant 有一个类似的特性:macros。但如果编写过 macro 的话,您肯定会发现 Gant 函数更加自然。

Gant 提供更好的逻辑

回顾本教程的 Ant 面临的挑战 小节中提到的挑战:将一些行为添加到构建,只删除超过某些时间戳的特定文件。本小节将展示如何使用 Gant 轻松编写这个逻辑。

闭包

正如 Gant 支持在构建脚本内创建独立的函数一样,它也支持创建闭包。闭包就像独立的函数一样,因为它们在本质上也是一级对象。它们可以像其他对象一样来回传递,更重要的是,它们可以延时调用。

可以通过本教程 Ant 面临的挑战 一节提到的编写删除逻辑来练习闭包的创建(记住,您也可以轻松创建一个函数)。该闭包采用可以浏览文件的目录和决定删除文件所依据的时间作为参数。因为在 Groovy 环境中工作,所以可以获得一些优势。您可以轻松地处理递归,并且可以利用 Groovy 与普通 Java 对象相关联的快捷键,如清单 16 所示:

清单 16. 使用 Groovy 闭包
def deleteAfter = { directory, time ->
 new File(directory).eachFileRecurse{ ifl ->
  if((ifl.isFile()) && (ifl.lastModified() < time.getTime())){
   ifl.delete()
  }
 }
}

清单 16 创建一个带有两个参数的闭包(名为 deleteAfter):一个浏览目录和一个时间。注意清单 16 如何将 directory 对象变为一个普通的 Java File 对象,然后在 base(是 directory 参数)下递归地获取每个文件和目录。

逻辑的其余部分应该与 Java 代码示例中的逻辑十分相似,我已经在前面 Ant 面临的挑战 一节介绍了后者(参见 清单 2),并解释了 Ant 有时很难构建简单的逻辑。相反,在 Gant 中则很轻松。

接下来创建一个使用该逻辑的新 target。清单 17 创建了一些仅删除超过一天的文件的逻辑:

清单 17. 文件删除逻辑
target(cleanDayOld:"cleans up dirs"){
 deleteAfter(clzzdir, (new Date() - 1))
}

尽管这一特定的逻辑有可能不适合于已生成的类文件,该示例还是有用的。Gant 能够轻松地支持创建逻辑。当想要创建逻辑时,使用 Java 这样的命令式语言通常更加容易 — 而不是 XML。并且通过 Gant,您能够便捷地编写该逻辑(并且 Groovy 的特性会使这更加容易)。

灵活地进行静态分析

现在已经用 Gant 脚本编译出代码,接下来将要开始评估已编译的代码。在这一节,将使用静态分析工具(类似于编译器)来检查源代码出现的问题,比如过长的名称和方法。

使用 PMD 检查 Ant

PMD 是一个开放源码静态代码分析库,它有超过 300 个可用于检查代码库的规则(参见 参考资料)。例如,PMD 可以检查不包含 {}if/else 条件。虽然这样的 Java 代码是完全有效的,但如果不小心的话,省略大括号会引起难以觉察的缺陷。

PMD 将它的所有规则归类为规则集。使用 PMD 时,需要告诉它将哪个规则集加载为 Ant 任务的一部分。但在进行这些操作之前,必须先加载 PMD 的 Ant 任务。

使用 PMD 与本教程已经讲到的其他操作有着根本的区别,因为必须通过 Ant 的 taskdef 任务加载 PMD 的二进制文件,从而使 Ant 能够在调用 PMD 时找到它。如果到现在您还不相信 Gant 能够处理一切 Ant 能够处理的事情(甚至更多),本文的示例将让您信服这一点。

例如,在一个普通的 Ant build.xml 文件中使用 PMD 会类似于清单 18 所示:

清单 18. 在 Ant 构建文件中使用 PMD
<target name="pmd">
 <mkdir dir="target/reports"/>
 <taskdef name="pmd" classname="net.sourceforge.pmd.ant.PMDTask" 
     classpathref="build.classpath"/>
 <pmd shortFilenames="true">
  <ruleset>rulesets/basic.xml</ruleset>
  <ruleset>rulesets/braces.xml</ruleset>
  <ruleset>rulesets/codesize.xml</ruleset>
  <ruleset>rulesets/coupling.xml</ruleset>
  <ruleset>rulesets/naming.xml</ruleset>
  <ruleset>rulesets/unusedcode.xml</ruleset>
  <ruleset>rulesets/design.xml</ruleset>
  <formatter type="html" toFile="target/reports/pmd_report.html" />
  <fileset dir="src">
   <include name="**/*.java"/>
  </fileset>
 </pmd>
</target>

在清单 18 中,您会注意到 target 开始时的 taskdef 调用使用一个类路径引用。这个引用对 Ant 的即插即用架构而言是必须的,这也是大量工具捆绑 Ant 任务的原因。另外需要注意的是,如果要使用多个 PMD 规则集,则必须按顺序逐个指定。

Gant 灵活使用 PMD

记住,Gant 属于 Ant,Gant 属于 Groovy,而 Groovy 属于 Java,因此将 PMD 包含到 Gant 的第一步是删除指定 ruleset 元素的长列表,而使用像 Collection 等更具编程特征的内容。

将下面的 PMD 规则集的 Groovy 列表添加到 Gant 构建文件。随意地添加其他规则集或从这个初始列表中选取:

final allrules = ["rulesets/naming.xml", "rulesets/braces.xml",
 "rulesets/basic.xml", "rulesets/unusedcode.xml",
 "rulesets/design.xml", "rulesets/coupling.xml"]

您将要利用编程迭代来遍历这个列表并将它传递到 PMD Ant 任务。在这个过程中,您将发现当在 Gant 中利用 PMD 时,Gant 实际上在使用 Ant。将清单 19 中的代码添加到 build.gant 文件:

清单 19. 在 build.gant 中使用 PMD
target(pmd:"runs static code analysis"){
 Ant.mkdir(dir:"target/reports")
 Ant.taskdef(name:"pmd", classname:"net.sourceforge.pmd.ant.PMDTask",
   classpathref:"libs")
 Ant.pmd(shortFilenames:"true"){
   allrules.each{ rfile ->
    ruleset(rfile)
   }
   formatter(type:"xml", tofile:"target/reports/pmd_report.xml")
   formatter(type:"html", tofile:"target/reports/pmd_report.html")
   fileset(dir:"src"){
     include(name:"**/*.java")
   }
  }  
}

在清单 19 中,看看 Gant 如何使用 Ant 的 taskdef 任务。另外,看看清单 19 如何使用 allrules 规则集集合实现更加紧凑的编码结构。并且要注意所有这些与 Groovy 的 Builder 逻辑之间是如何协调工作的 — 我认为编写这类任务非常有趣,并且简单很多。

出色的灵活性

到目前为止,您已经看到,当向构建文件输入编程范例以后,就可以做些非常有趣的事情,比如利用逻辑删除特定文件、自然地遍历集合以及为重用创建函数。这一节将展示输入编程范例也可以使您轻松地为构建添加一定程度的主动性。

前摄地使用 PMD

现在提出一个问题:对于构建生成的报告,您共查看了多少次?例如,在前面一节,您添加了生成 PMD 报告的功能。您很可能会查看一次 这个报告。不可思议,但这是真的,是吗?

不管您查看了多少遍 PMD 报告,在您查看的时候通常已经为时已晚。这就是说,该完成的都完成了:代码已经编写完成并且可能已经签入到代码库。如果构建更具主动性的话,会不会更高效呢?— 即如果它能快速地 告诉您代码出现了问题。

为构建添加一定程度的主动性非常简单。像 PMD 这样的工具能够生成报告,因此您只需要检查所生成的报告的值;如果该值不符合预期,您可以采取适当的操作,比如使构建失败(或发送电子邮件消息等等)。

用 Gant 进行主动性构建

PMD 的 XML 文件的逻辑性很强。它包含了一个 file 元素列表,并且每个 file 元素包含一个 violation 元素列表。现在您将构建一个便捷函数,用于读取 PMD 报告 XML 文件并检查每个 violation 元素的 priority 属性。如果 priority 等于某些阈值,则意味着构建失败。

如果您从来没有接触过 Groovy 的 XmlSlurper,它将让您耳目一新。请看看它是如何轻松地解析 XML 文档的。

将清单 20 中的代码添加到 build.gant 文件:

清单 20. 解析 PMD 的 XML
def threshold(pmdfile, thresh){
 def root = new XmlSlurper().parse(new File(pmdfile))
 root.file.each{ fl ->
  fl.violation.each{ vio ->
   if(Integer.parseInt(vio.@priority.text()) == thresh){
    Ant.fail(message:"priority was ${vio.@priority.text()}")
   }
  }
 }
}

清单 20 出现了很多变化。首先,XmlSlurper 对象解析 PMD 文件并向该 XML 的根返回一个引用。其次,遍历 file 元素集合,对于 file 元素内的每个 violationpriority 属性将与传入的 thresh 值(代表一个阈值)进行比较。如果传入的阈值等于任何 priority 属性,将通过 Ant 的 fail 任务使构建失败。这在主动性构建中会是什么情况呢?

当然,要使用这个新定义的函数,您必须先从 target 引用它。将它添加到前面已经构建的 PMD target(参见 清单 18)。只需要将下面的一行代码添加到底部:

threshold("target/reports/pmd_report.xml", 1)

如果 PMD 在代码库发现任何 priority 1 违背,都意味着构建将要失败,因此它会快速地通知您有可能出现问题。

想一想通过 XML 在普通 Ant 中获得相同的特性是多么了不起的事情。

结束语

如您所见,Gant 为构建软件提供了前所未有的灵活性 — 这种灵活性是普通的 Ant XML 所没有的。同时,Gant 也充分地利用了 Ant — 能够在 Ant 实现的东西都可以在 Gant 中实现。另外,Gant 还可以利用 Groovy,它实现了这种新的灵活性。

有 Groovy 在手,您可以在 Gant 文件中创建支持重用的函数,同时还可以创建闭包以获得更多的灵活性。此外,您还可以实现一些非常吸引人的功能,比如轻松地为构建添加高度的主动性 —— 这是 XML 无法比拟的。

与普通旧式 Ant XML 构建文件相比,Ant 和 Groovy 结合起来可以提供更加吸引人的替代方案。例如,如果需要条件逻辑,您应该更多地考虑使用 Gant。正如我们所见,Gant 使用起来非常方便并且有趣。


下载资源


相关主题

  • Podcast: Scott Davis on Groovy(JavaWorld,2007 年 11 月):developerWorks 的定期撰稿人 Scott Davis 讨论了针对 Java 平台的 Groovy 以及其他动态语言,包括 JRuby 和 Jython。
  • Ant-Contrib Tasks:Ant-Contrib 项目是 Apache Ant 的一个任务集。
  • MavenIvy:Gant 包含了利用这些 Apache 工具的设备。
  • 实战 Groovy(Andrew Glover et al.,developerWorks,从 2004 年 8 月到 2006 年 9 月):超越基础知识,开始学习 Groovy Builders 和模板、使用 Groovy 编写 Ant 脚本、服务器端的 Groovy、熟练的操作、Groovy 的元对象协议(Meta Object Protocol)等等。
  • 用 PMD 铲除 bug”(Elliotte Rusty Harold,developerWorks,2005 年 1 月):更多地了解在本教程示例中使用的 PMD 静态分析工具。
  • 实战 Groovy: 用 Groovy 生成器作标记”(Andrew Glover,developerWorks,2005 年 4 月)Groovy Builders 使您可以通过类似于 Swing 的框架模拟标记语言,比如 XML、HTML、Ant 任务,甚至 GUI。
  • The Disco Blog's Groovy articles(Andrew Glover,thediscoblog.com):阅读关于 Groovy 的各种主题,比如单元测试、元编程以及 Groovy 技术。
  • 让开发自动化: 使用 Raven 构建 Java 项目”(Paul Duvall,developerWorks,2007 年 11 月):介绍一种新的构建工具,它支持比 XML 更具表现力的范例。自动化专家 Paul Duvall 描述了 Raven(一个构建在 Ruby 上的构建平台)如何利用一种提供了丰富特性的编程语言,并且以构建为中心的 DSL 提供了简单性。
  • Sun JDK 1.5 或更高版本:要实践本教程中的示例,至少需要 1.5.0_09 版本。
  • IBM Developer Kit for Java technology 1.5.0 SR3:另外,您还可以使用 IBM Java SDK 来实践本教程中的示例。
  • Eclipse IDE:使用 Eclipse Groovy Plugin 轻松设置 Groovy 开发环境。
  • Gant:下载 Gant。
  • developerWorks Java 技术专区:可以找到数百篇关于 Java 编程的各个方面的文章。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=312429
ArticleTitle=用 Gant 构建软件
publish-date=06102008