Java构建工具
在本节中,我们将介绍两种流行的基于 Java 的构建工具:Ant 和 Maven。 我们将讨论它们的特性,查看运行中的示例脚本,并概述每个工具的缺点。 让我们从历史最悠久的工具——Ant 开始。
Apache Ant
Apache Ant(另一个 Neat Tool)是一个用 Java 编写的开源构建工具。 其主要目的是为 Java 项目中所需的典型任务提供自动化,例如将源文件编译为类、运行单元测试、打包 JAR 文件以及创建 Javadoc 文档。 此外,它还为文件系统和归档操作提供了广泛的预定义任务。 如果这些任务中的任何一个不能满足您的要求,您可以使用用 Java 编写的新任务来扩展构建。
虽然 Ant 的核心是用 Java 编写的,但您的构建文件是通过 XML 表示的,这使得它可以跨不同的运行时环境移植。 Ant 不提供依赖项管理器,因此您需要自己管理外部依赖项。 然而,Ant 与另一个名为 Ivy 的 Apache 项目集成得很好,Ivy 是一个成熟的、独立的依赖关系管理器。 将 Ant 与 Ivy 集成需要额外的工作,并且必须为每个单独的项目手动完成。 让我们看一下示例构建脚本。
构建脚本术语
要理解任何 Ant 构建脚本,您需要从一些快速术语开始。 构建脚本由三个基本元素组成:项目、多个目标和使用的任务。 图 1.10 说明了每个元素之间的关系。

在 Ant 中,任务是一段可执行代码,例如,用于创建新目录或移动文件。 在构建脚本中,通过预定义的 XML 标记名称使用任务。 任务的行为可以通过其公开的属性进行配置。 以下代码片段显示了在构建脚本中使用 javac Ant 任务编译 Java 源代码的用法:
<!--源目录和目标目录由属性 srcdir 和 destdir 配置;
编译位于目录 src 中的 Java 源文件,并将类文件放入目录 dest 中-->
<javac srcdir="src" destdir="dest"/>
虽然 Ant 附带了广泛的预定义任务,但您可以通过用 Java 编写自己的任务来扩展构建脚本的功能。
目标是您想要执行的一组任务。 将其视为逻辑分组。 在命令行上运行 Ant 时,提供要执行的目标的名称。 通过声明目标之间的依赖关系,可以创建整个命令链。 以下代码片段显示了两个依赖目标:
<!--名为 init 的目标使用任务 mkdir 创建目录 build-->
<target name="init">
<mkdir dir="build"/>
</target>
<!--名为compile的目标,用于通过javac Ant任务编译Java源代码。
这个target依赖于target init,所以如果你在命令行运行它,init会先被执行。-->
<target name="compile" depends="init">
<javac srcdir="src" destdir="build"/>
</target>
所有 Ant 项目都必须拥有总体容器,即项目(project)。 它是 Ant 脚本中的顶级元素,包含一个或多个目标。 每个构建脚本只能定义一个项目。 以下代码片段显示了与目标相关的项目:
<!--项目包含一个或多个目标并定义可选属性(例如名称)来描述项目-->
<project name="example-build">
<target name="init">
<mkdir dir="build"/>
</target>
<target name="compile" depends="init">
<javac srcdir="src" destdir="build"/>
</target>
</project>
对 Ant 的层次结构有了基本的了解后,让我们看一下示例构建脚本的完整场景。
构建脚本示例
假设您想编写一个脚本,使用 Java 编译器编译 src 目录中的 Java 源代码,并将其放入输出目录 build 中。 您的 Java 源代码依赖于外部库 Apache Commons Lang 中的类。 您可以通过在类路径中引用库的 JAR 文件来告诉编译器这一点。 编译完代码后,您想要组装一个 JAR 文件。 每个工作单元、源代码编译和 JAR 汇编都将分组在一个单独的目标中。 您还将添加另外两个目标来初始化和清理所需的输出目录。 您将创建的 Ant 构建脚本的结构如图 1.11 所示。

我们开始谈正事吧。 现在是时候将该示例实现为 Ant 构建脚本了。 以下清单显示了整个项目以及实现您的目标所需的目标。
<project name="my-app" default="dist" basedir=".">
<!-- 为此构建设置全局属性-->
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="dist" location="dist"/>
<property name="version" value="1.0"/>
<target name="init">
<!-- 创建编译使用的构建目录结构 -->
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init" description="compile the source">
<!-- Compile the java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}" classpath="lib/commons-lang3-3.1.jar" includeantruntime="false"/>
</target>
<target name="dist" depends="compile" description="generate the distribution">
<!-- Create the distribution directory -->
<mkdir dir="${dist}"/>
<!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
<jar jarfile="${dist}/my-app-${version}.jar" basedir="${build}"/>
</target>
<target name="clean" description="clean up">
<!-- Delete the ${build} and ${dist} directory trees -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>
Ant 不会对如何定义构建的结构施加任何限制。 这使得它可以轻松适应现有的项目布局。 例如,示例脚本中的源目录和输出目录是任意选择的。 通过为其相应的属性设置不同的值来更改它们将非常容易。 目标定义也是如此; 您可以完全灵活地选择每个目标需要执行哪些逻辑以及执行顺序。
缺点
尽管具有所有这些灵活性,您还是应该意识到一些缺点:
-
与使用更简洁的定义语言的构建工具相比,使用 XML 作为构建逻辑的定义语言会导致构建脚本过大且冗长。
-
复杂的构建逻辑会导致构建脚本冗长且难以维护。 使用标记语言时,尝试定义条件逻辑(例如 if-then/if-then-else 语句)会成为一种负担。
-
Ant 不会为您提供任何有关如何设置项目的指南。 在企业环境中,这通常会导致构建文件每次看起来都不同。 常见功能经常被复制和粘贴。 项目中的每个新开发人员都需要了解构建的单独结构。
-
您想知道在一次构建中已编译了多少个类或已执行了多少个任务。 Ant 不会公开可让您在运行时查询有关内存中模型信息的 API。
-
在没有 Ivy 的情况下使用 Ant 会导致管理依赖项变得困难。 通常,您需要将 JAR 文件签入版本控制并手动管理其组织。
Apache Maven
在企业内的许多项目中使用 Ant 对可维护性有很大影响。 灵活性带来了许多重复的代码片段,这些代码片段从一个项目复制到另一个项目。 Maven 团队意识到需要标准化的项目布局和统一的构建生命周期。 Maven 采用了约定优于配置的思想,这意味着它为您的项目配置及其行为提供了合理的默认值。 项目自动知道在哪些目录中搜索源代码以及在运行构建时要执行哪些任务。 只要您的项目遵循默认值,您就可以使用几行 XML 设置完整的项目。 此外,Maven 还能够生成 HTML 项目文档,其中包括应用程序的 Javadoc。
Maven 的核心功能可以通过作为插件开发的自定义逻辑进行扩展。 社区非常活跃,您可以找到几乎涵盖构建支持各个方面的插件,从与其他开发工具的集成到报告。 如果没有适合您特定需求的插件,您可以编写自己的扩展。
标准目录布局
通过引入默认的项目布局,Maven 确保每一位了解 Maven 项目的开发人员都会立即知道在哪里需要特定的文件类型。 例如,Java 应用程序源代码位于目录 src/main/java 中。 所有默认目录都是可配置的。 图 1.12 说明了 Maven 项目的默认布局。

构建生命周期
Maven 基于构建生命周期的概念。每个项目都确切地知道要执行哪些步骤来构建、打包和分发应用程序,包括以下功能:
-
编译源代码
-
运行单元和集成测试
-
组装工件(例如,JAR 文件)
-
将工件部署到本地存储库
-
将工件发布到远程存储库
此构建生命周期中的每个步骤称为一个阶段。 各阶段按顺序执行。 您想要执行的阶段是在命令行上运行构建时定义的。 如果调用打包应用程序的阶段,Maven 会自动确定需要提前执行源代码编译、运行测试等依赖阶段。 图 1.13 显示了 Maven 构建的预定义阶段及其执行顺序。
依赖管理
在 Maven 项目中,对外部库的依赖关系在构建脚本中声明。 例如,如果您的项目需要流行的 Java 库 Hibernate,您只需在依赖项配置块中定义其唯一的工件坐标,例如组织、名称和版本。 以下代码片段显示了如何声明对版本 4.1.7 的依赖关系。 Hibernate 核心库的最终版本:

<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.7.Final</version>
</dependency>
</dependencies>
在运行时,声明的库及其传递依赖项由 Maven 的依赖项管理器下载,存储在本地缓存中以供以后重用,并可供您的构建使用(例如,用于编译源代码)。 Maven 预先配置使用存储库 Maven Central 来下载依赖项。 后续构建将重用本地缓存中的现有工件,因此不会联系 Maven Central。 Maven Central 是 Java 社区中最流行的二进制工件存储库。 图 1.14 演示了 Maven 的工件检索过程。
Maven 中的依赖管理不仅限于外部库。 您还可以声明对其他 Maven 项目的依赖关系。 如果您将软件分解为模块,这些模块是基于相关功能的较小组件,就会出现这种需求。 图 1.15 显示了传统三层模块化架构的示例。 在此示例中,表示层包含用于在网页中呈现数据的代码,业务层对现实生活中的业务对象进行建模,集成层从数据库中检索数据。


构建脚本示例
以下清单显示了一个名为 pom.xml 的示例 Maven 构建脚本,它将实现与 Ant 构建相同的功能。 请记住,您在此处遵循默认约定,因此 Maven 将在目录 src/main/java 而不是 src 中查找源代码。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
下一代构建工具的要求
在上一节中,我们研究了现有构建工具 Ant 和 Maven 的特性、优点和缺点。 很明显,您经常必须在支持的功能上做出妥协,选择其中之一。 要么选择完全的灵活性和可扩展性,但项目标准化较弱,有大量的样板代码,并且选择 Ant 时不支持依赖管理; 或者你选择 Maven,它提供了一种约定优于配置的方法和一个无缝集成的依赖管理器,但是过于严格的思维方式和繁琐的插件系统。
如果构建工具能够涵盖中间地带,那不是很好吗? 以下是进化的构建工具应提供的一些功能:
-
富有表现力、声明性和可维护的构建语言。
-
标准化的项目布局和生命周期,但具有充分的灵活性和完全配置默认值的选项。
-
易于使用且灵活的方式来实现自定义逻辑。
-
支持由多个项目组成的项目结构以构建可交付成果。
-
支持依赖性管理。
-
现有构建基础架构的良好集成和迁移,包括导入现有 Ant 构建脚本和工具以将现有 Ant/Maven 逻辑转换为自己的规则集的能力。
-
强调可扩展和高性能的构建。 如果您有长时间运行的构建(例如,两个小时或更长时间)(某些大型企业项目就是这种情况),这将很重要。
本书将向您介绍一个提供所有这些出色功能的工具:Gradle。 我们将一起讨论如何使用它并利用它提供的所有优势。
总结
没有项目自动化的开发人员和 QA 人员的生活是重复、乏味且容易出错的。 软件交付过程中的每一步(从源代码编译到打包软件,再到将可交付成果发布到测试和生产环境)都必须手动完成。 项目自动化有助于消除手动干预的负担,使您的团队更加高效,并引导实现按钮式、故障安全的软件发布流程。
在本章中,我们确定了不同类型的项目自动化——按需、计划和触发构建启动——并涵盖了它们的特定用例。 您了解到,不同类型的项目自动化并不排斥。 事实上,它们是相辅相成的。
构建工具是项目自动化的推动者之一。 它允许您声明在启动构建时要执行的有序规则集。 我们通过分析构建工具的结构来讨论构建工具的移动部件。 构建引擎(构建工具可执行文件)处理构建脚本中定义的规则集并将其转换为可执行任务。 每个任务可能需要输入数据才能完成其工作。 结果,生成了构建输出。 依赖项管理器是构建工具体系结构的一个可选组件,可让您声明对构建过程正常运行所需的外部库的引用。
通过深入研究两种流行的 Java 构建工具实现:Ant 和 Maven,我们看到了构建工具的具体化特征。 Ant 提供了一种非常灵活且通用的方式来定义构建逻辑,但不提供有关标准项目布局的指导,也不提供项目中反复重复的任务的合理默认值。 它还没有附带开箱即用的依赖项管理器,这需要您自己管理外部依赖项。 另一方面,Maven 通过支持项目的合理默认配置以及标准化的构建生命周期,遵循约定优于配置的范例。 外部库以及 Maven 项目之间的自动依赖关系管理是一项内置功能。 Maven 缺乏自定义逻辑的轻松扩展性以及对非常规项目布局和任务的支持。 您了解到,高级构建工具需要在灵活性和可配置约定之间找到中间立场,以支持现代软件项目的需求。
在下一章中,我们将确定 Gradle 如何融入这个等式。