Gradle-精要-全-
Gradle 精要(全)
原文:
zh.annas-archive.org/md5/0bc9e13226efd87ab42341dd译者:飞龙
前言
当我在 2011 年第一次接触到 Gradle 时,它是一个年轻但功能强大的工具。如果我记得正确,版本是 0.9。尽管 Gradle 有足够的官方文档,但我仍然很难开始。我最缺少的是一本能帮助我首先理解核心概念的指南,而不必阅读整个文档。
Gradle 是一个出色的构建工具。关于它的学习内容非常多,新用户往往不知道从哪里开始。期望一个应用程序开发者阅读整个 Gradle 参考材料以了解基础知识是不明智的。
本书试图通过逐步揭示关键概念来帮助读者开始使用 Gradle。它简洁地介绍了更高级的主题。本书专注于 Gradle 的实际应用,读者可以立即将其应用于自己的项目。本书力求忠实于“精华”的精神,避免深入探讨 Gradle 提供的每一个可能的功能和选项。为了避免分散对应用程序逻辑的注意力,应用程序的代码示例被有意保持非常小。
这本书是 Gradle 的快速入门指南。如果你已经是一名使用 Ant 或 Maven 构建代码的 Java 开发者,并希望切换到 Gradle,这本书将帮助你快速理解 Gradle 的不同概念。即使你没有接触过其他构建工具,如 Ant 或 Maven,你也可以在这本书的帮助下从头开始学习 Gradle。它从 Gradle 的基础知识开始,然后逐步介绍多模块项目、迁移策略、测试策略、持续集成以及使用 Gradle 进行代码覆盖率的概念。
本书涵盖内容
本书可以大致分为三个部分。
第一部分包括第一章运行您的第一个 Gradle 任务,第二章构建 Java 项目,以及第三章构建 Web 应用程序。本部分通过非常简单的示例介绍了 Gradle 的基础知识,帮助读者创建 Java 项目和 Web 应用程序的构建文件。它以温和的方式开始,不涉及任何复杂的概念。
第二部分包括第四章, 揭秘构建脚本,以及第五章, 多项目构建。本节帮助读者更深入地理解 Gradle 的基础,同时仍保持本书的“基本”方面。它还帮助读者理解如何解释和编写符合 Gradle DSL 的脚本。
第三部分包括第六章, 使用 Gradle 的实战项目,第七章, 使用 Gradle 进行测试和报告,第八章, 组织构建逻辑和插件,以及第九章, 多语言项目。本节涵盖了 Gradle 用户可能遇到的更多实际用例。一些例子包括从现有的构建系统迁移到 Gradle,在 CI 服务器上使用 Gradle,使用 Gradle 维护代码质量,使用 Gradle 构建 Groovy 和 Scala 等项目语言等。这些概念大多围绕各种插件能提供什么,同时也允许读者创建自己的自定义插件。
此外,在所有章节的多个地方,读者都可以找到提示、参考资料和其他信息性注释。
第一章, 运行您的第一个 Gradle 任务,从 Gradle 及其安装的介绍开始,随后转向探索 Gradle 命令行界面,最后运行第一个构建文件。
第二章, 构建 Java 项目,解释了构建 Java 应用程序和库、使用 JUnit 进行单元测试、阅读测试报告以及创建应用程序分发的主题。
第三章, 构建 Web 应用程序,涉及构建和运行 Web 应用程序。它还简要介绍了依赖项、存储库和配置等概念。
第四章,揭秘构建脚本,从 Gradle DSL 上下文中的 Groovy 语法入门。然后,它继续解释 Gradle 构建的骨干概念,如构建阶段、项目 API 以及与 Gradle 任务相关的各种主题。
第五章,多项目构建,涵盖了结构化多项目目录的一些选项。然后,它讨论了构建逻辑的组织,这是一个多项目构建。
第六章,使用 Gradle 的实战项目,讨论了开发者面临的一个重要问题,即如何将现有的 Ant 和 Maven 脚本迁移到 Gradle。这一章提供了不同的策略和示例,指导开发者以更简单、更易于管理的方式进行迁移。这一章还深入探讨了使用 Gradle 发布工件的不同方式,以及开发者如何将 Gradle 与持续集成工作流程集成。
第七章,使用 Gradle 进行测试和报告,讨论了 TestNG 框架与 Gradle 的集成。除了使用 TestNG 进行单元测试外,它还涉及了不同的集成测试策略,用户可以遵循这些策略来执行与单元测试用例分开的集成测试。它还讨论了将 Sonar 与 Gradle 集成,这有助于开发者根据不同参数分析代码质量,以及 JaCoCo 集成进行代码覆盖率分析。
第八章,组织构建逻辑和插件,讨论了 Gradle 插件的一个重要构建块,没有它你会发现这本书不完整。它讨论了插件的需求以及开发者可以根据项目大小和复杂性采用的不同方式来创建插件。
第九章,多语言项目,展示了如何使用 Gradle 为使用 Java 之外或除 Java 之外的语言的项目;这一章展示了构建 Groovy 和 Scala 项目的示例。
你需要这本书的什么
在执行书中提到的代码之前,你的系统必须安装以下软件:
-
Gradle
-
Java 1.7 或更高版本
对于第六章至第八章,你需要以下软件:
-
Jenkins
-
Ant 1.9.4
-
Maven 3.2.2
这本书面向的对象
这本书是为想要使用 Gradle 或已经在他们的项目中使用 Gradle 的 Java 和其他 JVM 语言开发者而写的。
不需要具备 Gradle 的先验知识,但了解与构建相关的术语以及理解 Java 语言会有所帮助。
术语
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称显示如下:“这个类仅公开一个名为greet的方法,我们可以使用它来生成问候消息。”
代码块设置如下:
task helloWorld << {
println "Hello, World!"
}
任何命令行输入或输出都应如下书写:
$ gradle --version
或者它可以这样写:
> gradle --version
当某些输出或代码块被截断时,它将以省略号(...)表示,如下所示:
$ gradle tasks
...
Other tasks
-----------
helloWorld
...
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“一旦按下提交按钮,我们就会得到期望的结果。”
注意
警告或重要注意事项以如下方式显示在框中。
小贴士
技巧和窍门看起来像这样。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果你在某个主题上具有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲所有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你的账户中下载示例代码文件,地址为www.packtpub.com,适用于你购买的所有 Packt 出版图书。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
错误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题和建议
如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。
第一章:运行您的第一个 Gradle 任务
我们将开始一段快速学习Gradle 基础知识的旅程。为了温和地开始,我们首先将安装 Gradle。然后,我们将通过查看gradle命令的用法来熟悉 Gradle 的命令行界面。此外,到本章结束时,我们将运行我们的第一个 Gradle 构建脚本。
构建软件工件是一个复杂的过程,涉及各种活动,如编译源代码、运行自动化测试、打包可分发文件等。这些活动进一步分解成许多步骤,通常依赖于执行顺序、获取依赖工件、解析配置变量等。手动执行所有这些活动既繁琐又容易出错。一个好的构建自动化工具可以帮助我们以可重复的方式减少构建正确工件所需的时间和精力。
Gradle 是一个高级构建自动化工具,它从各种经过验证的构建工具中汲取精华,并在其基础上进行创新。Gradle 可以用于生成诸如 Web 应用程序、应用程序库、文档、静态站点、移动应用、命令行和桌面应用程序等工件。Gradle 可以用于基于各种语言和技术堆栈的项目构建,例如 Java、C/C++、Android、Scala、Groovy、Play、Grails 等。由于Java 虚拟机(JVM)恰好是 Gradle 支持的第一类平台之一,本书中的示例将主要关注构建基于 Java 的项目。
Gradle 像 Ant 一样让我们对构建有完全的控制权,但通过提供智能默认值(以约定形式)来避免重复。Gradle 真正通过约定而非配置来工作,就像 Maven 一样。然而,当我们需要偏离时,它永远不会妨碍我们。这也与 Maven 形成了鲜明的对比。Gradle 试图在约定和可配置性之间保持适当的平衡。
前一代构建工具,如 Ant 和 Maven,选择 XML 来表示构建逻辑。虽然 XML 是可读的,但它更多的是一种机器友好的格式(更容易被程序读取/写入)。它非常适合表示和交换层次化数据,但当涉及到编写任何逻辑时,即使是简单的逻辑也可能会轻易地占用数百行。另一方面,Gradle 构建可以通过非常人性化的 Groovy DSL 进行配置。Groovy 是一种强大、表达性强且低仪式的动态语言,非常适合构建脚本。
Gradle 本身是一个用 Java 和 Groovy 编写的JVM应用程序。由于 Gradle 在 JVM 上运行,因此它在 Windows、Mac OS X 和 Linux 上运行方式相同。Gradle 还拥有先进的依赖关系解析系统,可以从现有的 Maven 和 Ivy 仓库或甚至文件系统中解析依赖关系。
经过多年的发展,Gradle 已经成长为一个非常稳定的开源项目,拥有活跃的贡献者和商业支持。丰富的插件生态系统和充满活力的社区使 Gradle 成为各种项目的绝佳选择。Gradle 已经拥有一个令人印象深刻的采用者名单,其中包括像 Google Android、LinkedIn、Unity 3D、Netflix 等科技巨头。开源库和框架,如 Spring、Hibernate 和 Grails,正在使用 Gradle 来驱动它们的构建过程。
安装 Gradle
在我们运行 Gradle 之前,我们必须在我们的机器上安装它。Gradle 可以通过多种方式安装和更新。我们将首先了解一种更手动的方法来安装 Gradle,然后简要地看看通过一些常用的包管理器安装 Gradle 的方法。我们可以选择任何一种适合的方法。无论我们以何种方式安装 Gradle,我们都必须满足以下先决条件。
Gradle 需要Java 运行时环境(JRE)6 或Java 开发工具包(JDK)1.6 或更高版本。没有其他依赖项。我们建议安装 JDK。要验证这一点,我们可以在命令行中使用以下命令检查 Java 版本:
$ java -version
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)
如果我们没有看到与前面命令中显示的输出大致相同的内容,那么我们的 JDK 安装存在问题。
注意
可以从以下 URL 下载最新的 JDK:
www.oracle.com/technetwork/java/javase/downloads/index.html
手动安装
如果我们想要对安装有更精细的控制,那么这是一个合适的方法。这可能是在我们无法使用包管理器、需要下载和安装非常具体的二进制文件,或者在公司防火墙后面(自动下载包管理器不被允许)的情况下。我们需要下载 Gradle 的二进制文件,并在命令行上使它们可用于使用。
可以从www.gradle.org/downloads下载最新的 Gradle 发行版。截至编写时,最新版本是 2.9。
Gradle 二进制发行版有两种形式,如下所示:
-
gradle-2.9-all.zip:此文件包含二进制文件、源代码和文档 -
gradle-2.9-bin.zip:此文件仅包含二进制文件
我们可以根据需要下载上述任何一种。此外,这是一个与操作系统无关的 zip 文件,因此相同的 zip 文件可以在 Mac OS X、Windows 和 Linux 上提取。下一节将 Gradle 命令添加到命令行。本节取决于我们使用的操作系统。
在 Mac OS X 和 Linux 上安装
假设我们将下载的 zip 文件提取为~/gradle-2.9/。现在,我们只需根据操作系统和使用的 shell,在.bashrc/、.bash_profile/或.zshrc的末尾添加以下两行:
export GRADLE_HOME=~/gradle-2.9
export PATH=$PATH:$GRADLE_HOME/bin
重新启动终端或源修改后的文件以使更改生效。
在 Windows 上安装
假设我们将 zip 文件解压为 C:\gradle-2.9,然后执行以下步骤:
-
打开开始菜单,右键单击 计算机 并选择 属性。
-
在 高级系统设置 中,选择 高级 选项卡,然后选择 环境变量...。![在 Windows 上安装]
-
点击 新建。
-
创建一个值为
C:\gradle-2.9的GRADLE_HOME环境变量。小贴士
下载示例代码
您可以从您在
www.packtpub.com的账户中下载示例代码文件,以获取您购买的所有 Packt 出版物的书籍。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册以将文件直接通过电子邮件发送给您。![在 Windows 上安装]()
小贴士
在未来,当我们下载 Gradle 的后续版本时,我们需要更改此值以指向正确的文件夹。
-
编辑(或添加,如果尚未存在)
PATH环境变量。在其值的末尾,追加;%GRADLE_HOME%\bin(如果存在多个路径条目,请添加分号)。
安装 Gradle 的替代方法
虽然手动安装可以完全控制安装过程,但下载和提取正确版本、升级到最新版本、卸载和编辑环境变量等任务很快就会变得繁琐且容易出错。这就是为什么许多人更喜欢使用包管理器来控制整个过程。
通过操作系统特定的包管理器安装
在手动安装时,如前文所述,非常简单,我们可以通过使用包管理器使其变得超级简单。
一些 Linux 发行版,如 Ubuntu,随其包管理器提供,而 Mac OS X 和 Windows 默认没有安装任何包管理器。然而,幸运的是,这两个平台都有多个包管理器可用。我们将看到 Mac 上的 Homebrew 和 Windows 上的 Chocolatey 的示例。
Mac OS X
确保已安装 Homebrew。如果是的话,安装 Gradle 只需使用以下命令:
$ brew install gradle
注意
更多关于 Homebrew 的详细信息可以在 brew.sh 找到。
Linux (Ubuntu)
在 Ubuntu 上使用内置的包管理器,称为 高级包装工具(APT),我们可以使用以下命令安装 Gradle:
$ sudo apt-get install gradle
Windows
如果我们已安装 Chocolatey,安装 Gradle 只需一个命令:
c:\> cinst gradle
注意
更多关于 Chocolatey 的详细信息可以在 chocolatey.org 找到。
通过 SDKMAN 安装
SDKMAN 代表 软件开发工具包管理器。正如其自身所说,网站将其描述为:SDKMAN! 是一种用于管理大多数基于 Unix 的系统上多个软件开发工具包并行版本的工具。
SDKMAN 相对于其他包管理器的优势在于,我们可以在系统上安装多个 Gradle 版本,并为给定的项目选择不同的版本。如果我们已经安装了它,我们只需要运行以下命令:
$ sdk install gradle
SDKMAN 可以从sdkman.io/安装。
验证安装
无论我们选择哪种方式安装 Gradle,在继续之前验证它是否正常工作都是一个好主意。我们可以通过简单地检查命令行上的 Gradle 版本来完成这个任务:
$ gradle --version
------------------------------------------------------------
Gradle 2.9
------------------------------------------------------------
Build time: 2015-11-17 07:02:17 UTC
Build number: none
Revision: b463d7980c40d44c4657dc80025275b84a29e31f
Groovy: 2.4.4
Ant: Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM: 1.8.0_25 (Oracle Corporation 25.25-b02)
OS: Mac OS X 10.10.5 x86_64
如果我们看到类似上面的输出,那么我们已经正确地在我们的机器上安装了 Gradle。
小贴士
我们可以使用-v代替--version来得到相同的结果。
设置 JVM 选项
虽然大多数情况下并不需要,但如果我们需要为 Gradle 将要使用的 JVM 设置一些全局选项,Gradle 提供了一个方便的方式来完成这个任务。我们可以通过设置GRADLE_OPTS环境变量并使用可接受的标志来调整 JVM。
Gradle 也尊重JAVA_OPTS环境变量。然而,在设置它时我们需要小心,因为它会影响机器上所有 Java 程序设置。我们应该通过这个变量设置我们希望对所有 Java 应用程序保持通用的设置,而那些只需要应用到 Gradle 的设置应该通过GRADLE_OPTS设置。
小贴士
一些常用的选项是-Xms和-Xmx,它们设置 JVM 的最小和最大堆大小。
Gradle 命令行界面
与其他构建工具一样,Gradle 主要是通过命令行运行的。这就是为什么花些时间熟悉其命令行界面是值得的。通常,一个gradle命令是从项目目录的根目录发出的,并执行一些任务。假设我们目前在hello-gradle目录中,该目录目前为空。
Gradle 提供了一个非常简单的命令行界面(CLI),其形式如下:
gradle [options…] [tasks…]
如我们所见,除了gradle命令本身之外,其他一切都是可选的。options调整 Gradle 的执行,而tasks(我们将在后面详细讨论)是工作的基本单元。选项对所有项目都是通用的,并且特定于 Gradle,但任务可能因运行gradle命令的项目而异。
有些任务在所有项目中都是可用的。其中之一就是help任务:
$ gradle help
:help
Welcome to Gradle 2.9.
To run a build, run gradle <task> ...
To see a list of available tasks, run gradle tasks
To see a list of command-line options, run gradle --help
To see more detail about a task, run gradle help --task <task>
BUILD SUCCESSFUL
Total time: 0.639 secs
Gradle 通过告诉我们如何查找所有可用的任务和列出所有命令行选项来帮助我们。让我们首先检查一下我们项目当前可用的其他任务。记住我们仍然在空的目录hello-gradle中:
$ gradle tasks
:tasks
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]
Help tasks
----------
components - Displays the components produced by root project 'hello-gradle'. [incubating]
dependencies - Displays all dependencies declared in root project 'hello-gradle'.
dependencyInsight - Displays the insight into a specific dependency in root project 'hello-gradle'.
help - Displays a help message.
model - Displays the configuration model of root project 'hello-gradle'. [incubating]
projects - Displays the sub-projects of root project 'hello-gradle'.
properties - Displays the properties of root project 'hello-gradle'.
tasks - Displays the tasks runnable from root project 'hello-gradle'.
To see all tasks and more detail, run gradle tasks --all
To see more detail about a task, run gradle help --task <task>
BUILD SUCCESSFUL
Total time: 0.652 secs
这显示了一些即使我们没有在我们的项目中添加任何任务也能使用的通用任务。我们可以尝试运行所有这些任务并查看输出。我们将在接下来的章节中详细了解这些任务。
另一个有用的命令gradle help建议我们使用--help选项来检查所有可用的选项。
小贴士
help任务与--help选项并不相同。
当我们运行 gradle --help 命令时,我们得到以下输出:
$ gradle --help
USAGE: gradle [option...] [task...]
-?, -h, --help Shows this help message.
-a, --no-rebuild Do not rebuild project dependencies.
-b, --build-file Specifies the build file.
…..
(输出被截断以节省空间。)
选项有一个长形式,如 --help,可能还有一个短形式,如 -h。我们之前已经使用了一个选项,即 --version 或 -v,它会打印有关 Gradle 版本的信息。以下是一些常用选项;还有更多选项,可以使用 gradle --help 命令查看:
| 选项 | 描述 |
|---|---|
-b, --build-file |
这指定了一个构建文件(默认:build.gradle) |
--continue |
即使任务失败,也会继续任务执行 |
-D, --system-prop |
这会设置 JVM 的系统属性 |
-d, --debug |
这会打印调试级别的日志 |
--gui |
这将启动 Gradle 图形用户界面 |
-i, --info |
这会打印信息级别的日志 |
-P, --project-prop |
这会给项目添加一个属性 |
-q, --quiet |
这只记录错误 |
-s, --stacktrace |
这会打印异常的堆栈跟踪 |
-x, --exclude-task |
这会排除一个特定的任务 |
第一个 Gradle 构建脚本
因此,我们现在已经准备好湿脚并看到我们的第一个 Gradle 脚本的实际应用。让我们在 hello-gradle 目录中创建一个名为 build.gradle 的文件。除非使用 --build-file 选项提供了构建文件路径,否则 Gradle 将当前目录视为项目根目录,并尝试在那里找到 build.gradle 文件。如果我们之前使用过 Ant 或 Maven,我们可以将此文件与 build.xml 或 pom.xml 相关联。
现在,打开 build.gradle 文件,让我们通过添加以下行来声明一个任务:
task helloWorld
我们应该在命令行上看到以下任务:
$ gradle tasks
...
Other tasks
-----------
helloWorld
...
在这里,我们已经成功创建了一个名为 helloWorld 的任务对象。任务在 Gradle 中是一等对象,这意味着它们具有属性和方法。这为我们提供了在构建的可定制性和可编程性方面的巨大灵活性。
然而,这个任务实际上还没有做任何事情。所以让我们给这个任务添加一些有意义的操作:
task helloWorld << {
println "Hello, World!"
}
现在从命令行,我们可以通过以下命令执行此任务:
$ gradle -q helloWorld
Hello, World!
注意,我们使用了 –q 标志来减少输出中的冗余。当运行此任务时,我们看到我们的任务生成的输出,但没有来自 Gradle 的任何内容,除非是错误。
现在,让我们简要地了解 build.gradle 文件。第一行声明了任务并开始了一个代码块的主体,该代码块将在最后执行。左移运算符 (<<) 可能感觉放置得有些奇怪,但在这种情况下非常重要。我们将在后面的章节中看到它确切的意义。第二行是一个 Groovy 语句,它会将给定的字符串打印到控制台。此外,第三行结束了代码块。
小贴士
Groovy 的 println "Hello, World!" 等同于 Java 中的 System.out.println("Hello, World!")。
任务名称缩写
当从命令行调用 gradle 任务时,我们可以通过只输入足以唯一识别任务名称的字符来节省几个按键。例如,可以使用gradle hW调用helloWorld任务。我们也可以使用helloW、hWorld,甚至heWo。然而,如果我们只调用gradle h,那么将会调用help任务。
当我们需要频繁调用长的 Gradle 任务名称时,这非常有用。例如,名为deployToProductionServer的任务可以通过调用gradle dTPS来调用,前提是这个缩写不匹配任何其他任务名称。
Gradle 守护进程
当我们谈论频繁调用 Gradle 时,了解一个推荐的技术来提高构建性能是个好时机。Gradle 守护进程,一个在后台持续运行的进程,可以显著加快构建速度。
对于给定的 gradle 命令调用,我们可以指定--daemon标志来启用守护进程。然而,我们应该记住,当我们启动守护进程时,只有后续的构建会更快,但当前的构建不会。例如:
$ gradle helloWorld --daemon
Starting a new Gradle Daemon for this build (subsequent builds will be faster).
:helloWorld
Hello, World!
BUILD SUCCESSFUL
Total time: 2.899 secs
$ gradle helloWorld
:helloWorld
Hello, World!
BUILD SUCCESSFUL
Total time: 0.6 secs
在前面的例子中,如果我们注意到两次运行所需的时间,第二次完成得更快,这要归功于 Gradle 守护进程。
我们也可以通过传递--no-daemon标志来防止特定的构建调用使用守护进程。
有多种方法可以启用或禁用 Gradle 守护进程,这些方法在docs.gradle.org/current/userguide/gradle_daemon.html中有文档说明。
Gradle Wrapper
Gradle Wrapper 由 Linux/Mac OS X 的gradlew外壳脚本、Windows 的gradlew.bat批处理脚本和一些辅助文件组成。这些文件可以通过运行 gradle wrapper任务生成,并且应该与项目源代码一起提交到版本控制系统(VCS)。我们不必使用系统范围内的gradle命令,而是可以通过包装脚本运行构建。
通过包装脚本运行构建的一些优点如下:
-
我们不需要手动下载和安装 Gradle。包装脚本会处理这一点。
-
它使用项目所需的具体版本的 Gradle。这减少了由于不兼容的 Gradle 版本而破坏项目构建的风险。我们可以安全地升级(或降级)系统范围内的 Gradle 安装,而不会影响我们的项目。
-
它在团队中所有开发者的机器上透明地强制执行相同版本的 Gradle。
-
在持续集成构建环境中,这非常有用,因为我们不需要在服务器上安装/更新 Gradle。
生成包装文件
Gradle 的wrapper任务已经对所有 Gradle 项目可用。要生成包装脚本和辅助文件,只需从命令行执行以下代码:
$ gradle wrapper
在生成wrapper时,我们可以指定确切的 Gradle 版本如下:
$ gradle wrapper --gradle-version 2.9
在这个例子中,我们指定要使用的 Gradle 版本是 2.9。运行此命令后,我们应该将生成的文件提交到版本控制系统中。我们可以自定义wrapper任务来使用配置的 Gradle 版本,生成不同名称的包装脚本,更改它们的位置等等。
通过包装器运行构建
为了利用包装脚本的好处,而不是使用gradle命令,我们需要根据我们的操作系统调用基于我们的操作系统的包装脚本。
在 Mac OS X/Linux 上:
$ ./gradlew taskName
在 Windows 上:
$ gradlew taskName
我们可以使用与传递给gradle命令完全相同的方式使用参数和标志。
摘要
在本章中,我们首先对 Gradle 进行了简要介绍。然后,我们探讨了手动安装以及通过软件包管理器安装的方法。我们还学习了 Gradle 的命令行界面。最后,我们编写了我们的第一个 Gradle 构建脚本。
如果你已经跟随着本章内容学习到了这里,你现在就可以在你的机器上检查任何基于 Gradle 的项目并执行构建了。此外,你已经具备了编写一个非常基础的 Gradle 构建脚本的知识。接下来,我们将探讨如何使用 Gradle 构建基于 Java 的项目。
第二章。构建 Java 项目
在上一章中,我们看到了一个非常基础的构建脚本,它只是在控制台上打印了传统的Hello World。现在我们已经熟悉了 Gradle 命令行界面,这正是我们开始简单 Java 项目的完美时机。
在本章中,我们将看到如何使用 Gradle 构建、测试简单的 Java 项目,如何将外部依赖项添加到类路径中,以及如何构建可分发二进制文件。
我们将尽量使 Java 代码尽可能简洁,以便我们能够更多地关注项目的构建。在这个过程中,我们将学习一些基于 Gradle 的项目应该遵循的最佳实践。如果我们无法完全理解本章中所有构建脚本语法,那也是可以的,因为我们将详细地在第四章揭秘构建脚本中看到。
构建 Java 项目
为了展示使用 Gradle 构建 Java 项目的示例,让我们创建一个非常简单的 Java 应用程序,它将问候用户。在应用程序逻辑方面,这比简单的hello world要稍微复杂一点。
首先,创建一个名为hello-java的目录。这是我们项目目录。对于以下步骤,请随意选择您喜欢的 IDE/文本编辑器来编辑文件。
创建构建文件
在项目目录的根目录下,让我们创建build.gradle文件,并将其中的以下代码行添加到其中:
apply plugin: 'java'
是的,这就是目前构建文件中所有需要的内容,只有一行。我们很快就会看到它的含义。
添加源文件
默认情况下,就像 Maven 一样,Java 源文件是从项目的src/main/java目录中读取的。当然,我们可以配置它,但让我们留到以后再说。让我们在我们的项目中创建这个目录结构。
现在,我们需要创建一个 Java 类来生成问候信息。同时,我们还会创建一个包含main方法的Main类,以便可以从命令行运行应用程序。Java 文件应该保存在一个源根目录下的适当包结构中。我们将使用com.packtpub.ge.hello包作为此示例:
hello-java
├── build.gradle // build file
└── src
└── main
└── java // source root
└── com
└── packtpub
└── ge
└── hello
├── GreetingService.java
└── Main.java
正如我们可以从前面的结构中看到的那样,我们在src/main/java源根下创建了包结构。
让我们创建GreetingService.java文件:
package com.packtpub.ge.hello;
public class GreetingService {
public String greet(String user) {
return "Hello " + user;
}
}
这个类仅公开一个名为greet的方法,我们可以使用它来生成问候信息。
这就是我们的Main.java文件看起来像这样:
package com.packtpub.ge.hello;
public class Main {
public static void main(String[] args) {
GreetingService service = new GreetingService();
System.out.println(service.greet(args[0]));
}
}
这个类有一个main方法,当程序运行时将被调用。它实例化GreetingService并打印greet方法在控制台上的输出。
构建项目
在添加 Java 文件后,我们现在想要编译项目并生成类文件。这可以通过从命令行调用以下任务来完成:
$ gradle compileJava
编译后的类文件会存放在项目根目录下的build/classes/main。你可以通过再次检查项目树来确认。现在我们将忽略其他文件和目录:
hello-java
...
├── build
│ ├── classes
│ │ └── main
│ │ └── com
│ │ └── packtpub
│ │ └── ge
│ │ └── hello
│ │ ├── GreetingService.class
│ │ └── Main.class
...
到目前为止,我们可以直接运行这个类,但让我们要求更多,并为我们的应用程序生成.jar文件。让我们运行以下任务:
$ gradle build
它在build/libs目录下为我们的项目生成一个 Jar 文件:
hello-java
...
├── build
│ ...
│ ├── libs
│ │ └── hello-java.jar
...
让我们测试 Jar 是否按预期工作。要运行 Jar,请发出以下命令:
$ java -cp build/libs/hello-java.jar \ com.packtpub.ge.hello.Main Reader
我们将Reader作为参数传递给我们的 java Main类的main方法。这将产生以下输出:
Hello Reader
注意
当我们运行build任务时,Gradle 也会在执行构建任务之前调用compileJava和其他依赖任务。因此,我们在这里不需要显式调用compileJava来编译类。
.jar文件的名称与项目名称相同。这可以通过在build.gradle文件中设置archivesBaseName属性来配置。例如,要生成名为my-app.jar的 Jar 文件,请将以下代码行添加到构建文件中:
archivesBaseName = "my-app"
现在,让我们启动:
$ gradle clean
此外,再次检查目录树。不出所料,它已经清理完毕,源文件保持完整。
从我们使用 Ant 的经验来看,即使是这个规模的项目,我们也必须定义至少几个目标,这将需要很多行 XML。虽然 Maven 可以通过约定工作,但 Maven 的pom.xml文件在成为有效的pom.xml文件之前仍需要一些仪式。因此,一个最小的pom.xml文件仍然看起来像五到六行 XML。
与 Gradle 精心选择和默认的简单性相比。
这是一个很好的地方,我们应该看看java插件为我们构建引入了哪些所有任务:
$ gradle –q tasks
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.
Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Help tasks
----------
components - Displays the components produced by root project 'hello-java'. [incubating]
dependencies - Displays all dependencies declared in root project 'hello-java'.
dependencyInsight - Displays the insight into a specific dependency in root project 'hello-java'.
help - Displays a help message.
model - Displays the configuration model of root project 'hello-java'. [incubating]
projects - Displays the sub-projects of root project 'hello-java'.
properties - Displays the properties of root project 'hello-java'.
tasks - Displays the tasks runnable from root project 'hello-java'.
Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.
...
看到我们的构建中通过仅应用java插件就有这么多有用的任务,这很有趣。显然,Gradle 使用了一个非常强大的插件机制,可以利用它来在构建逻辑上应用不要重复自己(DRY)原则。
插件简介
Gradle 本身不过是一个任务运行器。它不知道如何编译 Java 文件或在哪里读取源文件。这意味着这些任务不是默认存在的。正如我们在上一章中看到的,没有应用任何插件的 Gradle 构建文件包含非常少的任务。
插件为 Gradle 构建添加相关的任务和约定。在我们的当前示例中,所有如compileJava、build、clean等任务实际上都是由我们应用于构建的java插件引入的。
这意味着 Gradle 不会强迫我们使用特定的方式来编译 Java 项目。完全取决于我们选择为构建使用java插件。我们可以根据需要配置它。如果我们仍然不喜欢它的工作方式,我们可以自由地直接在构建中添加自己的任务,或者通过一个自定义插件来实现我们想要的方式。
Gradle 自带了许多插件。java插件就是其中之一。在本书的整个过程中,我们将看到许多这样的插件,它们将为我们的构建带来许多有趣的功能。
单元测试
单元测试是软件开发不可或缺的方面。测试让我们对我们的代码正常工作充满信心,并在重构时提供安全网。幸运的是,Gradle 的 Java 插件使得单元测试代码变得简单且容易。
我们将为上面创建的相同示例应用程序编写一个简单的测试。我们现在将使用 JUnit(v4.12)库创建我们的第一个单元测试。
注意
更多关于 JUnit 的信息可以在junit.org找到。
添加单元测试源
再次,像 Maven 一样,Java 测试源代码保存在项目根目录相对的src/test/java目录中。我们将创建这个目录,并且作为一个好的实践,测试包结构将反映与源包相同的层次结构。
...
src
└── test
└── java // test source root
└── com
└── packtpub
└── ge
└── hello
└── GreetingServiceTest.java
...
我们将为GreetingService添加测试。按照惯例,测试的名称将是GreetingServiceTest.java。以下是这个文件的代码:
package com.packtpub.ge.hello;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GreetingServiceTest {
GreetingService service;
@Before
public void setup() {
service = new GreetingService();
}
@Test
public void testGreet() {
assertEquals("Hello Test", service.greet("Test"));
}
}
测试设置了一个系统测试对象(SUT)的实例,即GreetingService,testGreet方法检查 SUT 的greet方法输出与预期消息的相等性。
现在,花点时间尝试使用compileTestJava任务编译测试,它与compileJava完全相同,但编译测试源文件。它编译得很好吗?如果不是,我们能猜测一下可能出了什么问题吗?
任务应该因为 JUnit(一个外部库)不在类路径上编译文件而失败,并出现大量编译错误。
将 JUnit 添加到类路径
要编译和运行这个测试用例,我们需要 JUnit 库在类路径上。重要的是要记住,这个依赖项仅在编译和运行测试时需要。我们的应用程序在编译或运行时不需要 JUnit。我们还需要告诉 Gradle 在哪里搜索这个工件,以便在需要时下载它。为此,我们需要更新build.gradle文件,如下所示:
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:4.12'
}
从我们已知的内容来看,这个构建文件有两个新增内容。
在dependencies部分,我们列出了项目的所有依赖项及其作用域。我们声明 JUnit 在testCompile作用域内可用。
在repositories部分,我们配置了外部依赖项将找到的仓库的类型和位置。在这个例子中,我们告诉 Gradle 从 Maven 中央仓库获取依赖项。由于 Maven 中央是一个非常常用的仓库,Gradle 提供了一个通过mavenCentral()方法调用来配置它的快捷方式。
我们将在下一章更深入地介绍这两个部分。
运行测试
我们对运行测试以检查一切是否按预期工作感兴趣。让我们运行test任务,这将按顺序运行test任务依赖的所有任务。我们也可以通过查看列出作为此构建一部分运行的所有任务的输出来验证这一点:
$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
BUILD SUCCESSFUL
Total time: 1.662 secs
看起来测试通过了。为了了解 Gradle 如何告诉我们测试失败,让我们故意将断言中的预期值更改为Test Hello,以便断言失败:
@Test
public void testGreet() {
assertEquals("Test Hello", service.greet("Guest"));
}
然后再次运行命令,以查看测试失败的结果:
$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
com.packtpub.ge.hello.GreetingServiceTest > testGreet FAILEDorg.junit.ComparisonFailure at GreetingServiceTest.java:18
1 test completed, 1 failed
:test FAILED
FAILURE: Build failed with an exception.
......
是的,所以测试失败了,输出告诉你关于文件和行号的信息。此外,它还指向包含测试失败更多详细信息的报告文件。
查看测试报告
无论测试是否通过,都会创建一个包含所有运行测试详细信息的漂亮的 HTML 报告。默认情况下,此报告位于项目根目录相对于build/reports/tests/index.html。您可以在浏览器中打开此文件。
对于上述失败,报告看起来大致如此:

如果我们点击失败的测试,我们可以看到失败的详细信息:

我们可以在堆栈跟踪的第一行看到org.junit.ComparisonFailure: expected:<[Test Hello]> but was:<[Hello Test]>。
工作流程中的拟合测试
现在我们已经有了测试,只有在测试通过的情况下,才合理地构建我们的项目二进制文件(.jar)。为此,我们需要在任务之间定义某种类型的流程,以便如果任务失败,管道将在那里中断,后续的任务将不会执行。因此,在我们的示例中,构建的执行应取决于测试的成功。
猜猜看,这已经由我们的java插件为我们处理了。我们只需要调用流程中的最后一个任务,并且如果任何任务失败,构建将不会成功,所有依赖的任务将按顺序调用。
$ gradle build
此外,我们不需要明确调用构建所依赖的所有任务,因为它们无论如何都会被调用。
现在让我们修复测试,看看 Jar 文件是否再次被创建:
$gradle build
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:check
:build
BUILD SUCCESSFUL
Total time: 1.617 secs
哈哈!所以测试通过了,我们再次可以构建我们应用程序的二进制文件。
注意 Gradle 如何智能地确定,如果只有测试被更改,它只编译了测试。在前面的输出中,compileJava显示UP-TO-DATE,这意味着没有变化,因此 Gradle 没有必要再次编译源文件。
小贴士
如果我们需要强制运行任务操作,即使两次运行之间没有任何变化,我们可以在命令行上传递--rerun-tasks标志,以便所有任务操作都可以运行。
如果我们再次查看测试报告,它们将看起来如下:

测试摘要将看起来大致如此:

打包应用程序可分发文件
在第一个例子中,我们直接从命令行使用java命令运行我们的应用程序。通常,这样的命令行应用程序会附带脚本以运行应用程序,这样最终用户就不必总是手动编写整个命令。此外,在开发过程中,我们反复需要运行应用程序。如果我们可以在我们的构建文件中编写一个任务,以便在单个 Gradle 调用中运行应用程序,那就更好了。
好消息是,已经存在一个名为application的插件,它是与 Gradle 一起提供的,可以为我们完成这两项任务。在这个例子中,我们将hello-test项目复制为hello-app。让我们对我们的build.gradle文件进行以下简单修改:
apply plugin: 'java'
apply plugin: 'application'
mainClassName = "com.packtpub.ge.hello.Main"
run.args = ["Reader"]
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:4.11'
}
第二行将application插件应用于我们的构建。为了使此插件正常工作,我们需要配置 Gradle 以使用我们的Main入口点类,该类具有需要在应用程序运行时运行的静态main方法。我们在第#4行通过设置由application插件添加到构建的mainClassName属性来指定这一点。最后,当我们想要使用 Gradle(即在开发过程中)运行应用程序时,我们需要向我们的应用程序提供一些命令行参数。application插件将run任务添加到我们的构建中。正如我们之前所说的,任务就像任何常规对象一样,是对象,并且具有属性和方法。在第#5行,我们将run任务的args属性设置为包含一个元素Reader的列表,因此每次我们执行运行任务时,Reader都将作为命令行参数传递给我们的主方法。那些使用 IDE 设置运行配置的人可以很容易地理解这一点。文件的其他部分与上一个例子相同。
注意
在前面的例子中,由于我们正在应用application插件,因此没有必要显式地应用java插件,因为application插件隐式地将java插件应用于我们的构建。
它还隐式地应用了distribution插件,因此我们得到了将应用程序打包为 ZIP 或 TAR 存档的任务,同时也得到了安装应用程序分发的本地任务。
更多关于application插件的信息可以在docs.gradle.org/current/userguide/distribution_plugin.html找到。
现在,如果我们检查我们的构建中可用的任务,我们会在Application tasks和Distribution tasks组下看到一些新增的任务:
$ gradle tasks
...
Application tasks
-----------------
installApp - Installs the project as a JVM application along with libs and OS specific scripts.
run - Runs this project as a JVM application
...
Distribution tasks
------------------
assembleDist - Assembles the main distributions
distTar - Bundles the project as a distribution.
distZip - Bundles the project as a distribution.
installDist - Installs the project as a distribution as-is.
...
使用 Gradle 运行应用程序
让我们首先看看run任务。我们将使用–q标志调用此任务以抑制 Gradle 的其他消息:
$ gradle -q run
Hello Reader
如预期的那样,我们在控制台上看到了输出。当我们需要更改并可以一键运行我们的应用程序时,这项任务表现得尤为出色:
public String greet(String user) {
return "Hola " + user;
}
我们暂时将GreetingService更改回返回"Hola"而不是"Hello",看看运行任务是否反映了这些更改:
$ gradle -q run
Hola Reader
是的,它确实如此。
小贴士
有些人可能会想知道如何从命令行本身传递命令行参数来运行任务,而不是从构建文件中,这就像以下这样:
$ gradle –q run Reader
然而,这种方式并不奏效。因为 Gradle 可以从命令行接受多个任务名称,所以 Gradle 无法知道Reader是传递给运行任务需要传递的参数,还是它本身就是一个任务名称。例如,以下命令调用了两个任务:
$ gradle –q clean build
如果你确实需要在每次运行任务时将命令行传递给程序,当然也有一些解决方案。其中一种方法是通过使用–Pproperty=value命令行选项,然后在run任务中提取属性值,将其作为args传递给程序。–P将属性添加到 Gradle Project中。
要实现这一点,请按照以下方式更新build.gradle中的run.args:
run.args = [project.runArgs]
此外,还可以从命令行通过调用以下方式提供属性值:
$ gradle -q run -PrunArgs=world
在前面的示例中,我们在调用gradle命令时提供了属性的值。
或者,我们可以在项目根目录中与build.gradle文件平行的位置创建一个gradle.properties文件。在这种情况下,对于这个例子,它将只包含runArgs=world。但它可以声明更多的属性,这些属性将在构建中作为项目对象上的属性可用。
当然,还有其他声明属性的方法,可以在docs.gradle.org/current/userguide/build_environment.html中找到。
构建分发存档
另一个有趣的任务是distZip,它将应用程序与特定于操作系统的启动脚本一起打包:
$ gradle distZip
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:startScripts
:distZip
BUILD SUCCESSFUL
Total time: 1.29 secs
它会在项目根目录的build/distributions中生成 ZIP 格式的应用程序分发。ZIP 的名称默认为项目名称。在这种情况下,它将是hello-app.zip。如果需要,可以使用以下属性在build.gradle中更改它:
distributions.main.baseName = 'someName'
让我们解压缩存档以查看其内容:
hello-app
├── bin
│ ├── hello-app
│ └── hello-app.bat
└── lib
└── hello-app.jar
我们在 ZIP 文件内部看到了一个非常标准的目录结构。它包含一个 shell 脚本和一个 Windows 批处理脚本,用于运行我们的应用程序。它还包含我们的应用程序的 JAR 文件。lib目录也包含应用程序的运行时依赖项。我们可以配置distribution插件,在我们的分发中添加更多文件,如 Javadoc、README 等。
我们可以运行脚本以验证它是否工作。使用命令提示符,我们可以在 Windows 上执行此命令。为此,使用cd命令,并将目录更改为解压缩 ZIP 文件的bin目录。
$ hello-app Reader
Hello Reader
在 Mac OS X/Linux 上,执行以下命令:
$ ./hello-app Reader
Hello Reader
生成 IDE 项目文件
IDE 是 Java 开发者工具链和工作流程的一个基本组成部分。然而,手动设置 IDE 以正确识别任何中等规模项目的项目结构和依赖关系并不是一件容易的事情。
检入特定于 IDE 的文件或目录,如.classpath、.project、.ipr、.iws、.nbproject、.idea、.settings、.iml不是一个好主意。我们知道有些人仍然这样做,因为每次有人从版本控制系统中检出项目时,手动生成 IDE 文件都很困难。然而,检入此类文件会引发问题,因为它们最终会与主构建文件不同步。此外,这迫使整个团队使用相同的 IDE,并在构建有变化时手动更新 IDE 文件。
如果我们只需检入那些使项目能够独立于 IDE 构建所必需的文件,让我们的构建系统生成针对我们最喜欢的 IDE 的特定文件,那会多么美好?我们的愿望实现了。而且,这里还有最好的部分。你需要在 Gradle 构建文件中修改的行数只有一行。Gradle 自带了许多有趣的插件,可以生成特定于 IDE 的项目文件。IntelliJ IDEA 和 Eclipse 都由它们各自的插件支持。根据你想要支持哪个 IDE,你将包含apply plugin: 'idea'或apply plugin: 'eclipse'。
实际上,包含两者都没有害处。
现在,从命令行分别执行以下操作以使用 Eclipse 和 IntelliJ IDEA:
$ gradle eclipse
$ gradle idea
它应该为你生成特定于 IDE 的文件,现在你可以直接在任何 IDE 中打开项目。
小贴士
确保你在版本控制中忽略特定于 IDE 的文件。例如,如果你使用 Git,考虑在你的.gitignore文件中添加以下条目,以防止意外提交特定于 IDE 的文件:
.idea/
*.iml
*.ipr
*.iws
.classpath
.project
.settings/
摘要
我们本章开始时构建了一个非常简单的 Java 项目。我们看到了java插件的智能约定如何帮助我们使构建文件简洁。然后,我们向该项目添加了单元测试,并从 Maven 中央仓库中包含了 JUnit 库。我们使测试失败,并检查报告以查看解释。然后,我们看到了如何使用application插件创建应用程序的发行版。最后,我们看到了idea和eclipse插件,它们帮助我们为项目生成特定于 IDE 的文件。
总体来说,我们意识到 Gradle 的插件系统是多么强大。Gradle 自带了许多有趣的插件,但我们并不被迫使用它们。我们将在下一章构建一个 Web 应用程序,并学习配置和依赖管理是如何工作的。
第三章。构建 Web 应用程序
现在我们已经看到了使用 Gradle 构建命令行 Java 应用程序的便捷性,我们不应该对基于 Java servlet 规范构建 web 应用程序也同样容易感到惊讶。
在本章中,我们将首先构建一个简单的 web 应用程序,它以 WAR 文件的形式分发,可以部署到任何 servlet 容器。然后,我们将看看如何在构建文件中配置依赖项和仓库。
构建简单的 Java web 项目
再次强调,我们将使我们的应用程序尽可能简单,并创建一个基于上一章开发的 web 版本的应用程序。该应用程序将提供一个用户输入姓名的表单和一个 提交 按钮。当用户点击 提交 按钮时,将显示问候语。
该应用程序将基于 Servlet 3.1 规范。我们将重用上一章中开发的 GreetService。表单将由静态 HTML 文件提供服务,该文件可以将数据发送到我们的 servlet。servlet 将创建一个问候消息并将其转发到 JSP 进行渲染。
注意
更多关于 Servlet 规范 3.1 的详细信息,请访问 jcp.org/aboutJava/communityprocess/final/jsr340/index.html。
创建源文件
让我们以 hello-web 作为项目的根目录。其结构类似于我们之前看到的简单 Java 应用程序的结构,增加了一个新的部分,即 web 应用程序根目录。默认情况下,web 应用程序根目录位于 src/main/webapp。熟悉 Maven 的人会立刻注意到,这个路径与 Maven 使用的路径相同。
Web 应用程序根目录(webapp)包含运行 web 应用程序所需的所有公共资源,包括动态页面,如 JSP 或其他视图模板引擎(如 Thymeleaf、FreeMarker、Velocity 等)所需的文件;以及静态资源,如 HTML、CSS、JavaScript 和图像文件;以及特殊目录 WEB-INF 中的其他配置文件,如 web.xml。存储在 WEB-INF 中的文件不能直接被客户端访问;因此,这是一个存储受保护文件的完美位置。
我们将开始创建最终应用程序应具有的目录结构:
hello-web
├── build.gradle
└── src
└── main
├── java// source root
│ └── com
│ └── packtpub
│ └── ge
│ └── hello
│ ├── GreetingService.java
│ └── GreetingServlet.java
└── webapp// web-app root
├── WEB-INF
│ └── greet.jsp
└── index.html
然后,执行以下步骤:
-
让我们先添加上一章中熟悉的
GreetingService到我们的源代码中。我们可能会注意到,复制 Java 源文件并不是重用的正确方式。有更好的方法来组织这样的依赖。其中一种方法就是使用多模块项目。我们将在 第五章 中看到这一点,多项目构建。 -
现在,将以下内容添加到
index.html文件中:<!doctype html> <html> <head> <title>Hello Web</title> </head> <body> <form action="greet" method="post"> <input type="text" name="name"/> <input type="submit"/> </form> </body> </html>此文件以 HTML 5 的
doctype声明开始,这是我们能够使用的最简单的doctype。然后,我们创建一个表单,它将向greet端点(它是页面的一个相对路径)发送 POST 请求。 -
现在,在这个应用程序的核心部分,有一个名为
GreetServlet的 servlet,它响应 POST 请求:package com.packtpub.ge.hello; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/greet") public class GreetingServlet extends HttpServlet { GreetingService service = new GreetingService(); @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); String message = service.greet(name); request.setAttribute("message", message); RequestDispatcher dispatcher = getServletContext() .getRequestDispatcher("/WEB-INF/greet.jsp"); dispatcher.forward(request, response); } }在前面的代码中,
WebServlet注解的值将这个 servlet 映射到应用程序上下文中的/greet路径。然后,在这个 servlet 中创建了一个GreetService实例。重写的方法doPost从request对象中提取名称,生成问候信息,将此消息作为属性设置回request,以便在 JSP 中访问,然后最终将请求转发到位于/WEB-INF/greet.jsp的greet.jsp文件。 -
这将带我们到
greet.jsp文件,它被保存在WEB-INF中,这样它就不能直接访问,并且请求必须始终通过设置正确请求属性的 servlet 来传递:<!doctype html> <html> <head> <title>Hello Web</title> </head> <body> <h1>${requestScope.message}</h1> </body> </html>这个 JSP 只是打印出在请求属性中可用的
message。
创建构建文件
最后,让我们在项目的根目录中创建我们一直在等待的文件——build.gradle文件:
apply plugin: 'war'
repositories {
mavenCentral()
}
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}
让我们现在尝试理解这个文件:
-
第一行将
war插件应用于项目。此插件向项目添加一个war任务。有人可能会想知道为什么我们不需要应用java插件来编译类。这是因为war插件扩展了java插件;因此,当我们应用java插件时所有可用的任务仍然可用,除了war任务。 -
接下来是
repositories部分,它配置我们的构建过程以在 Maven 中央仓库中查找所有依赖项。
最后,在dependencies块中,我们将servlet-api添加到providedCompile配置(范围)。这告诉 Gradle 不要将 servlet API 打包到应用程序中,因为它将在应用程序部署的容器中已经可用。providedCompile配置是由war插件添加的(它还添加了providedRuntime)。如果我们有任何其他需要与我们的应用程序一起打包的依赖项,它将使用编译配置声明。例如,如果我们的应用程序依赖于 Spring 框架,那么依赖项部分可能看起来如下:
dependencies {
compile 'org.springframework:spring-context:4.0.6.RELEASE'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}
如果觉得repositories、configurations和dependencies的细节有点模糊,请不要担心。我们很快将在本章的后面部分更详细地看到它们。
构建工件
现在我们已经准备好了构建文件,我们必须构建可部署的 WAR 文件。让我们使用以下命令来验证我们的构建任务:
$ gradle tasks --all
…
war - Generates a war archive with all the compiled classes, the web-app content and the libraries. [classes]
…
我们会注意到那里的war任务,它依赖于classes(任务)。我们不需要显式地编译和构建 Java 源代码,这由classes任务自动处理。所以我们现在需要做的就是,使用以下命令:
$ gradle war
一旦构建完成,我们将看到目录结构类似于以下结构:
hello-web
├── build
│ ├── classes
│ │ └── main
│ │ └── com
│ │ └── packtpub
│ │ └── ge
│ │ └── hello
│ │ ├── GreetService.class
│ │ └── GreetServlet.class
│ ├── dependency-cache
│ ├── libs
│ │ └── hello-web.war
│ └── tmp
│ └── war
│ └── MANIFEST.MF
…
WAR 文件创建在/build/libs/hello-web.war。
注意
war文件不过是一个具有不同文件扩展名的 ZIP 文件。对于.ear或.jar文件也是如此。我们可以使用标准的 zip/unzip 工具,或者使用 JDK 的jar实用程序对这些文件执行各种操作。要列出 WAR 的内容,使用jar -tf build/libs/hello-web.war。
让我们检查这个 WAR 文件的内容:
…
├── META-INF
│ └── MANIFEST.MF
├── WEB-INF
│ ├── classes
│ │ └── com
│ │ └── packtpub
│ │ └── ge
│ │ └── hello
│ │ ├── GreetService.class
│ │ └── GreetServlet.class
│ └── greet.jsp
└── index.html
完美。编译后的类已经落在WEB-INF/classes目录中。servlet API 的 JAR 文件没有包含在内,因为它在providedCompile范围内。
提示
练习
在dependencies部分添加compile 'org.springframework:spring-context:4.0.6.RELEASE',然后执行gradle war文件,查看创建的 WAR 文件的内容。
运行 web 应用
我们在创建 web-app 方面已经走了很长的路。然而,为了使用它,它必须部署到一个 servlet 容器中。可以通过复制 servlet 容器指定目录(例如 Tomcat 中的webapps)中的.war文件来经典地部署到 servlet 容器中。或者,可以使用一种更近期的技术将 Servlet 容器嵌入到 Java 应用中,该应用被打包为.jar文件,并以任何其他java –jar命令运行。
Web 应用通常以三种模式运行,开发、功能测试和生产。所有三种模式的关键特性如下有所不同:
-
在开发模式下运行 web 的关键特性包括更快的部署(最好是热重载)、快速的服务器启动和关闭、非常低的服务器占用空间等。
-
在功能测试期间,我们通常在整个测试套件的运行中只部署一次
web-app。我们需要尽可能地模拟应用的生产类似行为。我们需要设置和销毁 web-app 的状态(如数据库),对于所有测试使用轻量级数据库(最好是内存中的)。我们还需要模拟外部服务。 -
相比之下,在生产部署中,应用服务器(无论是独立还是嵌入)的配置、安全性、应用优化、缓存等,具有更高的优先级;像热部署这样的特性很少使用;更快的启动时间具有较低的优先级。
我们将在本章中仅涵盖开发场景。我们将从传统的做法开始,以突出其问题,然后转向 Gradle 的方式。
现在,如果我们需要手动部署 war 文件。我们可以选择任何 Java Servlet 容器,例如 Jetty 或 Tomcat 来运行我们的 Web 应用。在这个例子中,让我们使用 Tomcat。假设 Tomcat 安装在 ~/tomcat 或 C:\tomcat(根据我们使用的操作系统):
-
如果服务器正在运行,理想情况下我们应该停止它。
-
将 WAR 文件复制到 Tomcat 的
webapp(~/tomcat/webapps) 目录。 -
然后,使用
~/tomcat/bin/startup.sh或C:\tomcat\bin\startup.bat启动 Tomcat 服务器。
然而,在 Gradle 的时代,这种部署方式感觉已经过时了。特别是,在开发 Web 应用时,我们必须不断将应用程序打包成 war 文件,将最新版本复制到容器中,并重新启动容器以运行最新代码。当我们说构建自动化时,这隐含着不需要手动干预,一切应该一键(或在 Gradle 的情况下,一键命令)完成。幸运的是,有许多选项可以实现这种程度的自动化。
插件拯救
默认情况下,Gradle 不支持现代 Servlet 容器。然而,这正是 Gradle 架构的美丽之处。创新和/或实现不必来自少数几个创建 Gradle 的人。借助插件 API,任何人都可以创建功能丰富的插件。我们将使用一个名为 Gretty 的插件来为我们的 Web 应用开发时间部署,但你也应该检查其他插件,看看哪个最适合你。
注意
可用 jetty 插件,该插件随 Gradle 一起提供。然而,它并未得到积极更新;因此,它官方只支持 Jetty 6.x(截至本文撰写时)。所以,如果我们的 Web 应用基于 Servlet 2.5 规范或更低版本,我们可以使用它。
Gretty 插件可以在 Gradle 插件门户中找到(请参阅以下参考)。此插件为构建添加了众多任务,并支持 Tomcat 和 Jetty 的各种版本。安装它非常简单。此代码使用与上一节相同的 hello-web 源代码,但更新了 build.gradle 文件。此示例的完整源代码可以在书籍示例代码的 chapter-03/hello-gretty 目录中找到。
只需在 build.gradle 的第一行包含以下内容:
plugins {
id "org.akhikhl.gretty" version "1.2.4"
}
就这样——我们完成了。这是 Gradle 中应用插件的新语法,它是在 Gradle 2.1 中添加的。这对于应用第三方插件特别有用。与调用 apply 方法应用插件不同,我们从第一行的插件块开始。然后,我们指定插件的 ID。对于应用外部插件,我们必须使用完全限定的插件 ID 和版本。我们可以在该块内包含 war 插件的应用。对于内部插件,我们不需要指定版本。它看起来如下:
plugins {
id "org.akhikhl.gretty" version "1.2.4"
id "war"
}
如果我们现在运行gradle tasks,必须在Gretty组下有一个appRun任务。这个组中还有许多由 Gretty 插件添加的任务。如果我们运行appRun任务,而没有明确配置插件,那么默认情况下将在http://localhot:8080上运行 Jetty 9。我们可以打开浏览器进行验证。
该插件暴露了许多配置,以便控制服务器版本、端口号等各个方面。在build.gradle文件中添加一个gretty块,如下所示:`
-
如果我们想在 8080 端口上使用 Tomcat 8,我们将添加以下代码行:
gretty { servletContainer = 'tomcat8' port = 8080 } -
如果我们想在 9080 端口上使用 Jetty 9,我们需要添加以下代码行:
gretty { servletContainer = 'jetty9' port = 9080 }
Gretty 提供了许多更多的配置选项;我们建议您查看 Gretty 的在线文档。请参阅参考文献部分中 Gretty 的链接。
运行中的应用程序看起来如下:

一旦按下提交按钮,我们将得到以下结果:

参考文献
对于 Gradle,请参考以下 URL:
- Gradle 插件门户:
plugins.gradle.org/
对于 Gretty,请参考以下 URL:
-
Gretty 文档:
akhikhl.github.io/gretty-doc/
有许多插件可用于自动化部署。其中一些列在这里:
-
Arquillian 插件:
github.com/arquillian/arquillian-gradle-plugin -
Tomcat 插件:
github.com/bmuschko/gradle-tomcat-plugin
项目依赖
在现实生活中,我们处理的应用程序比我们刚才看到的要复杂得多。这些应用程序依赖于其他专门的组件来提供某些功能。例如,企业 Java 应用程序的构建可能依赖于各种组件,如 Maven 中央的开源库、内部开发和托管的库,以及(可能)其他子项目。这些依赖项本身位于各种位置,如本地内网、本地文件系统等。它们需要被解析、下载,并引入构建的适当配置(如compile、testCompile等)。
Gradle 在定位和使依赖项在适当的classpath和打包(如果需要)中可用方面做得非常出色。让我们从最常见的依赖类型——外部库——开始。
外部库
几乎所有现实世界的项目都依赖于外部库来重用经过验证和测试的组件。这些依赖包括语言工具、数据库驱动程序、Web 框架、XML/JSON 序列化库、ORM、日志工具等等。
项目的依赖关系在构建文件中的dependencies部分声明。
Gradle 提供了声明工件坐标的极其简洁的语法。它通常采用group:name:version的形式。请注意,每个值都由冒号(:)分隔。
例如,Spring 框架的核心库可以使用以下代码引用:
dependencies {
compile 'org.springframework:spring-core:4.0.6.RELEASE'
}
注意
对于那些不喜欢简洁性的人来说,依赖项可以用更描述性的格式(称为映射格式)引用。
compile group:'org.springframework', name:'spring-core', version:'4.0.6.RELEASE'
我们也可以如下指定多个依赖项:
configurationName dep1, dep2, dep3….
其中configurationName代表配置,如compile、testCompile等,我们很快就会看到在这个上下文中配置是什么。
动态版本
我们的依赖版本会时不时地更新。此外,当我们处于开发阶段时,我们不想手动检查是否有新版本可用。
在这种情况下,我们可以添加一个+来表示上述版本,给定工件的数量。例如,org.slf4j:slf4j-nop:1.7+声明任何高于 1.7 的 SLF4J 版本。让我们将其包含在一个build.gradle文件中,并检查 Gradle 为我们带来了什么。
我们在我们的build.gradle文件中运行以下代码:
runtime 'org.slf4j:slf4j-nop:1.7+'
然后,我们运行dependencies任务:
$ gradle dependencies
…
+--- org.slf4j:slf4j-nop:1.7+ -> 1.7.7
| \--- org.slf4j:slf4j-api:1.7.7
…
我们可以看到 Gradle 选择了 1.7.7 版本,因为这是本书撰写时可用的新版版本。如果你观察第二行,它告诉我们slf4j-nop依赖于slf4j-api;因此,它是我们项目的传递依赖。
这里有一个警告,始终只使用+进行小版本升级(如前例中的1.7+)。让主版本自动更新(例如,想象 Spring 自动从 3 更新到 4,compile 'org.springframework:spring-core:+')不过是一场赌博。动态依赖解析是一个很好的功能,但应该谨慎使用。理想情况下,它应该只在项目的开发阶段使用,而不是用于发布候选版本。
当依赖项的版本更新到与我们的应用程序不兼容的版本时,我们会得到一个不稳定的构建。我们应该追求可重复的构建,这样的构建应该产生完全相同的工件,无论是今天还是一年后。
传递依赖
默认情况下,Gradle 非常智能地解析传递依赖,如果存在,优先选择最新的冲突版本。然而,由于某种原因,如果我们想禁用传递依赖,我们只需要在我们的依赖声明中提供一个额外的块:
runtime ('org.slf4j:slf4j-nop:1.7+') {
transitive = false
}
现在,如果我们检查dependencies任务的输出,我们会看到不再包含其他依赖项:
\--- org.slf4j:slf4j-nop:1.7.2
我们也可以强制指定库的给定版本,即使有相同的工件,较新版本通过传递依赖项获取;我们强制指定的版本将获胜:
runtime ('org.slf4j:slf4j-nop:1.7.2') {
force = true
}
现在运行依赖项任务将产生:
+--- org.slf4j:slf4j-api:1.7.2
\--- org.slf4j:slf4j-nop:1.7.7
\--- org.slf4j:slf4j-api:1.7.7 -> 1.7.2
这表明较旧的slf4j-api版本获胜,即使可以通过传递依赖项获取较新版本。
依赖项配置
Gradle 提供了一种非常优雅的方式来声明在项目构建的不同阶段构建不同源代码组所需的依赖项。
小贴士
这些源代码组被称为源集。源集最简单和最易于理解的例子是main和test。main源集包含将被编译和构建为 JAR 文件并部署到某处或发布到某个仓库的文件。另一方面,test源集包含将由 JUnit 等测试工具执行的文件,但不会进入生产环境。现在,这两个源集对依赖项、构建、打包和执行有不同的要求。我们将在第七章中看到如何添加新的源集,使用 Gradle 进行测试和报告,以进行集成测试。
由于我们在源集中定义了相关源代码的组,因此依赖项也被定义为名为配置的组。每个配置都有自己的名称,例如编译、测试编译等。包含在各种配置中的依赖项也有所不同。配置是根据依赖项的特性进行分组的。例如,以下是由java和war插件添加的配置:
-
编译:这是由java插件添加的。向此配置添加依赖意味着该依赖项是编译源代码所必需的。在war的情况下,这些依赖项也将被复制到WEB-INF/lib中。此类依赖项的例子包括 Spring 框架、Hibernate 等库。 -
运行时:这是由java插件添加的。默认情况下,这包括编译依赖项。此组中的依赖项在运行时对于编译的源代码是必需的,但它们不是编译它的必需品。例如,JDBC 驱动程序这样的依赖项仅是运行时依赖项。我们不需要它们在我们的类路径上编译源代码,因为我们针对的是 JDK 中可用的标准 JDBC API 接口进行编码。然而,为了我们的应用程序能够正常运行,我们需要在运行时特定的驱动程序实现。例如,runtime 'mysql:mysql-connector-java:5.1.37'包括 MySQL 驱动程序。 -
testCompile:这是由java插件添加的。默认情况下,这包括compile依赖项。添加到该配置的依赖项仅对测试源可用。例如,JUnit、TestNG 等测试库,或者仅由测试源使用的任何库,如 Mockito。它们既不需要编译,也不需要在主源集的运行时存在。在构建web-app的情况下,它们不会包含在war中。 -
testRuntime:这是由java插件添加的。默认情况下,这包括testCompile和runtime依赖项。此配置中的依赖项仅需要在运行时(即在运行测试时)测试源。因此,它们不包括在测试的编译类路径中。这就像运行时配置一样,但仅限于测试源。 -
providedCompile:这是由war插件添加的。例如,servlet API 这样的依赖项由应用程序服务器提供,因此不需要打包在我们的war中。任何我们期望已经包含在服务器运行时中的内容都可以添加到这个配置中。然而,它必须在源代码编译时存在。因此,我们可以将这些依赖项声明为providedCompile。例如,servlet API 和任何在服务器运行时提供的 Java EE 实现。此类依赖项不会包含在war中。 -
providedRuntime:这是由war插件添加的。服务器和应用程序将在应用运行时提供的依赖项不需要在编译时包含,因为没有直接引用实现。此类库可以添加到该配置中。此类依赖项将不会包含在war中。因此,我们应该确保在应用运行时有实现可用。
如我们所知,当我们应用 war 插件时,java 插件也会被应用。这就是为什么当我们构建 Web 应用程序时,所有六个配置都可用。可以通过插件添加更多配置,或者我们可以在构建脚本中自行声明它们。
有趣的是,配置不仅包括依赖项,还包括由该配置生成的工件。
仓库
仓库部分配置了 Gradle 将在其中查找依赖项的仓库。Gradle 将依赖项下载到其自己的缓存中,这样就不需要在每次运行 Gradle 时都进行下载。我们可以按以下方式配置多个仓库:
repositories {
mavenCentral() // shortcut to maven central
mavenLocal() // shortcut to maven local (typically ~/.m2)
jcenter() // shortcut to jcenter
maven {
url "http://repo.company.com/maven"
}
ivy {
url "http://repo.company.com/ivy"
}
flatDir { // jars kept on local file system
dirs 'libDir'
}
}
支持使用 Maven、Ivy 和平面目录(文件系统)等仓库进行依赖项解析和上传工件。对于常用的 Maven 仓库,如 mavenCentral()、jcenter() 和 mavenLocal(),还有一些更具体的便捷方法可用。然而,可以使用以下语法轻松配置更多 Maven 仓库:
maven {
url"http://intranet.example.com/repo"
}
在中央仓库之前,项目通常会在文件系统中管理库,这些库大多与源代码一起提交。有些项目仍然这样做;尽管我们不建议这样做,但人们有自己的理由这样做,而 Gradle 没有不支持的理由。
需要记住的是,Gradle 并不会自动假设任何仓库来搜索和下载依赖项。我们必须在 repositories 块中显式配置至少一个仓库,Gradle 将在其中搜索工件。
提示
练习
使用以下方法将 Apache Commons Lang 库包含到消息中,以将其转换为标题大小写:
WordUtils.capitalize(String str)
将字符串中所有空格分隔的单词转换为大写。
总结
在本章中,我们首先使用 Gradle 开发了一个 Web 应用程序。我们通过构建应用程序生成了 WAR 工件,然后将其部署到本地 Tomcat。然后,我们学习了关于 Gradle 中的依赖管理、配置和支持的仓库的一些基础知识。
注意
读者应该在 Gradle 的官方文档中花更多时间详细阅读这些概念,官方文档地址为 docs.gradle.org/current/userguide/userguide。
目前,我们应该能够使用 Gradle 构建最常见类型的 Java 应用程序。在下一章中,我们将尝试理解 Gradle 提供的 Groovy DSL 以及基本的项目模型。
第四章 揭秘构建脚本
在前三章中,我们看到了 Gradle 通过在构建文件中添加几行代码就能为我们的构建添加许多有趣的功能。然而,这只是冰山一角。我们所探索的多数是 Gradle 随附的插件添加的任务。从我们的经验来看,我们知道项目构建永远不会这么简单。无论我们多么努力避免,它们都会有定制化。这就是为什么对于构建工具来说,添加自定义逻辑的能力极其重要。
此外,Gradle 的美也恰恰在于此。当我们决定扩展现有功能或完全偏离传统,想要做一些非常规的事情时,Gradle 不会妨碍我们。如果我们想在构建中添加一些逻辑,我们不需要编写 XML 汤或一大堆 Java 代码。我们可以创建自己的任务或扩展现有任务以做更多的事情。
这种灵活性伴随着学习 Groovy DSL 的非常温和的学习曲线。在本章中,我们将了解 Gradle 构建脚本的语法以及 Gradle 的一些关键概念。我们将涵盖以下主题:
-
一本 Groovy 入门指南,将帮助我们理解 Gradle 构建脚本的语法
-
在我们的构建中可用的两个重要对象,即
project对象和task对象(们) -
构建阶段和生命周期回调
-
任务的一些细节(任务执行和任务依赖)
Groovy 用于 Gradle 构建脚本
要熟练使用 Gradle 并编写有效的构建脚本,我们需要了解一些 Groovy 的基础知识,Groovy 本身是一种出色的动态语言。如果我们对动态语言如 Ruby 或 Python 有任何经验,再加上 Java,我们将对 Groovy 感到非常熟悉。如果没有,只要知道大多数 Java 语法也是有效的 Groovy 语法,就足以让我们对 Groovy 感到高兴,因为我们可以从第一天开始编写 Groovy 代码并变得高效,而不需要学习任何新东西。
对于没有准备的人来说,Gradle 脚本可能一开始看起来有点难以理解。Gradle 构建脚本不仅使用 Groovy 语法,还使用一个丰富且具有表达性的 DSL,它提供了高级抽象来表示常见的构建相关逻辑。让我们快速看一下是什么让 Groovy 成为编写构建文件的一个很好的选择。
注意
使用 Groovy 编写构建逻辑并不新鲜。Gant 和 GMaven 已经使用 Groovy 来编写构建逻辑,以利用 Groovy 的简洁和表达性。GMavenPlus 是 GMaven 的继任者。它们所基于的工具,即 Ant 和 Maven,分别限制了 Gant 和 GMaven。
与其依赖现有工具仅为了添加语法增强,Gradle 是通过利用从过去工具中学到的经验来设计的。
为什么选择 Groovy?
Gradle 的核心主要用 Java 编写(见下文信息)。Java 是一种伟大的语言,但并不是编写脚本的最好选择。想象一下用 Java 编写脚本,我们可能需要为我们的主项目定义构建编写另一个项目,因为 Java 的冗长和仪式感。在上一代构建工具(Ant 和 Maven)中广泛使用的 XML,对于声明性部分来说是可以的,但不是编写逻辑的最佳选择。
注意
我们可以从 GitHub 下载 Gradle 的源代码,网址为 github.com/gradle/gradle。
Groovy 是 Java 的动态化身。如前所述,大多数 Java 语法也是有效的 Groovy 语法。如果我们知道 Java,我们就可以编写 Groovy 代码。考虑到现在能够编写 Java 的人数众多,这是一个很大的优势。
Groovy 的语法简洁、表达性强且功能强大。Groovy 是动态风味和类型使用的完美结合。它是少数几种具有可选类型特性的语言之一,即如果我们想提供类型信息,就可以提供;如果我们不想提供,就可以不提供。由于 Groovy 支持一等 lambda 和元编程功能,它是一种构建内部 DSL 的优秀语言。所有这些因素使它成为编写构建脚本的最佳候选人之一。
Groovy 入门
虽然我们可以在 Groovy 中编写 Java 风格的代码,但如果我们在学习语言动态特性和 Groovy 提供的一些语法增强上投入一些时间,我们将能够编写更好的 Gradle 构建脚本和插件。如果我们还不了解 Groovy,这将是一件很有趣的事情。
让我们学习足够的 Groovy,以便能够正确理解 Gradle 脚本。我们将快速浏览 Groovy 的几个语言特性。
强烈建议尝试执行以下子节中的代码。此外,自己编写和尝试更多代码以探索 Groovy 将有助于我们加强语言基础的理解。本指南绝非详尽无遗,只是为了让 Groovy 的发展迈出第一步。
运行 Groovy 代码
最简单且推荐的方式是在本地安装最新的 Groovy SDK。Groovy 代码片段可以使用以下任何一种选项执行:
-
将代码片段保存到
.groovy脚本中,然后使用以下代码在命令行中运行:groovy scriptname.groovy -
我们可以使用随 Groovy 安装包一起提供的 Groovy 控制台 GUI 来编辑和运行脚本。
-
我们还可以使用 Groovy shell,这是一个用于执行或评估 Groovy 语句和表达式的交互式 shell。
如果我们不希望在本地上安装 Groovy,那么:
-
我们可以使用 Groovy 控制台在浏览器中在线运行 Groovy 代码,网址为
groovyconsole.appspot.com。 -
我们还可以通过创建任务并将代码片段放入其中(我们也可以将它们放在任何任务之外,它仍然会在配置阶段运行它们)在构建脚本中运行 Groovy 代码。
变量
在 Groovy 脚本中,def 关键字可以定义一个变量(取决于上下文):
def a = 10
然而,a 的类型是在运行时根据它指向的对象类型来决定的。大致来说,声明为 def 的引用可以指向任何 Object 或其子类。
声明一个更具体的类型同样有效,并且应该在我们想要有类型安全时使用:
Integer b = 10
我们也可以使用 Java 原始数据类型,但请注意,在 Groovy 中它们实际上不是原始类型。它们仍然是第一类对象,实际上是针对相应数据类型的 Java 包装类。让我们通过以下示例来确认:
int c = 10
println c.getClass()
它打印以下输出:
class java.lang.Integer
这表明 c 是一个对象,因为我们可以在它上面调用方法,并且 c 的类型是 Integer。
我们建议尽可能使用特定类型,因为这增加了可读性,并帮助 Groovy 编译器通过捕获无效赋值来早期检测错误。这也帮助 IDE 进行代码补全。
字符串
与 Java 不同,单引号('')是字符串字面量,而不是 char:
String s = 'hello'
当然,Java 的常规字符串字面量("")也可以使用,但在 Groovy 中它们被称为 GStrings。它们具有字符串插值或变量或表达式的内联展开的附加功能:
def name = "Gradle"
println "$name is an awesome build tool"
这将打印以下输出:
Gradle is an awesome build tool
``\({var}` 和 `\)var 都是有效的,但包装(${}`)更适合,并且在复杂或较长的表达式中是必需的。例如:
def number = 4
println "number is even ? ${number % 2 == 0 }"
它将打印以下内容:
number is even ? true
我们所有人都记得在 Java 中,为了产生多行字符串,需要在每一行的末尾添加 + "\\n"。那些日子已经过去了,因为 Groovy 支持多行字符串字面量。多行字面量以三个单引号或双引号开始(与 GString 功能相同),并以三个单引号或双引号结束:
def multilineString = '''\
Hello
World
'''
println multilineString
它将打印以下内容:
Hello
World
第 1 行上的正斜杠是可选的,用于排除第一个新行。如果我们不放置正斜杠,输出开头将会有一个额外的空行。
此外,查看 stripMargin 和 stripIndent 方法以了解对前导空白的特殊处理。
如果我们的字面量包含大量的转义字符(例如,正则表达式),那么我们最好使用“斜杠”字符串字面量,它以单个正斜杠(/)开始和结束:
def r = /(\d)+/
println r.class
它将打印以下内容:
class java.lang.String
在上面的例子中,如果我们必须使用普通字符串,那么我们不得不在字符类 d 前面的反斜杠进行转义。它看起来如下所示:
"(\\d)+"
正则表达式
Groovy 支持一个模式操作符(~),当应用于字符串时,会给出一个模式对象:
def pattern = ~/(\d)+/
println pattern.class
它打印以下内容:
class java.util.regex.Pattern
我们还可以使用查找操作符直接将字符串与模式匹配:
if ("groovy" ==~ /gr(.*)/)
println "regex support rocks"
它将打印以下内容:
regex support rocks
闭包
Groovy 中的闭包是一段代码块,它可以被赋值给引用或像任何其他变量一样传递。这个概念在许多其他语言中被称为lambda,包括 Java 8 或函数指针。
注意
Lambda 自 Java 8 以来一直被支持,但其语法与 Groovy 闭包的语法略有不同。你不需要使用 Java 8 就可以在 Groovy 中使用闭包。
如果我们没有接触过上述任何一种,那么为了很好地理解这个概念,就需要进行一些详细的阅读,因为它为后续许多高级主题奠定了基础。闭包本身就是一个很大的主题,深入的讨论超出了本书的范围。
闭包几乎就像一个常规方法或函数,但它也可以被赋值给变量。由于它可以被赋值给变量,它也必须是一个对象;因此,它将有自己的方法:
def cl1 = {
println "hello world!"
}
在这里,代码块被赋值给一个名为cl1的变量。现在,代码块可以在未来的调用方法中执行,或者cl1变量可以在以后传递并执行:
cl1.call()
毫不奇怪,它打印了以下内容:
hello world!
由于闭包就像方法一样,它们也可以接受参数:
def cl2 = { n ->
println "value of param : $n"
}
cl2.call(101)
它打印了以下内容:
value of param : 101
就像方法一样,它们也可以返回值。如果没有显式声明return语句,闭包的最后一个表达式将自动返回。
当我们有接受闭包的方法时,闭包开始发光。例如,times方法在整数上可用,它接受一个闭包并执行它,次数与整数的值相同;每次调用时,它将当前值作为如果我们是从0循环到该值一样传递:
3.times(cl2)
它打印了以下内容:
value of param : 0
value of param : 1
value of param : 2
我们还可以将代码块内联并直接传递给方法:
3.times { println it * it }
它打印了以下内容:
0
1
4
有一个特殊的变量叫做it,如果闭包没有定义其参数,它将在代码块的作用域内可用。在上面的例子中,我们使用it访问传递给代码块的数量,并将其自身相乘以获得其平方。
闭包在诸如回调处理等情况下非常有用,而在 Java 7 及以下版本中,我们不得不使用匿名接口实现来达到相同的结果。
数据结构
Groovy 支持常用数据结构的字面量声明,这使得代码更加简洁,同时又不牺牲可读性。
列表
Groovy 依赖于经过充分测试的 Java Collection API,并在底层使用相同的类,但增加了一些额外的方法和语法糖:
def aList = []
println aList.getClass()
它打印了以下内容:
class java.util.ArrayList
注意
在 Groovy 中,[] 实际上是 Java 的 List 实例,而不是数组。
让我们创建另一个带有一些初始内容的列表:
def anotherList = ['a','b','c']
多亏了运算符重载,我们可以在列表上直观地使用许多运算符。例如,使用anotherList[1]将给我们b。
下面是一些更多有用的运算符的例子。这个例子将两个列表相加,并将结果赋值给列表变量:
def list = [10, 20, 30] + [40, 50]
这会将 60 添加到列表中:
list << 60
以下两个示例简单地从一个列表中减去另一个列表:
list = list – [20, 30, 40]
list -= [20,30,40]
遍历列表同样简单直观:
list.each {println it}
它将打印以下内容
10
50
60
传递给 each 的闭包为列表中的每个元素执行,元素作为闭包的参数。因此,前面的代码遍历列表并打印每个元素的值。注意 it 的使用,它是列表当前元素的句柄。
集合
定义一个集合与列表类似,但除此之外,我们必须使用 as Set:
def aSet = [1,2,3] as Set
println aSet.class
这将打印以下内容:
class java.util.LinkedHashSet
由于选择的实现类是 LinkedHashSet,aSet 将保持插入顺序。
或者,声明变量的类型以获取正确的实现:
TreeSet anotherSet = [1,2,3]
println anotherSet.class
这将打印以下内容:
class java.util.TreeSet
向集合中添加元素就像使用间接操作符的列表一样。其他集合接口方法也是可用的:
aSet << 4
aSet << 3
println aSet
这将打印以下内容:
[1, 2, 3, 4]
由于集合是集合实现,它按定义消除了重复,所以我们看不到条目 4 两次。
映射
映射是任何动态语言最重要的数据结构之一。因此,它在 Groovy 的语法中得到了应有的位置。映射可以使用映射字面量 [:] 来声明:
def a = [:]
默认选择的实现是 java.util.LinkedHashMap,它保留了插入顺序:
def tool = [version:'2.8', name:'Gradle', platform:'all']
注意,键不是字符串字面量,但它们会自动转换为字符串:
println tool.name
println tool["version"]
println tool.get("platform")
我们可以通过索引操作符和点操作符以及普通的 get() 方法来访问值。
我们可以使用索引操作符、点操作符以及当然的 put() 方法在映射中添加和更新数据:
tool.version = "2.9"
tool["releaseDate"] = "2015-11-17"
tool.put("platform", "ALL")
方法
以下更像是 Java 类型的方法,当然这也是一个有效的 Groovy 方法:
int sum(int a, int b) {
return a + b;
}
前面的方法可以简洁地重写如下:
def sum(a, b) {
a + b
}
我们没有指定返回类型,只是声明了 def,这意味着方法可以返回任何 Object 或子类引用。然后,我们省略了形式参数的类型,因为对于方法的形式参数来说,声明 def 是可选的。在第 2 行,我们省略了 return 语句,因为方法的最后一个表达式的评估会自动返回。我们还省略了分号,因为它也是可选的。
这两个示例都是有效的 Groovy 方法声明。然而,建议读者明智地选择类型,因为它们提供了类型安全,并作为方法的活文档。如果我们没有声明参数的类型,就像前面的方法一样,求和 (1,"2") 也将成为一个有效的方法调用,而且更糟糕的是,它返回了没有异常的意外结果。
调用方法
在 Groovy 中,在许多情况下可以省略方法调用中的括号。以下两种情况都是有效的方法调用。
sum(1,2)
sum 1, 2
参数的默认值
有很多次,我们想要通过提供一个默认值来使参数可选,这样如果调用者没有提供值,则使用默认值。看看以下示例:
def divide(number, by=2) {
number/by
}
println divide (10, 5)
println divide (10)
它打印以下内容:
2
5
如果我们提供了将要使用的 by 参数的值,则默认值 2 将被假定为该参数。
带有 map 参数/命名参数的方法
Groovy 不支持像 Python 那样的命名参数,但 Map 提供了对相同功能的非常接近的近似:
def method(Map options) {
def a = options.a ?: 10
def b = options.b ?: 20
}
在前面的代码中,我们期望 map 包含键 a 和 b。
注意
在第 2 行和第 3 行,注意 elvis 运算符 ?:,如果存在值且是 truthy,则返回左侧值;否则返回右侧(默认)值。它基本上是以下代码的简写:
options.a ? options.a : 10
现在,这个方法可以这样调用:
method([a:10,b:20])
我们可以省略方括号 ([]),因为 maps 在方法调用中有特殊支持:
method(a:10, b:20)
现在,它看起来明显像是命名参数。参数的顺序并不重要,并且不需要传递所有参数。此外,括号包裹是可选的,就像任何方法调用一样:
method b:30, a:40
method b:30
带有 varags 的方法
如同 Java 一样,varags 用 ... 表示,但提供类型是可选的:
def sumSquares(...numbers) {
numbers.collect{ it * it }.sum()
}
sumSquares 1, 2, 3
在前面的示例中,数字是数组,它具有接受闭包并按顺序转换集合中每个元素的 collect 方法,以产生一个新的集合。在这种情况下,我们转换集合中的平方数。最后,我们使用内置的 sum 方法来求所有平方的和。
带闭包参数的方法
闭包很重要,因此 Groovy 如果闭包是方法签名中的最后一个参数,则具有特殊的闭包语法:
def myMethod (param, cls) {
...
}
然后,这个方法可以这样调用:
myMethod(1,{ ... })
myMethod 2, {... }
myMethod(3) {...}
在这些中,第三个是特殊的语法支持,其中括号仅包裹其他参数,而闭包则写在括号外,就像是一个方法体。
类
Groovy 中的类声明与 Java 类类似,但仪式较少。类默认是公共的。它们可以使用 extends 从其他类继承,或者使用 implmenets 实现接口。
以下是一个非常简单的类 Person 的定义,它有两个属性,name 和 age:
class Person {
def name, age
}
我们可以使用更具体的类型来代替 def 用于属性。
构造函数
除了默认构造函数之外,Groovy 中的类还有一个特殊的构造函数,它接受类的属性映射。以下是它的用法:
def person = new Person(name:"John Doe", age:35)
在前面的代码中,我们使用特殊构造函数创建了 person 对象。参数是键值对,其中键是类中属性的名称。为键提供的值将设置对应的属性。
属性
Groovy 在语言级别上支持属性。在前面的类中,name 和 age,与 Java 不同,不仅仅是字段,而且还是具有其 getter 和 setter 的类的属性。字段默认是私有的,它们的公共访问器和修改器(getter 和 setter)是自动生成的。
我们可以在上面创建的person对象上调用getAge()/setAge()和getName()/setName()方法。然而,有一个更简洁的方法可以做到这一点。我们可以像访问公共字段一样访问属性,但幕后 Groovy 会通过 getter 和 setter 进行路由。让我们试试:
println person.age
person.age = 36
println person.age
它打印以下内容:
35
36
在前面的代码中,在第 1 行,person.age实际上是调用person.getAge(),因此它返回了人的年龄。然后,我们使用person.age和右侧的赋值运算符及值更新了年龄。我们没有更新字段,但它内部通过 setter setAge()进行传递。这之所以可能,是因为 Groovy 提供了对属性的语法支持。
我们可以为所需的字段提供自己的 getter 和/或 setter,这将优先于生成的那个,但只有在我们需要在这些地方编写一些逻辑时才是必要的。例如,如果我们想设置一个年龄的正值,那么我们可以提供自己的setAge()实现,并且每次更新属性时都会使用它:
void setAge(age){
if (age < 0)
throw new IllegalArgumentException("age must be a positive number")
else
this.age = age
}
属性的支持导致类定义中的样板代码显著减少,并增强了可读性。
小贴士
属性在 Groovy 中是一等公民。从现在开始,每次我们提到属性时,不要在属性和字段之间混淆。
实例方法
我们可以向类中添加实例方法和静态方法,就像我们在 Java 中做的那样:
def speak(){
println "${this.name} speaking"
}
static def now(){
new Date().format("yyyy-MM-dd HH:mm:ss")
}
如我们上面所讨论的,方法部分没有使用类,而是直接应用于类内部的方法。
注意
脚本即类
实际上,我们上面讨论的方法都在类内部,它们不是自由浮动的函数。由于脚本被透明地转换为类,所以我们感觉就像在使用函数。
我相信到目前为止你已经享受了 Groovy。Groovy 还有很多内容可以介绍,但我们不得不将焦点转回到 Gradle 上。然而,我希望我已经激发了你足够的对 Groovy 的好奇心,这样你就可以欣赏它作为一种语言,并自己探索更多。参考资料部分包含了一些很好的资源。
再次看看应用插件
现在我们已经了解了基本的 Groovy,让我们将其应用于 Gradle 构建脚本的环境中。在早期章节中,我们已经看到了应用插件的语法。它看起来如下所示:
apply plugin: 'java'
如果我们仔细观察,apply是一个方法调用。我们可以将参数括在括号中:
apply(plugin: 'java')
一个接收映射的方法可以像命名参数一样传递键值。然而,为了更清晰地表示映射,我们可以将参数用[]括起来:
apply([plugin: 'java'])
最后,apply方法在project对象上隐式应用(我们将在本章接下来的部分中看到这一点)。因此,我们也可以在project对象的引用上调用它:
project.apply([plugin: 'java'])
因此,从前面的例子中,我们可以看到将插件应用于项目的声明仅仅是一个语法糖,它实际上是对 project 对象的方法调用。我们只是在使用 Gradle API 编写 Groovy 代码。一旦我们意识到这一点,我们对理解构建脚本语法的看法就会发生根本性的改变。
Gradle – 一个面向对象的构建工具
如果我们以面向对象的方式思考构建系统,以下类会立刻浮现在我们的脑海中:
-
一个代表正在构建的系统的
project -
一个封装需要执行的部分构建逻辑的
task
好吧,我们很幸运。正如我们所预期的那样,Gradle 创建了 project 和 task 类型的对象。这些对象在我们的构建脚本中是可访问的,以便我们进行自定义。当然,其底层实现并不简单,API 非常复杂。
project 对象是暴露给构建脚本并配置的 API 的核心部分。在脚本中,project 对象是可用的,这样就可以在 project 对象上智能地调用没有对象引用的方法。我们已经在上一节中看到了一个例子。大多数构建脚本语法都可以通过阅读项目 API 来理解。
task 对象是为在构建文件中直接声明的每个任务以及插件而创建的。我们已经在 第一章 中创建了一个非常简单的任务,即 运行您的第一个 Gradle 任务,并在 第二章 和 第三章 中使用了来自插件的任务,即 构建 Java 项目 和 构建 Web 应用程序。
注意
正如我们所见,一些任务在我们的构建文件中已经可用,我们无需在构建文件中添加任何一行代码(例如 help 任务和 tasks 任务等)。即使是这些任务,我们也会有任务对象。
我们很快就会看到这些对象是如何以及何时被创建的。
构建阶段
每次调用时,Gradle 构建都会遵循一个非常简单的生命周期。构建过程会经过三个阶段:初始化、配置和执行。当调用 gradle 命令时,并非我们构建文件中编写的所有代码都会按顺序从上到下依次执行。只有与当前构建阶段相关的代码块会被执行。此外,构建阶段的顺序决定了代码块何时执行。例如,任务配置与任务执行。理解这些阶段对于正确配置我们的构建非常重要。
初始化
Gradle 首先确定当前项目是否有子项目,或者它是否是构建中的唯一项目。对于多项目构建,Gradle 确定哪些项目(或子模块,许多人更喜欢这样称呼它们)需要包含在构建中。我们将在下一章中看到多项目构建。然后,Gradle 为根项目和每个项目的子项目创建一个Project实例。对于到目前为止我们所看到的单模块项目,在这个阶段没有太多可以配置的。
配置
在这个阶段,参与项目的构建脚本将与初始化阶段创建的相应项目对象进行评估。在多模块项目中,评估是按广度进行的,也就是说,在评估和配置子项目之前,所有兄弟项目都将被评估和配置。然而,这种行为是可以配置的。
注意,执行脚本并不意味着任务也被执行。为了快速验证这一点,我们只需在build.gradle文件中添加一个println语句,并创建一个打印消息的任务:
task myTask << {
println "My task is executed"
}
// The following statement will execute before any task
println "build script is evaluated"
如果我们执行以下代码:
$ gradle -q myTask
我们将看到以下输出:
build script is evaluated
My task is executed
实际上,选择任何内置任务也可以,例如help:
$ gradle -q help
在任何任务执行之前,我们仍然会看到我们的build script is evaluated消息。为什么是这样?
当一个脚本被评估时,脚本中的所有语句都会按顺序执行。这就是为什么根级别的println语句会被执行。如果你注意到,任务动作实际上是一个闭包;因此,它仅在语句执行期间附加到任务上。然而,闭包本身尚未执行。动作闭包内的语句只有在任务执行时才会执行,这发生在下一个阶段。
任务仅在此时进行配置。无论将要调用哪些任务,所有任务都将被配置。Gradle 为任务准备了一个有向无环图(DAG)表示,以确定任务的依赖关系和执行顺序。
执行
在这个阶段,Gradle 根据诸如通过命令行参数传递的任务名称和当前目录等参数确定需要运行的任务。这就是任务动作将被执行的地方。因此,在这里,如果任务要运行,动作闭包实际上会执行。
注意
在后续调用中,Gradle 智能地确定哪些任务实际上需要运行,哪些可以跳过。例如,对于编译任务,如果源文件在最后一次构建后没有变化,再次编译就没有意义了。在这种情况下,执行可能会被跳过。我们可以在输出中看到这样的任务,标记为UP-TO-DATE:
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
在前面的输出中,由于与前一次构建没有变化,Gradle 实际上跳过了每个任务。然而,这不会发生在我们编写的自定义任务上,除非我们告诉 Gradle 确定任务是否需要执行的逻辑。
生命周期回调
Gradle 在生命周期事件的不同阶段提供了各种钩子来执行代码。我们可以在构建脚本中实现回调接口或提供回调闭包。例如,我们可以使用project上的beforeEvaluate和afterEvaluate方法来监听项目评估前后的事件。我们不会逐一查看它们,但Project和Gradle(接口名称不要与工具本身的名称混淆)API 以及 DSL 文档是检查可用回调的正确地方,如果我们觉得需要实现生命周期回调的话。
Gradle 项目 API
如前所述,Gradle 在初始化阶段为我们为每个build.gradle创建了一个project对象。这个对象可以通过project引用在我们的构建脚本中使用。作为一个 API 的核心部分,这个对象上有许多方法和属性。
项目方法
我们甚至没有意识到,我们已经在调用project对象的methods了,尽管我们一直在使用项目 API。根据一些管理规则,如果未提供明确的引用,构建脚本中的所有顶级方法调用都是在项目对象上进行的。
让我们重写第一章中非常简单的构建文件,即运行您的第一个 Gradle 任务,以使用项目引用进行方法调用:
project.apply plugin: 'java'
project.repositories {
mavenCentral()
}
project.dependencies {
testCompile 'junit:junit:4.11'
}
正如我们在本章前面看到的,apply是project上的一个方法。所谓的dependencies块实际上是在project上接受闭包的dependencies()方法。对于repositories部分也是如此。我们可以将闭包块用括号括起来,使其看起来像是一个普通的方法调用:
project.repositories({...})
project.dependencies({...})
在这个对象上还有许多其他有趣的方法,我们将在接下来的章节中看到,这些方法要么有,要么没有对project对象的明确引用。
项目属性
在project对象上有几个属性可用。一些属性是只读属性,如name、path、parent等,而其他属性则是可读可写的。
例如,我们可以设置project.description来提供项目的描述。我们可以使用project.version属性来设置项目的版本。这个版本将被其他任务,如Jar使用,以在生成的工件中包含版本号。
注意
我们不能从build.gradle文件中更改project.name,但我们可以使用同一项目中的settings.gradle来设置项目名称。当我们学习多项目构建时,我们将更详细地了解这个文件。
除了直接通过名称访问属性外,我们还可以使用以下在project对象上的方法来访问属性。
要检查属性是否存在,请使用以下方法:
boolean hasProperty(String propertyName)
要获取给定属性名的属性值,请使用以下方法:
Object property(String propertyName)
要设置给定属性名的属性值,请使用以下方法:
void setProperty(String name, Object value)
例如,让我们创建一个包含以下内容的build.gradle文件:
description = "a sample project"
version = "1.0"
task printProperties << {
println project.version
println project.property("description")
}
执行以下任务:
$ gradle -q printProperties
1.0
a sample project
如前所述,在 Groovy 中,我们可以使用property = value语法来调用 setter。我们在project对象上设置了description和version属性。然后,我们添加了一个任务,该任务使用project引用和description使用project对象上的property()方法来打印版本。
我们在上面看到的属性必须在项目上存在,否则构建将失败,并显示Could not find property …消息。
项目上的额外属性
Gradle 使得在项目上存储用户定义的属性变得非常容易,同时仍然能够享受项目属性语法的便利。我们只需要使用ext命名空间将值分配给自定义属性。然后,这个属性可以在项目上像常规项目属性一样访问。以下是一个示例:
ext.abc = "123"
task printExtraProperties << {
println project.abc
println project.property("abc")
println project.ext.abc
}
执行以下任务:
$ gradle -q printExtraProperties
123
123
123
在前面的示例中,我们声明了一个名为abc的自定义属性,并将其赋值为123。我们没有使用project引用,因为它在脚本根级别上是隐式可用的。在任务操作中,我们首先使用项目引用直接打印它,就像它是在Project上的一个属性一样。然后,我们使用property()方法和project.ext引用来访问它。请注意,在任务的动作闭包内部,我们应该使用project引用以避免任何歧义。
额外的属性将在子项目(模块)中可用。也可以在其他对象上设置额外的属性。
注意
我们本可以使用局部变量,通过使用def声明它。然而,这样的变量在词法作用域之外不可访问。此外,它们也不可查询。
尽管我们已经查看了一些方法和属性,但在这里涵盖所有这些是不切实际的;因此,花些时间阅读project接口的 API 和 DSL 文档是值得的。
任务
如我们所见,task是一个执行某些构建逻辑的命名操作。它是一个构建工作的单元。例如,clean、compile、dist等,如果我们必须为我们自己的项目编写任务,这些是典型的构建任务,很容易浮现在我们的脑海中。任务在某种程度上类似于 Ant 的目标。
创建任务的简单方法如下:
task someTask
在我们进一步讨论任务之前,让我们花点时间思考一下任务创建。
我们使用了语句的taskName任务形式。
如果我们将其重写为task (taskName),它将立即看起来像方法调用。
我们可能已经猜到的,前面的方法在项目对象上是可用的。
因此,我们也可以写出以下内容之一:
-
project.task "myTask" -
project.task("myTask")
注意,在后面的示例中,我们必须传递任务名称作为字符串。task taskName 是一种特殊形式,我们可以使用 taskName 作为字面量而不是字符串。这是通过 Groovy AST 转换魔法实现的。
项目有几种创建任务对象的 task 方法:
Task task(String name)
Task task(String name, Closure configureClosure)
Task task(Map<String, ?> args, String name)
Task task(Map<String, ?> args, String name, Closure configureClosure)
然而,本质上,我们可以在创建任务时传递一些键值作为命名参数,并传递一个配置闭包来配置任务。
我们实际上创建了一个 Task 类型(确切类名现在并不重要)的对象。我们可以查询属性并在此对象上调用方法。Gradle 优雅地使这个 task 对象可用于使用。在优雅的 DSL 之下,我们实际上正在编写一个脚本,以优雅的面向对象方式创建构建逻辑。
将操作附加到任务
一个 Task 对象,例如上面创建的,实际上并没有做什么。事实上,它没有附加任何操作。我们需要将操作附加到 Task 对象上,以便 Gradle 在运行任务时执行这些操作。
一个 Task 对象有一个名为 doLast 的方法,它接受一个闭包。Gradle 确保传递给此方法的闭包按它们传递的顺序执行:
someTask.doLast({
println "this should be printed when the task is run"
})
我们现在可以再次调用 doLast:
someTask.doLast({
println "this should ALSO be printed when the task is run"
})
此外,在另一种语法中:
someTask {
doLast {
println "third line that should be printed"
}
}
有多种方法可以向任务添加 doLast 逻辑,但最符合习惯用法,也许是最简洁的方法如下:
someTask << {
println "the action of someTask"
}
就像 Project 对象一样,我们有一个 Task 对象,其中方法和属性是可访问的。然而,与 Project 对象不同,它不是在脚本的顶层隐式可用,而仅在任务的配置范围内。直观地说,我们可以认为每个 build.gradle 将会有多个 Task 对象。我们将在稍后看到访问 Task 对象的各种方法。
任务流程控制
项目内的任务可能相互依赖。在本节中,我们将看到项目任务中可能存在不同类型的关系。
dependsOn
有一些任务的执行依赖于其他任务的成功完成。例如,为了创建可分发的 JAR 文件,代码应该首先被编译,并且 "class" 文件应该已经存在。在这种情况下,我们不希望用户明确指定所有任务及其顺序,如下所示:
$ gradle compile dist
这可能会导致错误。我们可能会忘记包含一个任务,或者如果有多个任务依赖于前一个任务的完成,顺序可能会变得复杂。我们希望能够指定是否:
task compile << {
println 'compling the source'
}
task dist(dependsOn: compile) << {
println "preparing a jar dist"
}
finalizedBy
我们还可以声明,如果调用一个任务,它应该跟随另一个任务,即使没有明确调用另一个任务。这与 dependsOn 相反,其中另一个任务在调用任务之前执行。在 finalizedBy 的情况下,另一个任务在调用任务的执行之后执行:
task distUsingTemp << {
println ("preapring dist using a temp dir")
}
task cleanup << {
println("removing tmp dir")
}
distUsingTemp.finalizedBy cleanup
onlyIf
我们可以指定一个条件,如果满足该条件,则任务将被执行:
cleanup.onlyIf { file("/tmp").exists()}
mustRunAfter 和 shouldRunAfter
有时候,如果我们想要以特定的顺序排列任务,而这种关系并不完全等同于dependsOn。例如,如果我们执行以下命令:
$ gradle build clean
然后,无关的任务将按照在命令行上指定的顺序执行,在这种情况下这没有意义。
在这种情况下,我们可能需要添加以下代码行:
build.mustRunAfter clean
这告诉 Gradle,如果任务图中有这两个任务,那么build必须在clean运行之后运行。在这里,build不依赖于clean。
shouldRunAfter和mustRunAfter之间的区别在于前者对 Gradle 更具暗示性,但并不强制 Gradle 始终遵循这种顺序。在以下两种情况下,Gradle 可能不会遵守shouldRunAfter:
-
当它引入循环排序时的情况。
-
在并行执行的情况下,当只有
shouldRunAfter任务尚未成功完成且其他依赖项已满足时,则shouldRunAfter将被忽略。
动态创建任务
Gradle 的一个优点是我们可以动态创建任务。这意味着在编写构建时,任务的名称和逻辑并不完全已知,但根据某些变量参数,任务将自动添加到我们的 Gradle 项目中。
让我们通过一个示例来尝试理解:
10.times { number ->
task "dynamicTask$number" << {
println "this is dynamic task number # $number "
}
}
在前面的虚构示例中,我们正在动态创建并添加十个任务到我们的构建中。尽管它们只是打印任务编号,但能够动态创建和添加任务到我们的项目中是非常强大的。
设置默认任务
到目前为止,我们一直使用带有任务名称(s)的gradle命令行界面。这在本质上有点重复,尤其是在开发期间,Gradle 这样的工具为我们提供了覆盖:
defaultTasks "myTaskName", "myOtherTask"
设置默认任务是个明智的选择,这样如果我们没有指定任何任务名称,默认的任务就会被执行。
在前面的示例中,不带任何参数从命令行运行gradle会依次运行defaultTasks中指定的默认任务。
任务类型
我们之前看到的任务都是临时的。我们必须编写任务动作的代码,每当任务执行时都需要执行。然而,无论我们构建哪个项目,总有许多任务的动作逻辑不需要改变,如果我们有能力对现有逻辑进行一些配置更改。例如,当你复制文件时,只有源、目标和包含/排除模式会改变,但如何从一处复制文件到另一处并遵守包含/排除模式的实际逻辑保持不变。所以,如果一个项目中需要两个类似复制任务的,比如说copyDocumentation和deployWar,我们真的需要编写整个逻辑来复制选定的文件两次吗?
对于非常小的构建(例如我们章节中的示例),这可能是可行的,但这种方法扩展性不好。如果我们继续编写任务操作来执行这些日常操作,那么我们的构建脚本将很快膨胀到一个难以管理的状态。
自定义任务类型是 Gradle 将可重用的构建逻辑抽象到自定义任务类中的解决方案,这些类在任务对象上公开输入/输出配置变量。这有助于我们调整类型化任务以满足我们的特定需求。这有助于我们保持常见的构建逻辑可重用和可测试。
另一个与临时代码任务操作相关的问题是它本质上是命令式的。为了工具的灵活性,Gradle 允许我们在构建脚本中以命令式方式编写自定义逻辑。然而,在构建脚本中过度使用命令式代码会使构建脚本难以维护。Gradle 应尽可能以声明式方式使用。命令式逻辑应封装在自定义任务类中,同时向用户公开任务配置以便配置。在 Gradle 的术语中,自定义任务类被称为 增强任务。
自定义任务类型充当一个模板,为常见的构建逻辑提供一些合理的默认值。我们仍然需要在构建中声明一个任务,但我们只需告诉 Gradle 这个任务类型以及配置这个任务类型的设置,而不是再次编写整个任务操作块。Gradle 已经提供了许多自定义任务类型;例如,Copy、Exec、Delete、Jar、Sync、Test、JavaCompile、Zip 等等。我们也可以轻松编写我们自己的增强任务。我们将简要地查看这两种场景。
使用任务类型
我们可以使用以下语法配置类型为 Copy 的任务:
task copyDocumentation(type:Copy) {
from file("src/docs/html")
into file("$buildDir/docs")
}
在前面的示例中,第一个重要的区别是我们传递了一个带有自定义任务类名(在这种情况下为 Copy)作为值的 type 键。此外,请注意没有 doLast 或间接(<<)操作符。我们传递给这个任务的闭包实际上在构建配置阶段执行。闭包内的方法调用被委派给隐式可用的 task 对象,该对象正在被配置。我们没有在这里编写任何逻辑,只是为类型为 Copy 的任务提供了配置。在继续编写临时代码任务操作之前,查看可用的自定义任务总是值得的。
创建任务类型
如果我们现在回顾一下,我们为示例任务编写的任务操作代码主要是打印给定消息到 System.out 的 println 语句。现在,想象一下,如果我们发现 System.out 不符合我们的要求,我们更应该使用文本文件从任务中打印消息。我们需要遍历所有任务并更改实现以将消息写入文件而不是 println。
处理此类变化需求有更好的方法。我们可以通过提供自己的任务类型来利用任务类型的功能。让我们在build.gradle中放入以下代码:
class Print extends DefaultTask {
@Input
String message = "Welcome to Gradle"
@TaskAction
def print() {
println "$message"
}
}
task welcome(type: Print)
task thanks(type: Print) {
message = "Thanks for trying custom tasks"
}
task bye(type: Print)
bye.message = "See you again"
thanks.dependsOn welcome
thanks.finalizedBy bye
在前面的代码示例中:
-
我们首先创建了一个类(它将成为我们的任务类型),该类扩展了
DefaultTask,这是在 Gradle 中已经定义好的。 -
接下来,我们使用
@Input在名为message的属性上声明了一个可配置的任务输入。我们的任务消费者可以配置这个属性。 -
然后,我们在
print方法上使用了@TaskAction注解。当我们的任务被调用时,这个方法会被执行。它只是使用println来打印message。 -
然后,我们声明了三个任务;所有任务都使用不同的方式来配置我们的任务。注意没有任务动作。
-
最后,我们应用了任务流程控制技术来声明任务依赖关系。
如果我们现在运行thanks任务,我们可以看到预期的输出,如下所示:
$ gradle -q thanks
Welcome to Gradle
Thanks for trying custom tasks
See you again
在这里需要注意的几点如下:
-
如果我们想要更改打印逻辑的实现,我们只需要在一个地方进行更改,那就是我们自定义任务类的
print方法。 -
使用任务类型的任务被使用,并且它们的工作方式与任何其他任务一样。它们也可以使用
doLast {}、<< {}来执行任务动作闭包,但通常不是必需的。
参考文献
下几节提到了一些 Groovy 的有用参考资料。
Groovy
对于 Groovy,有大量的在线参考资料可用。我们可以从这里开始:
-
对于进一步阅读,请参考 Groovy 的在线文档
www.groovy-lang.org/documentation.html -
更多 Groovy 资源的参考信息可在
github.com/kdabir/awesome-groovy找到。
这里有一份关于 Groovy 的书籍列表:
-
《Groovy in Action》一书可在
www.manning.com/books/groovy-in-action-second-edition找到。 -
《Groovy Cookbook》一书可在
www.packtpub.com/application-development/groovy-2-cookbook找到。 -
《Programming Groovy 2》一书可在
pragprog.com/book/vslg2/programming-groovy-2找到。
本章中使用的 Gradle API 和 DSL
Gradle 的官方 API 和 DSL 文档是探索和学习本章中讨论的各种类的好地方。这些 API 和 DSL 非常丰富,值得我们花时间阅读。
-
Project: -
Gradle(接口): -
任务:
摘要
我们本章从 Groovy 语言的快速功能概述开始,涵盖了一些有助于我们理解 Gradle 语法和编写更好的构建脚本的议题。然后,我们研究了 Gradle 向我们的构建脚本公开的 API 以及如何通过 DSL 消费 API。我们还涵盖了 Gradle 构建阶段。然后,我们探讨了任务可以被创建、配置、相互依赖以及默认运行的方式。
在阅读完本章后,我们应该能够理解 Gradle DSL,而不仅仅是试图记住语法。我们现在能够阅读并理解任何给定的 Gradle 构建文件,并且现在能够轻松地编写自定义任务。
本章可能感觉有点长且复杂。我们应该抽出一些时间来练习和重新阅读那些不清楚的部分,并且查阅本章中给出的在线参考资料。接下来的章节将会更加顺畅。
第五章:多项目构建
现在我们已经熟悉了构建脚本语法,我们准备处理更复杂的项目结构。在本章中,我们将重点关注跨越多个项目的构建,它们之间的相互依赖关系,以及许多其他相关内容。
随着项目代码库的增长,很多时候,根据层、责任、生成的工件,有时甚至根据开发团队来将其拆分为多个模块是很有必要的,以有效地分解工作。无论原因是什么,现实是大型项目迟早会被拆分为更小的子项目。此外,像 Gradle 这样的构建工具完全能够处理这种复杂性。
多项目目录布局
多项目(或称为多模块,有些人更喜欢这样称呼)是一组逻辑上相互关联的项目,通常具有相同的开发-构建-发布周期。目录结构对于布局此类项目的策略非常重要。通常,顶级根项目包含一个或多个子项目。根项目可能包含自己的源集,可能只包含集成测试,这些测试用于测试子项目的集成,或者甚至仅作为一个没有源和测试的母构建。Gradle 支持所有此类配置。
子项目相对于根项目的排列可能是平的,也就是说,所有子项目都是根项目的直接子项目(如样本 1 所示)或者是有层次的,这样子项目也可能有嵌套的子项目(如样本 2 所示)或者任何混合的目录结构。
让我们将以下目录结构称为样本 1:
sample1
├── repository
├── services
└── web-app
在样本 1 中,我们看到一个虚构的示例项目,其中所有子项目都是根项目的直接子项目,彼此之间是兄弟关系。仅为了这个例子,我们将我们的应用程序拆分为三个子项目,分别命名为:repository、:services和:web-app。正如它们的名称所暗示的,一个仓库包含数据访问代码,而服务层封装了以可消费 API 形式存在的业务规则。web-app只包含特定于 Web 应用程序的代码,例如控制器和视图模板。然而,请注意,:web-app项目可能依赖于:services项目,而:services项目反过来可能依赖于:repository项目。我们很快就会看到这些依赖是如何工作的。
小贴士
不要将多项目结构与单个项目中的多个源目录混淆。
让我们看看一个相对更复杂的结构,并将其称为样本 2:
sample2
├── core
│ ├── models
│ ├── repository
│ └── services
├── client
│ ├── client-api
│ ├── cli-client
│ └── desktop-client
└── web
├── webservices
└── webapp
我们的应用现在已经进化,为了满足更多需求,我们为其添加了更多功能。我们创建了更多子项目,例如应用桌面客户端和命令行界面。在示例 2 中,根项目被分割成三个项目(组),它们各自有自己的子项目。在这个例子中,每个目录都可以被视为一个项目。这个示例的目的是仅展示可能的目录结构之一。Gradle 不会强制使用一种目录结构代替另一种。
一个人可能会想知道,我们把这些build.gradle文件放在哪里,里面有什么内容?这取决于我们的需求和如何构建我们的项目结构。在我们理解了什么是settings.gradle之后,我们将很快回答所有这些问题。
settings.gradle 文件
在初始化过程中,Gradle 读取settings.gradle文件以确定哪些项目将参与构建。Gradle 创建一个类型为Setting的对象。这发生在任何build.gradle被解析之前。它通常放置在根项目与build.gradle平行的地方。建议将setting.gradle放在根项目中,否则我们必须使用命令行选项-c显式告诉 Gradle 设置文件的路径。将这两个文件添加到示例 1 的目录结构中,我们会得到如下内容:
sample1
├── repository
│ └── ...
├── services
│ └── ...
├── web-app
│ └── ...
├── build.gradle
└── settings.gradle
settings.gradle最常见的使用是将所有参与构建的子项目列出:
include ':repository', ':services', ':web-app'
此外,这就是告诉 Gradle 当前构建是一个多项目构建所需的所有内容。当然,这并不是故事的结尾,我们还可以在多项目构建中做更多的事情,但这是最基本的要求,有时也足以让多项目构建工作。
Settings的方法和属性在settings.gradle文件中可用,并且在对Settings实例进行操作时隐式调用,就像我们在上一章中看到的那样,Project API 的方法在build.gradle文件中可用。
注意
你是否想知道为什么在上一节的项目名称前使用了冒号(:)?它表示相对于根项目的项目路径。然而,include方法允许一级子项目名称省略冒号。因此,include调用可以重写如下:
include 'repository', 'services', 'web-app'
让我们只通过从命令行调用任务projects来查询项目。projects任务列出了 Gradle 构建中可用的所有项目:
$ gradle projects
:projects
------------------------------------------------------------
Root project
------------------------------------------------------------
Root project 'sample1'
+--- Project ':repository'
+--- Project ':services'
\--- Project ':web-app'
To see a list of the tasks of a project, run gradle <project-path>:tasks.
For example, try running gradle :repository:tasks.
BUILD SUCCESSFUL
注意
在嵌套超过一个级别的场景中,例如在示例 2 中,所有项目都必须使用以下语法包含在根项目的settings.gradle中:
include 'core',
'core:models', 'core:repository', 'core:services',
'client' //... so on
我们可以在 Settings DSL 文档(www.gradle.org/docs/current/dsl/org.gradle.api.initialization.Settings.html)和 Settings API 文档(www.gradle.org/docs/current/javadoc/org/gradle/api/initialization/Settings.html)中找到更多关于 Settings 的信息。
在多项目构建中组织构建逻辑
Gradle 给我们提供了创建一个构建文件用于所有项目或每个项目单独的构建文件;你也可以混合使用。让我们从向根项目的 build.gradle 中添加一个简单的任务开始:
task sayHello << {
println "Hello from multi-project build"
}
我们正在创建一个带有打印消息动作的任务。现在,让我们检查根项目中可用的任务。从 root 目录,让我们调用任务 tasks:
$ gradle tasks
...
Other tasks
-----------
sayHello
....
没有疑问,sayHello 任务在根项目中可用。然而,如果我们只想查看子项目上可用的任务怎么办?比如说 :repository。对于多项目构建,我们可以使用 gradle <project-path>:<task-name> 语法或进入子项目目录并执行 gradle <task-name> 来调用任何嵌套项目的任务。所以现在,如果我们执行以下代码,我们将看不到 sayHello 任务:
$ gradle repository:tasks
这是因为 sayHello 只在根项目中定义;因此,它不在子项目中可用。
将构建逻辑应用于所有项目
build.gradle:
allprojects {
task whoami << {println "I am ${project.name}"}
}
在尝试理解代码片段之前,让我们再次运行熟悉的任务。首先,从根项目开始:
$ gradle tasks
...
Other tasks
-----------
sayHello
whoami
...
然后,从仓库项目:
$ gradle repository:tasks
...
Other tasks
-----------
whoami
...
allprojects block (the closure being passed to allprojects, to be technically correct) gets applied to all the projects. The task's action prints the name of the project using the project object reference. Remember that the project object will refer to different projects depending on the project on which the task is being called. This happens because in the configuration phase, the allproject block is executed for each project once we have the project reference for that project.
传递给 allprojects 的闭包中的内容将完全像一个单项目 build.gradle 文件。我们甚至可以应用插件、声明仓库和依赖项等等。所以,本质上,我们可以编写适用于所有项目的任何通用构建逻辑,然后它将被应用到所有项目上。allprojects 方法也可以用来查询当前构建中的项目对象。有关 allprojects 的更多详细信息,请参阅项目的 API。
如果我们将 --all 标志传递给 tasks 任务,我们将看到 whoami 任务存在于所有子项目中,包括 root 项目:
$ gradle tasks --all
...
Other tasks
-----------
sayHello
whoami
repository:whoami
services:whoami
web-app:whoami
...
如果我们只想在特定的项目上执行 whoami,比如说 :repository,那么命令就像以下这样简单:
$ gradle -q repository:whoami
I am repository
当我们在没有任何项目路径的情况下执行 whoami:
$ gradle -q whoami
I am root
I am repository
I am services
I am web-app
哇,Gradle 走得更远,以确保当我们从父项目执行任务时,具有相同名称的子项目任务也会被执行。当我们考虑像 assemble 这样的任务时,这非常有用,因为我们实际上希望所有子项目都进行组装,或者测试,它测试根项目以及子项目。
然而,关于仅在根项目上执行任务怎么办?这确实是一个有效场景。记住绝对任务路径:
$ gradle -q :whoami
I am root
冒号起着至关重要的作用。在这里,我们只引用root项目的whoami。没有其他任务匹配相同的路径。例如,repository的whoami有一个路径repository:whoami。
现在,切换到repository目录,然后执行whoami:
$ gradle –q whoami
I am repository
因此,任务执行是上下文相关的。在这里,默认情况下,Gradle 假设任务必须在当前项目上调用。不错,不是吗?
让我们在现有的build.gradle文件中添加一些更动态的代码:
allprojects {
task("describe${project.name.capitalize()}") << {
println project.name
}
}
在这里,根据项目名称,我们将任务名称设置为describe,并在项目名称前加上前缀。因此,所有项目都得到了它们自己的任务,但名称不会相同。我们添加了一个仅打印项目名称的操作。如果我们现在在我们的项目上执行tasks,我们可以看到任务名称包括项目名称:
$ gradle tasks
...
Other tasks
-----------
describeRepository
describeSample1
describeServices
describeWeb-app
sayHello
whoami
...
尽管这个例子非常简单,但我们学到了一些东西。首先,allprojects块是累加的,就像 Gradle 中的大多数其他方法一样。我们添加了第二个allprojects块,并且两者都运行得很好。其次,任务名称可以动态分配,例如,使用项目名称。
现在,我们可以从项目根目录调用任何describe*任务。正如我们可能猜测的那样,任务名称是唯一的;我们不需要在项目路径前加上前缀:
$ gradle -q describeServices
services
让我们切换到repository目录并列出任务:
$ gradle -q tasks
...
Other tasks
-----------
describeRepository
whoami
我们只看到适用于当前项目的任务,即repository。
将构建逻辑应用于子项目
让我们继续我们的例子。在这里,根项目将没有任何源集,因为所有的 Java 代码都将位于三个子项目中的任何一个。因此,只将java插件应用于子项目不是明智的吗?这正是subprojects方法发挥作用的地方,即当我们只想在子项目上应用一些构建逻辑而不影响父项目时。它的用法类似于allprojects。让我们将java插件应用于所有子项目:
subprojects {
apply plugin: 'java'
}
现在,运行gradle tasks应该会显示由java插件添加的任务。尽管这些任务可能看起来在根项目中可用,但实际上并非如此。在这种情况下,检查gradle -q tasks --all的输出。在子项目上存在的任务可以从根项目调用,但这并不意味着它们在根项目中存在。由java插件添加的任务仅可在子项目中使用,而如帮助任务这样的任务将在所有项目中可用。
子项目依赖
在本章的开头,我们提到一个子项目可能依赖于另一个子项目(或多个子项目),就像它可以依赖于外部库依赖一样。例如,services项目的编译依赖于repository项目,这意味着我们需要repository项目的编译类在services项目的编译类路径上可用。
要实现这一点,我们当然可以在services项目中创建一个build.gradle文件并将依赖声明放在那里。然而,为了展示一种替代方法,我们将这个声明放在root项目的build.gradle中。
与allprojects或subprojects不同,我们需要一个更精细的机制来仅从root项目的build.gradle中配置单个项目。实际上,使用project方法非常简单。此方法除了接受一个闭包,就像allprojects和subprojects方法一样,还需要指定一个项目名称,该闭包将应用于该项目。在配置阶段,闭包将在该项目的对象上执行。
因此,让我们将其添加到root项目的build.gradle中:
project(':services') {
dependencies {
compile project(':repository')
}
}
在这里,我们只为services项目配置依赖项。在dependencies块中,我们声明:repository项目是services项目的编译时依赖项。这基本上类似于外部库声明;我们使用project(:sub-project)来引用子项目,而不是在group-id:artifact-id:version表示法中使用库名称。
我们之前也说过web-app项目依赖于services项目。所以这次,让我们使用web-app自己的build.gradle来声明这个依赖项。我们将在web-app目录中创建一个build.gradle文件:
root
├── build.gradle
├── settings.gradle
├── repository
├── services
└── web-app
└── build.gradle
由于这是一个特定于项目的构建文件,我们只需像在其他任何项目中一样添加dependencies块即可:
dependencies {
compile project(':services')
}
现在,让我们使用dependencies任务可视化 Web 项目的依赖关系:
$ gradle -q web-app:dependencies
------------------------------------------------------------
Project :web-app
------------------------------------------------------------
archives - Configuration for archive artifacts.
No dependencies
compile - Compile classpath for source set 'main'.
\--- project :services
\--- project :repository
default - Configuration for default artifacts.
\--- project :services
\--- project :repository
runtime - Runtime classpath for source set 'main'.
\--- project :services
\--- project :repository
testCompile - Compile classpath for source set 'test'.
\--- project :services
\--- project :repository
testRuntime - Runtime classpath for source set 'test'.
\--- project :services
\--- project :repository
Gradle 向我们展示了web-app在不同配置下的依赖关系。我们还可以清楚地看到 Gradle 理解了传递依赖;因此,它显示了web-app通过services传递依赖于repository。请注意,我们实际上并没有在任何项目中声明任何外部依赖项(如servlet-api),否则它们也会在这里显示。
值得注意的是,查看project对象上configure方法的变体,以便过滤和配置选定的项目。有关configure方法的更多信息,请参阅docs.gradle.org/current/javadoc/org/gradle/api/Project.html。
摘要
在这一简短的章节中,我们了解到 Gradle 支持灵活的目录结构,适用于复杂的项目层次结构,并允许我们为我们的构建选择合适的结构。然后我们探讨了在多项目构建的背景下settings.gradle的重要性。接着我们看到了将构建逻辑应用于所有项目、子项目或单个项目的各种方法。最后,我们举了一个关于项目间依赖关系的小例子。
在 Gradle 语法方面,我们只需要关注这些。接下来几章将主要关注各种插件为我们构建添加的功能以及如何配置它们。
第六章:使用 Gradle 的实战项目
到目前为止,我们已经讨论了构建 Java 项目、Web 项目、Gradle 生命周期和 Gradle 的多模块功能。正如我们所知,在 Gradle 之前,市场上有很多其他的构建工具,其中最受欢迎的是 Ant 和 Maven。由于许多项目构建脚本已经用这两种构建工具编写。在本章中,我们将讨论不同的迁移策略,将现有项目的构建脚本从 Ant、Maven 迁移到 Gradle。同时,我们还将关注将 Gradle 构建脚本集成到持续集成工具(如 Jenkins)以及为代码生成 Java 文档。
从基于 Ant 的项目迁移
Ant 是最初且最受欢迎的构建工具之一,与其他基于脚本的原生构建工具相比,它使构建和部署过程变得更加简单。尽管如此,你仍然可以找到许多使用 Ant 构建脚本来构建项目的项目。Ant 是在命令式编程模型的哲学基础上开发的,它告诉系统做什么以及如何做。因此,你可以控制构建脚本中的每一个动作或步骤。以下是一个用于构建任何 Java 项目的示例 Ant 构建脚本。在这里,我们只考虑构建 Java 项目所需的最小任务,因为我们的目的是讨论从 Ant 脚本迁移到 Gradle 脚本的策略:
<project name="Ant build project" default="createJar">
<target name="clean" description="clean the existing dirs">
<delete dir="build"/>
<delete dir="dist"/>
</target>
<target name="compile" description="compile the source"
depends="clean">
<mkdir dir="build"/>
<mkdir dir="dist"/>
<mkdir dir="build/classes"/>
<javac srcdir="src" destdir="build/classes"/>
</target>
<target name="createJar" depends="compile" description="create the
jar">
<jar jarfile="dist/JavaProject-1.0.jar" basedir="build/classes"/>
</target>
</project>
在这里,我们定义了三个目标,例如clean、compile和createJar,分别用于删除目录、创建目录、编译源目录中存在的 Java 文件,并最终创建.jar文件。开发者可以遵循以下三种不同的策略来将构建脚本从 Ant 迁移到 Gradle:
-
导入 Ant 文件
-
使用 AntBuilder API
-
将 Ant 任务重写为 Gradle 任务
我们将用示例讨论每一个。
导入 Ant 文件
迁移的最简单和最直接的方法是将你的 Ant 脚本文件直接导入到 Gradle 脚本中。考虑以下结构:
C:\GRADLE\CHAPTER6
│ build_import.gradle
│ build.xml
│
└───src
└───main
└───java
└───ch6
SampleJava.java
在这里,项目名称是Chapter6,Java 源目录是src/main/java,Ant 构建脚本文件是build.xml。上面提到了build.xml的源代码。现在,作为迁移的一部分,创建一个包含以下内容的build_import.gradle文件:
ant.importBuild 'build.xml'
就这样。是的,我们已经成功将 Ant 构建脚本迁移到了 Gradle 脚本。现在,尝试执行以下命令:
> gradle –b build_import.gradle createJar
:clean
:compile
:createJar
BUILD SUCCESSFUL
Total time: 3.045 secs
执行此操作后,你可以在项目目录中找到build/classes和dist目录,其中dist包含JavaProject.jar文件。
使用 AntBuilder API
迁移的另一种方法是使用 AntBuilder API。默认情况下,Gradle 为用户提供了一个 AntBuilder 对象ant。用户可以直接在 Gradle 脚本中使用此对象来调用 Ant 任务。以下是一个使用 AntBuilder API 的build_antbuilder.gradle文件的示例代码:
task cleanDir << {
ant.delete(dir:"build")
ant.delete(dir:"dist")
}
task compileSrc(dependsOn:'cleanDir') << {
ant.mkdir(dir:"build/classes")
ant.mkdir(dir:"dist")
ant.javac(srcdir:"src", destdir:"build/classes", includeantruntime:"false")
}
task createJar(dependsOn:'compileSrc') << {
ant.jar(destfile: "dist/JavaProject-1.0.jar", basedir:"build/classes")
}
在这里,你可以看到我们使用了不同的 Ant 任务,如mkdir、javac、jar等,作为ant对象的方法。现在,执行以下命令:
> gradle –b build_antbuilder.gradle createJar
:cleanDir
:compileSrc
:createJar
BUILD SUCCESSFUL
Total time: 3.437 secs
在这里,你也会发现相同的输出,即它会创建一个build/classes目录,其中你可以找到类文件,以及一个dist目录,其中你可以找到.jar文件。
将 Ant 任务重写为 Gradle 任务
这是最终的方案。使用这种方法而不是使用ant对象,你实际上是用实际的 Gradle 任务重写了完整的构建逻辑或功能。遵循这一策略的一个简单方法就是用户首先需要逻辑上理解用 Ant 编写的完整流程文件,然后逐步将其转换为 Gradle 脚本。对于 Ant 中定义的所有目标,用户可以在 Gradle 中创建任务,对于 Ant 中定义的所有任务,用户可以使用 Gradle 功能来复制相同的行为。Gradle 提供了不同的标准插件来支持构建需求的大部分步骤。插件有自己的生命周期,借助插件,用户可以避免为常见的构建功能重写大量的样板脚本。其中一个这样的插件是java插件。我们已经在第二章中看到了java插件的详细信息,即构建 Java 项目。如果我们想将这个 Ant 脚本迁移到 Gradle 脚本以构建一个 Java 项目,用户可以简单地使用一个Java插件,任务就完成了。
考虑以下内容的build.gradle文件:
apply plugin:'java'
如果开发者遵循java插件的默认约定,他只需要写这一行就能构建一个 Java 项目,在执行gradle build命令后,所有必要的步骤都会完成,例如编译代码、执行单元测试用例,以及准备.jar文件。然而,这并不总是如此;许多遗留项目并不遵循约定,它们可能有自己的一套约定。gradle插件提供了根据项目需求配置插件的灵活性。以下示例代码将 Ant 脚本重写为 Gradle 脚本:
apply plugin:'java'
task cleanDir << {
delete "build"
delete "dist"
}
task createDirs(dependsOn:'cleanDir') << {
def classes = file("build/classes")
def dist = file("dist")
classes.mkdirs()
dist.mkdirs()
}
compileJava {
File classesDir = file("build/classes")
FileTree srcDir = fileTree(dir: "src")
source srcDir
destinationDir classesDir
}
task createJar(type: Jar) {
destinationDir = file("dist")
baseName = "JavaProject-1.0"
from "build/classes"
}
createJar.dependsOn compileJava
compileJava.dependsOn createDirs
gradle createJar command, it will generate the same output which was generated by following above migration strategies.
从 Maven 项目迁移
Maven,另一个构建工具,在 Ant 之后获得了最多的普及,它还带来了依赖管理解决方案,以解决用户在 Ant 中遇到的问题。Ant 的第一个问题是命令式编程,用户必须编写大量的样板代码。另一个问题是依赖管理。Ant 没有内置的依赖管理解决方案(Ant 后来与 Ivy 集成以进行依赖管理)。用户必须在每个构建文件中写入它需要下载的每个 JAR 文件的路径,在传递依赖的情况下,对用户来说识别每个依赖的 JAR 文件并在构建文件中提及 JAR 名称非常复杂。此外,在版本冲突的情况下,这消耗了开发者大量的精力。Maven 带来了声明式编程模型和内置的依赖管理解决方案。Gradle 也是基于这些原则构建的;因此,从 Maven 迁移到 Gradle 对用户来说似乎非常舒适。
与 Ant 迁移类似,Gradle 不提供任何导入功能或内置的 Maven 对象。用户需要将 Maven 脚本重写为 Gradle 脚本。以下是一些有助于您顺利从 Maven 迁移到 Gradle 的概念:
-
插件声明
-
常见约定
-
依赖管理
-
仓库配置
让我们转向这些概念的解释:
-
插件声明:插件是 Maven 和 Gradle 功能的关键驱动。与 Maven 插件相同,Gradle 也将大部分功能打包成插件。在 Maven 中,用户可以通过以下 XML 格式包含插件:
<plugin> <artifactId>pluginName</artifactId> <version>2.3.2</version> </plugin>要包含一个插件,用户只需写下以下
apply plugin语句:apply plugin: '<plugin name>' -
常见约定:在 Maven 和 Gradle 中,插件总是为其功能提供一些常见约定。例如,如果用户包含一个
java插件,常见约定是源代码位置应该是src/main/java,测试代码位置应该是src/test/java,等等。如果用户包含插件并遵循相同的约定,那么他可以避免编写任何样板代码,这可以节省他的时间和精力。 -
依赖管理:Maven 和 Gradle 都自带内置的依赖管理功能。用户无需担心项目中每个单独的 JAR 文件。他只需在项目中提及一级依赖,其余的都由构建工具处理。
在 Maven 中,用户可以以下格式提及依赖:
<dependency> <groupId> org.apache.logging.log4j</ groupId> <artifactId>log4j-core </ artifactId> <version>1.2</version> <scope>compile</scope> </dependency>要在 Gradle 中定义依赖,用户必须使用以下语法:
dependencies{ compile(' org.apache.logging.log4j: log4j-core:1.2') }对于 Maven 来说,作用域是针对依赖配置的。你可能已经注意到了 Maven 中的作用域属性和 Gradle 中的依赖配置属性。在 Maven 中,作用域标识了在构建的哪个阶段需要下载依赖。在 Gradle 中,依赖配置满足同样的需求。
-
仓库配置:每当谈到依赖项时,首先想到的是仓库。这是你下载依赖项的位置。以下是一个代码片段,可以帮助你在 Maven 中提及仓库位置:
<repositories> <repository> <id>repository_1</id> <name>custom Name</name> <url> http://companylocalrepository.org </url> </repository> </repositories>在 Gradle 中,你可以使用以下语法提及仓库:
repositories { maven { url "http://companylocalrepository.org" } }
正如我们所看到的,Maven 和 Gradle 在构建任何项目时遵循相同的哲学。主要区别是 Maven 使用 XML,它擅长结构,但在配置构建脚本时可能会很痛苦,而 Gradle 使用 Groovy 脚本,它是一种 DSL,在管理和更改默认行为时提供了很大的灵活性。
发布工件
构建软件没有多大意义,除非你将你的软件发布到一些常见的仓库,以便在需要时可以被其他软件或项目重用。我们已经讨论了在下载依赖项时的仓库。仓库的另一个方面是将构建结果(JAR、WAR、EAR 等)上传到某个常见位置,以便其他开发者可以下载。Gradle 中的不同插件提供了一种自动化的方式来发布插件的默认工件。例如,一个 java 插件提供了一个任务来上传 JAR 文件,一个 war 插件提供了一个任务来上传 WAR 文件,一个 scala 插件提供了一个任务来上传 JAR 文件,等等。用户只需要配置 上传仓库 位置。如果用户不想上传默认的构建工件,或者用户想上传一些自定义工件,他可以轻松地自定义 Gradle 任务来上传其他工件,并按照他的自定义要求。
正如我们所看到的,一个 java 插件提供了不同的配置,如编译、测试编译、运行时等,以下载特定范围的 JAR 文件。为了上传工件,Gradle 提供了一个额外的配置,存档。用户可以在存档配置中配置工件,并使用 uploadArchive 任务将工件上传到仓库。
以下是一个构建文件(build_uploadArtifact.gradle)的示例,用于上传由 java 插件生成的 JAR 文件:
apply plugin: 'java'
version=1.0
repositories {
mavenCentral()
}
dependencies {
compile ('log4j:log4j:1.2.16')
}
uploadArchives {
repositories {
maven {
credentials {
username "user1"
password "user1"
}
url "http://company.private.repo"
}
}
}
你可以执行 gradle –b build_uploadArtifact.gradle uploadArchives 命令来上传工件。作为生命周期的一部分,它将构建并上传工件。
在前面的例子中,uploadArchives 任务将工件上传到仓库(在 URL 中提及)。如果是一个受保护的仓库,你可以提供用户名和密码,否则忽略它。你已经注意到我们没有在这里提到存档,那么会上传什么?正如我们已经讨论过的,一个 java 插件构建 JAR 文件,一个 war 插件构建 WAR 文件,等等。因此,插件默认生成的工件将默认作为 uploadArchives 任务的一部分上传。我们将看到另一个示例,说明如何上传你的自定义工件。
以下是 build_uploadCustom.gradle 文件:
apply plugin: 'java'
archivesBaseName="JavaProject" // to customize Jar Name
version=1.0
repositories {
mavenCentral()
}
def customFile= file('configurations.xml')
task customZip(type: Zip) {
from 'src'
}
artifacts {
archives customFile
archives customZip
}
uploadArchives {
repositories {
flatDir {dirs "./tempRepo"}
}
}
现在,执行 gradle –b build_uploadCustom.gradle uploadArchives 命令:
>gradle -b build_uploadCustom.gradle uploadArchives
:customZip UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:uploadArchives
BUILD SUCCESSFUL
Total time: 4.014 secs
在这里,你可以发现执行构建脚本后创建了一个新的目录 tempRepo。这个目录包含了 Gradle 脚本发布的所有上述工件(ZIP、JAR 和 XML 文件)。
在前面的例子中,我们涵盖了以下两个情况:
-
上传自定义文件(一个 XML 文件和一个 ZIP 文件以及默认的工件)
-
上传到本地文件系统(不在中央仓库上)
如果你将任何其他自定义文件(JAR、WAR 或其他文件)配置到存档中,它也会被上传到仓库。在这里,我们配置了两个额外的文件,一个是 .xml 文件,另一个是 .zip 文件,以及默认的 Java 工件。如果你想与你的团队成员共享工件,同时又不希望将工件上传到仓库,除非它通过了集成测试,Gradle 提供了使用 flatDir 将文件上传到本地文件系统的灵活性。
Gradle 最近引入了一个 maven-publish 插件,以便更好地控制发布过程。它提供了许多额外的灵活性,以及默认的发布任务。用户可以修改 POM 文件,发布多个模块等等。
注意
你可以在 docs.gradle.org/current/userguide/publishing_maven.html 找到更多详细信息。
持续集成
持续集成(CI)是你在任何地方都能看到的流行术语之一。从其名称可以看出,CI 是每次代码库有提交时进行代码集成的过程。它编译代码,运行单元测试并准备构建。用户在这里获得的一个好处是,如果存在编译问题和集成问题,用户可以在早期阶段发现,而不是等到太晚。以下是一个 CI 工具遵循的通用工作流程:

图 6.1
Gradle 如何融入这个流程?为了规划任何软件的构建和部署自动化解决方案,我们需要一套不同的工具协同工作以实现共同目标。Jenkins 是帮助整合完整工作流程的集成工具之一。它还基于插件的概念;你可以根据需要向 Jenkins 添加不同的插件(例如,Gradle、Git、Svn 等),并配置它们以规划自动化流程。
在这里,我们假设你已经安装了 Jenkins。你可以通过导航到 管理 Jenkins | 管理插件 | 搜索 Gradle 来安装一个 Gradle 插件。

图 6.2
一旦安装了插件,你就可以使用以下截图在 Jenkins 中配置作业:

图 6.3
在项目配置屏幕下,你需要配置仓库路径。默认情况下,Jenkins 提供了 CVS 和 SVN 插件。如果你需要其他仓库(如 Perforce 或 Git),你可以添加相应的插件。在仓库配置之后,你需要配置 构建触发器。它允许你定期触发构建,或者如果你想在每个提交时构建,你可以选择 轮询源代码管理。现在,是时候配置你的构建脚本,该脚本将构建你的项目。
在 构建 菜单下,你可以选择 调用 Gradle 脚本:

图 6.4
如果你使用的是默认的构建文件名 build.gradle,则无需配置构建文件。在 任务 下,你可以提及你想要执行的任务名称。例如,如果你想构建项目,你可以在文本框中提及 build。
一旦完成配置,你可以在左侧菜单中点击 现在构建 来构建项目。完成后,点击相应的构建编号,它将在主屏幕上显示 控制台输出:

图 6.5
生成文档
文档是开发生命周期的重要组成部分,但开发者对其关注不足。如果代码没有适当记录,它总是会增加维护工作量,而且如果代码缺乏文档,新团队成员理解代码也需要花费时间。当你将 Java 插件应用到构建文件时,Gradle 会为你提供一个 javadoc 任务。默认情况下,即使用户在文件中没有提及任何 Javadoc,Gradle 也会为你的代码生成初始文档。
考虑以下 Java 示例代码:
package ch6;
public class SampleTask {
public static void main(String[] args) {
System.out.println("Building Project");
}
public String greetings(String name) {
return "hello "+name;
}
}
现在,尝试执行以下命令:
> gradle clean javadoc
:clean
:cleanDir
:createDirs
:compileJava
:processResources UP-TO-DATE
:classes
:javadoc
BUILD SUCCESSFUL
Total time: 4.341 secs
此命令将在 <project> \build\docs\javadoc 生成基本的 Java 文档。
根据要求,你可以在上述类中添加自己的标签(@description、@param 等)和详细信息,以获取更新的 Java 文档。
摘要
在本章中,我们讨论了从现有的构建工具迁移到 Gradle 的不同迁移策略,这对于计划将现有的 Ant 和 Maven 脚本迁移到 Gradle 的用户来说非常有用。我们还讨论了如何将工件发布到仓库,这是任何构建工具的关键功能,它帮助用户始终从仓库获取最新的工件。我们借助 Jenkins 讨论了 CI 框架,以及 Gradle 如何融入这一流程,从而自动化构建和部署解决方案。最后,我们讨论了如何为 Java 代码生成文档。
在下一章中,我们将讨论如何将 TestNG 与 Gradle 集成,这将帮助用户将测试用例作为 Gradle 构建的一部分运行。我们还将讨论集成测试策略以及 Gradle 与代码分析和代码覆盖率工具的集成。
第七章。使用 Gradle 进行测试和报告
在本章中,我们将涵盖四个不同的主题:使用 TestNG 进行测试、集成测试、使用 JaCoCo 进行代码覆盖率以及使用 Sonar 进行代码分析。在第二章中,构建 Java 项目,我们已经讨论了使用 JUnit 进行单元测试。在本章中,我们将介绍另一个广泛使用的测试工具,TestNG。代码覆盖率和代码质量是测试驱动开发(TDD)中的另外两个重要方面。在今天的敏捷开发过程中,开发者需要对其开发的代码进行持续的反馈。代码质量工具帮助我们实现这一目标。通常,这些工具与持续集成(CI)系统集成,以便这些报告每天(甚至每次提交后)生成,在不同团队之间共享,甚至可以持久化以供未来分析。在本章中,我们将专注于不同工具的 Gradle 方面。我们将主要介绍支持这些特性的不同 Gradle 插件。
使用 TestNG 进行测试
使用 TestNG 与我们在第二章中讨论的 JUnit 集成类似,即构建 Java 项目。第一步是创建带有 TestNG 依赖项的构建文件并配置测试闭包。以下构建文件将 TestNG 库添加为testCompile依赖项,并在测试闭包中,我们添加了一个testng.xml文件来执行测试用例。在本节中,我们将简要讨论testng.xml的使用:
apply plugin:'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'org.testng:testng:6.8.21'
}
test {
ignoreFailures = true
useTestNG(){
suites("src/test/resources/testng.xml")
}
}
注意
然而,你可以在testng.org/doc/documentation-main.html了解更多关于 TestNG 配置的信息。
在我们的示例中,我们创建了三个测试用例,分别命名为verifyMapSize、verifyMapNotNull和addEvenNumbers。这些测试用例被分组为Smoke和Integration测试用例。如果你执行 Gradle 测试命令,所有三个测试用例都将被执行,并在build/reports/tests目录中创建测试报告。报告的外观和感觉与我们在前面看到的 JUnit 报告类似。实际的 TestNG 报告是在项目主目录下的test-output/目录中创建的。JUnit 和 TestNG 都生成它们自己的不同报告格式,但 Gradle 将它们协调成标准的外观和感觉:
package com.packtpub.ge.ch7;
import java.util.HashMap;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class HashTest {
private HashMap<Integer,String> hm;
@BeforeClass(alwaysRun = true)
public void setup(){
hm = new HashMap<Integer, String>();
}
@AfterMethod(alwaysRun = true)
public void cleantask(){
hm.clear();
}
@Test(groups = "Smoke")
public void verifyMapSize(){
Assert.assertEquals(hm.size(), 0);
hm.put(1, "first");
hm.put(2, "second");
hm.put(3, "third");
Assert.assertEquals(hm.size(), 3);
}
@Test(groups = "Smoke")
public void verifyMapNotNull(){
Assert.assertNotNull(hm);
}
@Test(groups = "Integration")
public void addEvenNumbers(){
hm.put(2, "second");
hm.put(4, "fourth");
Assert.assertEquals(hm.size(), 2);
}
}
一个 TestNG 测试用例可以从命令行、Ant 文件、Gradle 脚本、Eclipse 插件或 TestNG 测试套件文件中执行。TestNG 套件文件提供了一个灵活的测试执行控制机制。在测试套件文件中,你可以定义测试类、测试、测试组名称、监听器信息等。
我们在src/test/resource文件夹中创建了一个示例testng.xml文件。该文件包含一些重要信息。用于创建报告格式的监听器配置,一个测试组声明为Smoke,以及一个名为com.packtpub.ge.ch7.HashTest的测试类。
Gradle 不会强迫你将testng.xml放在src/test/resources中,我们只是这样做作为一种保持其组织性的手段:
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite1" verbose="1" >
<listeners>
<listener class-name="org.testng.reporters.EmailableReporter" />
</listeners>
<test name="Smoke Test">
<groups>
<run>
<exclude name="Integration" />
<include name="Smoke" />
</run>
</groups>
<classes>
<class name="com.packtpub.ge.ch7.HashTest">
</class>
</classes>
</test>
</suite>
由于我们只包含了标记为Smoke的测试用例,当我们执行gradle test命令时,TestNG 只调用了两个测试用例,verifyMapNotNull和addEvenNumbers。以下图显示了在<Project_Home>/test-output/目录中创建的 TestNG 报告:

图 7.1
集成测试
单元测试是软件开发生命周期中的关键步骤之一。它是验证代码质量的第一步检查。大多数基本功能都可以通过单元测试用例进行测试。它们快速且执行时间短。我们讨论了 JUnit 框架和 TestNG 框架来对代码进行单元测试。质量检查流程的下一步是集成测试。根据单元测试的一般定义,你将代码划分为小的单元并独立测试它们,这在独立开发代码时是好的。一旦你提交了代码并与其他开发者集成代码,你需要另一个级别的测试,这被称为集成测试。它验证不同组件按预期协同工作或否。你的测试报告可能在单元测试中给出 100%的成功结果,但除非你执行集成测试,否则你不能保证整个软件的功能。
我们已经看到了 Gradle 对单元测试的支持以及 Gradle 如何提供约定来在不同的目录结构和任务中编写测试类以及执行测试用例。在提供约定的术语中,Gradle 不会区分单元测试和集成测试。要在 Gradle 中同时启用单元测试和集成测试,你需要自定义 Gradle 以启用两者。考虑以下项目源代码的层次结构:
C:.
└───IntegrationSample
└───src
├───main
│ └───java
└───test
└───java
这是您为源代码和测试代码创建的标准文件夹结构。您创建 src/test/java 来存储您的单元测试用例。现在,如果您想将集成测试用例添加到项目中,您可以将集成测试用例合并到相同的目录结构中;然而,这并不是一个好的设计——因为您可能希望在构建项目时每次都执行单元测试,并且可能希望每两周或每周执行一次集成测试——这可能会根据项目的复杂性和大小消耗更多时间。因此,而不是将集成测试合并到单元测试用例的目录结构中,我们建议您为集成测试用例创建一个单独的目录结构,src/integrationTest/java,并在您的 Gradle 构建脚本中进行配置。
以下将是存储集成测试用例的更新目录结构:
C:.
└───IntegrationSample
└───src
├───integrationTest
│ └───java
├───main
│ └───java
└───test
└───java
一旦创建了目录结构,您需要在您的 Gradle 构建脚本中进行配置。更新的构建脚本如下:
apply plugin: 'java'
sourceSets {
integrationTest {
java.srcDir file('src/integrationTest/java')
resources.srcDir file('src/integrationTest/resources') // to add the resources
}
}
task runIntegrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath
}
在这里,我们添加了一个额外的配置,integrationTest,以添加集成测试用例。为了执行集成测试,我们还定义了一个任务,runIntegrationTest,其类型为 Test,并配置了 testClassesDir 和类路径属性。一旦我们在构建脚本中添加了额外的 sourceSets,Java 插件会自动将两个新的依赖配置添加到您的构建脚本中 integrationTestCompile 和 integrationTestRuntime。
执行以下命令以检查当前依赖项:
> gradle dependencies
------------------------------------------------------------
Root project
------------------------------------------------------------
……...
compile - Compile classpath for source set 'main'.
No dependencies
integrationTestCompile - Compile classpath for source set 'integration test'.
No dependencies
integrationTestRuntime - Runtime classpath for source set 'integration test'.
No dependencies
……….
BUILD SUCCESSFUL
Total time: 3.34 secs
在这里,integrationTestCompile 可以用于配置编译测试用例所需的依赖项,而 integrationTestRuntime 可以用于配置执行测试用例所需的依赖项。如您所见,没有为集成测试用例显式配置依赖项。您可以在依赖项闭包下进行配置:
dependencies {
// other configuration dependencies
integrationTestCompile 'org.hibernate:hibernate:3.2.3.ga'
}
我们不希望在每次构建项目时都执行集成测试。因此,要执行集成测试,您需要显式执行以下命令:
> gradle runIntegrationTest
这将调用 runIntegrationTest 任务并执行集成测试用例。如果您希望在构建代码时每次都执行这些测试用例,您可以使用 dependsOn 或其他依赖属性将此任务与其他任务链接。
代码覆盖率
有许多可用于源代码分析的覆盖率工具,例如 EMMA、Corbatura、JaCoCo 等。在本节中,我们将介绍 Gradle 与 JaCoCo 的集成以进行源代码分析。
在我们开始之前,我们需要了解代码覆盖率是什么以及为什么它在测试驱动开发中很重要。
代码覆盖率是我们用来检查源代码被测试了多少的指标。更高的代码覆盖率意味着我们的代码被测试的比例更大。代码覆盖率通常在单元测试周期中完成。在代码覆盖率期间,开发者必须确保源代码中的不同逻辑路径已被测试和验证,以达到更好的代码覆盖率。
这里,重要的是要理解代码覆盖率与代码质量没有直接关系。高代码覆盖率并不能保证编写了高质量的代码。开发者必须使用静态代码分析工具,如 PMD (pmd.github.io/) 来查找代码的质量。另一个需要记住的点是,即使有 100%的代码覆盖率,也不能保证编写了完全无错误的代码。因此,许多开发者认为这不是衡量代码质量或单元测试的正确指标。然而,70-80%的代码覆盖率被认为是健康代码覆盖率的好数字。
在 Gradle 中,代码覆盖率工具 JaCoCo 可以像任何其他插件一样应用于项目:
apply plugin: 'jacoco'
我们的build.gradle文件包含以下内容。我们创建了一些 TestNG 测试用例来测试源代码的功能。我们还配置了一个测试任务,使其依赖于jacocoTestReport任务。这是为了确保在运行和创建测试覆盖率报告之前执行测试用例:
apply plugin: 'java'
apply plugin: 'jacoco'
repositories {
mavenCentral()
}
dependencies {
testCompile 'org.testng:testng:6.8.8'
}
test{
systemProperty "url",System.properties['url']
useTestNG()
}
jacocoTestReport.dependsOn test
默认情况下,报告将在<build dir>/reports/jacoco/test/html目录下创建,并将生成一个 HTML 报告文件。例如,我们创建了一个简单的 POJO User.java文件,其中包含 getter 和 setter 方法。我们还创建了一些单元测试用例来验证功能。以下两个示例测试用例如下:
@Test
public void userEmailTest() {
User user1 = new User("User2", "User2 user2", "user2@abc.com");
Assert.assertEquals(user1.getEmail(), "user2@abc.com");
}
@Test
public void userIdTest() {
User user1 = new User();
user1.setUserId("User3");
user1.setName("User3 user3");
user1.setEmail("user3@abc.com");
Assert.assertEquals(user1.getName(), "User3 user3");
Assert.assertEquals(user1.getUserId(), "User3");
}
接下来,我们可以执行jacocoTestReport任务来生成代码覆盖率报告:
> gradle clean jacocoTestReport
:clean
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:jacocoTestReport
BUILD SUCCESSFUL
Total time: 7.433 secs
在覆盖率报告中,你可以观察到 Java 类的所有方法都经过了单元测试。你可以进一步通过报告中的链接深入挖掘,这些链接显示了源代码的行覆盖率。源代码用绿色和红色标记来显示已覆盖和未测试的部分。以下图(图 7.2)显示了User.java类的代码覆盖率统计:

图 7.2
默认情况下,HTML 报告文件将在build/reports/jacoco/test/html目录下生成。此外,jacoco插件的默认版本可以通过修改以下jacoco扩展来修改:
jacoco {
toolVersion = "<Required-Version>"
reportsDir = file("Path_to_Jacoco_ReportDir")
}
类似地,可以通过以下方式配置jacocoTestReport任务来自定义报告:
jacocoTestReport {
reports {
xml.enabled false
html.destination "<Path_to_dircectory>"
}
}
代码分析报告
Sonar 是最受欢迎的质量管理工具之一,它可以从代码行数、文档、测试覆盖率、问题复杂度等方面对项目进行全面分析。作为开发者,我们主要对以下领域感兴趣:
-
重复的代码行
-
源代码中缺少注释,尤其是在公共 API 中
-
不遵循编码标准和最佳实践
-
寻找代码复杂性
-
单元测试产生的代码覆盖率
在本节中,我们将讨论 Gradle 与 Sonar 的集成。唯一的前提是,Sonar 服务器应该已安装并运行。
运行 Sonar 的前提是在机器上安装 Java。一旦满足前提条件,你只需三个简单步骤就可以安装 Sonar,如下所示:
-
从
www.sonarqube.org/downloads/下载分发版并将其解压。 -
打开控制台并启动 Sonar 服务器:
-
在 Windows 平台上,启动
$SONAR_HOME\bin\windows-x86-32\StartSonar.bat -
在其他平台上,启动
$SONAR_HOME/bin/[OS]/sonar.sh
-
-
访问
http://localhost:9000。
要运行sonar-runner插件,我们只需应用sonar-runner插件并将其配置为连接到 Sonar 服务器。
使用以下内容为你的项目创建构建文件build.gradle:
apply plugin: 'groovy'
apply plugin: "sonar-runner"
repositories {
mavenCentral()
}
version = '1.0'
repositories {
mavenCentral()
}
sonarRunner {
sonarProperties {
property "sonar.host.url", "http://<IP_ADDRESS>:<PORT>"
property "sonar.jdbc.url",
"jdbc:h2:tcp://<IP_ADDRESS>:<PORT>/sonar"
property "sonar.jdbc.driverClassName", "org.h2.Driver"
property "sonar.jdbc.username", "sonar"
property "sonar.jdbc.password", "sonar"
}
}
上述配置是自我解释的。你需要添加诸如 Sonar URL、DB URL 和 JDBC 驱动程序详情等配置,我们的构建文件就准备好了。
下一步是运行sonarRunner任务进行代码分析。在成功执行此任务后,你将在 Sonar 服务器上找到报告:
>gradle clean sonarRunner
:clean
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:sonarRunner
SonarQube Runner 2.3
Java 1.7.0_51 Oracle Corporation (64-bit)
Windows 7 6.1 amd64
INFO: Runner configuration file: NONE
INFO: Project configuration file: <Project_Home>\UserService\build\tmp\sonarRunner\sonar-project.properties
INFO: Default locale: "en_IN", source code encoding: "windows-1252" (analysis is platform dependent)
INFO: Work directory: <Project_Home>\UserService\build\sonar
INFO: SonarQube Server 3.7.4
...
...
现在,你可以打开http://localhost:9000/来浏览项目。这个页面是默认仪表板页面,显示了所有项目的详细信息。你可以找到你的项目并浏览其详细信息。详细信息将如下显示:

图 7.3
你可以通过遵循项目主页提供的链接进一步验证每个指标的详细信息。例如,以下图显示了 Sonar 中的源代码相关指标。它提供了代码复杂性、代码行数、方法、文档等详细信息:

图 7.4
注意
你可以在docs.sonarqube.org/display/SONAR/Documentation/找到更多关于 Sonar 的信息。
摘要
在本章中,我们讨论了 Gradle 的测试和报告方面。我们的讨论从 TestNG 开始,并讨论了如何配置 Gradle 以支持集成测试用例与单元测试用例分离。然后,我们讨论了 JaCoCo 的代码覆盖率,最后我们讨论了 Gradle 与 Sonar 的集成。
在下一章中,我们将讨论如何在构建脚本和插件中组织构建逻辑。我们将探讨如何模块化插件代码,以便在多项目 Gradle 构建中共享。我们还将探讨如何在 Gradle 中创建自定义插件。
第八章:组织构建逻辑和插件
插件是 Gradle 的主要构建块之一,我们之前并没有过多讨论。你已经看到了不同的标准插件,如 Java、Eclipse、Scala 等,它们都附带了一系列定义好的任务。开发者只需包含插件,配置所需的任务,就可以利用其功能。在本章中,我们将概述插件是什么,如何将任务分组到插件中,如何将插件逻辑从构建文件提取到 buildSrc,以及如何创建独立的插件。
将构建逻辑提取到 buildSrc
插件不过是按照特定顺序和默认配置创建的任务组,旨在提供某种功能。例如,java 插件包含提供构建 Java 项目功能的任务,scala 插件包含构建 Scala 项目的任务,等等。尽管 Gradle 提供了许多标准插件,但你也可以找到不同的第三方插件来满足项目的需求。可能总会有这样的情况,即你无法使用现有的插件找到所需的功能,并希望为你的定制需求创建一个新的插件。我们将探讨开发者可以创建和使用插件的不同方式。
用户可以创建的第一个插件就是构建文件本身。以下是一个插件的示例代码,开发者可以在 build.gradle 中编写并使用它:
apply plugin: CustomPlugin
class CustomPlugin implements Plugin<Project> {
void apply(Project project) {
project.task('task1') << {
println "Sample task1 in custom plugin"
}
project.task('task2') << {
println "Sample task2 in custom plugin"
}
}
}
task2.dependsOn task1
在这里,我们在构建文件本身中创建了一个插件。这是 Gradle 脚本之美。你还可以在 Gradle 文件中编写一个类。要创建自定义插件,你需要创建一个实现 Plugin 接口的 Groovy 类。你甚至可以用 Java 或任何其他 JVN 语言编写插件。由于 Gradle 构建脚本是用 Groovy 编写的,所以我们使用了 Groovy 来编写插件实现。你想要实现的所有任务,都需要在 apply 方法中定义。我们定义了两个任务,task1 和 task2。我们还定义了生命周期作为两个任务之间的关系。如果开发者调用 task1,只有 task1 将被执行。如果你执行 task2,task1 和 task2 都将执行。尝试执行以下命令:
> gradle task2
:task1
Sample task1 in customer plugin
:task2
Sample task2 in custom plugin
BUILD SUCCESSFUL
Total time: 2.206 secs
小贴士
要在构建文件中使用插件,你始终需要使用 apply plugin:<plugin name/plugin class(如果插件是在同一脚本或 buildSrc 目录中实现的)。
这是一种开发者定义自定义插件简单的方式。然而,如果我们遵循设计原则,将构建逻辑和自定义逻辑混合到同一个文件中并不是一个好的实践。这将很难维护代码,也可能增加维护工作量。我们始终建议你将插件代码与构建逻辑分开编写。为了实现这一点,Gradle 提供了两种不同的方式,如下所示:
-
将插件代码提取到
buildSrc -
独立插件
为了将插件代码提取到buildSrc,Gradle 建议您在项目目录内创建一个buildSrc目录,并将插件代码保存在那里。以下是该目录结构的示例:
C:./Gradle/Chapter8/CustomPlugin1
│ build.gradle
│
└───buildSrc
└───src
└───main
└───groovy
└───ch8
CustomPlugin.groovy
在这里,我们已经创建了一个单独的buildSrc目录;在该目录内,我们将插件代码保存在CustomPlugin.groovy文件中。将前面的 Groovy 类从build.gradle文件移动到这个文件中。在顶部包含包声明。您还需要导入org.gradle.api.*。您的CustomPlugin.groovy文件将如下所示:
package ch8
import org.gradle.api.*
class CustomPlugin implements Plugin<Project> {
// Plugin functionality here
}
build.gradle文件的内容将如下所示:
import ch8.CustomPlugin
apply plugin: CustomPlugin
您只需导入包并添加apply plugin语句。所有编译类和将类包含到运行时类路径中的后台工作将由 Gradle 执行。现在,尝试执行以下命令:
> gradle task1
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:task1
Sample task1 in custom plugin
BUILD SUCCESSFUL
Total time: 3.374 secs
这里,您可以看到 Gradle 为您自定义的插件代码执行了编译和构建任务,现在您只需执行自定义插件中的任务。Gradle 还允许您在构建文件中配置自定义插件。您可以在任务之间设置依赖关系或向构建文件中的任务添加更多功能,而不是反复更新插件代码。如果您想为task1添加更多功能,可以按照以下方式操作:
task1.doLast {
println "Added more functionality to task1"
}
task2.dependsOn task1
现在,如果您尝试执行task1,它将附加前面的语句。
以这种方式,您可以将构建逻辑从build.gradle文件分离出来,将其放置在buildSrc目录下的一个单独的类文件中。如果您有一个多项目构建,根项目buildSrc中定义的插件可以被所有子项目的构建文件重用。您不需要为每个子项目定义一个单独的插件。这个过程仍然有一个限制。它不允许您将此插件用于其他项目。由于它与当前项目紧密耦合,您只能使用此插件与同一项目或根项目中定义的子项目。为了克服这一点,您可以将插件代码提取出来,创建一个独立的插件,并将其打包成一个 JAR 文件,这样您就可以将其发布到仓库中,以便任何项目都可以重用它。在下一节中,我们将讨论独立插件。
第一个插件
为了使插件对所有其他项目可重用,Gradle 允许您将插件代码分离出来,并将其打包成一个 JAR 文件。您可以将此 JAR 文件包含在任何您想要重用此功能的项目中。您可以使用 Java 或 Groovy 创建独立项目。我们将使用 Groovy。您可以使用任何编辑器(Eclipse、NetBeans 或 Idea)来创建插件。由于我们的主要目的是向您展示如何创建独立插件,我们不会深入编辑器的细节。我们将使用一个简单的文本编辑器。要继续创建独立插件,将上述buildSrc代码分离到一个独立的目录中。您可以将其命名为CustomPlugin。因此,目录结构将如下所示:
C:/Gradle/Chapter8/CustomPlugin.
│ build.gradle
│
└───src
└───main
└───groovy
└───ch8
CustomPlugin.groovy
你可能会惊讶地想知道为什么在这里创建 build.gradle 文件。通过这个 build.gradle 文件,我们将插件代码打包成一个 jar 文件。现在,问题出现了,那就是你将如何将这个插件包含到其他构建文件中。你需要为这个插件提供一个 插件 ID。为了给插件添加一个插件 ID,你需要在 src/main/resources/META-INF/gradle-plugins 目录中创建一个属性文件。属性文件的名字将是你的插件 ID。在这里,我们将在上述目录中添加 customplugin.properties 文件。这个文件的内容如下:
implementation-class=ch8.CustomPlugin
Your build file content would be.
apply plugin: 'groovy'
version = '1.0'
dependencies {
compile gradleApi()
compile localGroovy()
}
要编译 Groovy 代码,你需要在编译配置中包含前面的两个语句。由于我们在这里使用的是一个普通的 Groovy 类,所以我们没有添加任何其他的依赖 JAR 文件。如果你的插件代码依赖于任何其他的第三方 JAR 文件,你可以在依赖中包含它们,并配置相应的仓库。
现在,我们将按照以下方式构建插件:
> gradle clean build
:clean
:compileJava UP-TO-DATE
:compileGroovy
:processResources
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:compileTestGroovy UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build
BUILD SUCCESSFUL
Total time: 4.671 secs
你可以在 <project>/build/libs/CustomPlugin-1.0.jar 中找到这个 JAR 文件。
你可以将这个插件 JAR 发布到组织的内部仓库中,这样其他项目可以直接从那里下载并使用它。现在,我们将创建另一个项目,并将这个插件 JAR 引用到那个项目中。
创建一个新的目录,SampleProject,并将 build.gradle 文件添加到项目中。现在,一个问题出现了,那就是你的 build.gradle 文件将如何引用 SamplePlugin。为此,你需要在 buildscript closure 中提及 SamplePlugin JAR 文件的位置,并在 dependencies closure 中添加对这个 JAR 文件的依赖。
你的 build.gradle 文件内容如下:
buildscript {
repositories {
flatDir {dirs "../CustomPlugin/build/libs/"}
}
dependencies {
classpath group: 'ch8', name: 'CustomPlugin',version: '1.0'
}
}
apply plugin: 'customplugin'
在这里,我们使用的是 flat file repository,因此使用 flatDir 配置来引用自定义插件 JAR 文件。我们建议你使用组织的本地仓库;这样,组织的任何项目都可以集中访问。在 dependencies closure 中,我们引用了 CustomPlugin JAR 文件。这是使用任何插件的前提。最后,我们添加了 apply plugin 语句,并用单引号提到了插件名称。
小贴士
插件名称是你创建在 src/main/resources/META-INF/gradle-plugins 目录中的属性文件的名称。
现在,你可以使用以下命令执行构建文件:
> gradle task1
:task1
Sample task1 in custom plugin
BUILD SUCCESSFUL
Total time: 2.497 secs
配置插件
到目前为止,我们已经看到了如何创建一个独立的自定义插件并将其包含在另一个项目的构建文件中。Gradle 还允许你配置插件属性并根据项目需求进行自定义。你已经学习了如何在一个java插件中自定义源代码位置和测试代码位置。我们将看到一个示例,展示你如何在自定义插件中复制相同的行为。要定义插件属性,你需要创建一个额外的extension类并将该类注册到你的plugin类中。假设我们想向插件添加location属性,创建CustomPluginExtension.groovy类如下:
package ch8
class CustomPluginExtension {
def location = "/plugin/defaultlocation"
}
现在,将此类注册到你的plugin类中:
class CustomPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create("customExt",CustomPluginExtension)
project.task('task1') << {
println "Sample task1 in custom plugin"
println "location is "+project.customExt.location
}
}
}
现在,再次构建插件,以确保你的更改成为最新插件 JAR 文件的一部分,然后尝试执行SampleProject的build.gradle:
> gradle task1
:task1
Sample task1 in custom plugin
location is /plugin/defaultlocation
BUILD SUCCESSFUL
Total time: 2.79 secs
在这里,你可以看到命令行输出的默认值。如果你想将此字段更改为其他值,请将customExt closure添加到你的SampleProject build.gradle文件中,并为位置配置不同的值:
buildscript {
repositories {
flatDir {dirs "../CustomPlugin/build/libs/"}
}
dependencies {
classpath group: 'ch8', name: 'CustomPlugin',version: '1.0'
}
}
apply plugin: 'customplugin'
customExt {
location="/plugin/newlocation"
}
现在,再次尝试执行task1:
> gradle task1
:task1
Sample task1 in custom plugin
location is /plugin/newlocation
BUILD SUCCESSFUL
Total time: 5.794 secs
在这里,你可以观察到位置属性的更新值。
摘要
在本章中,我们讨论了 Gradle 的主要构建块之一,插件。插件有助于组织和模块化功能,并有助于打包一系列相关的任务和配置。我们还讨论了创建自定义插件的不同方法,从在构建文件中编写插件代码到创建独立的插件 JAR 文件并在不同的项目中重用它。在最后一节中,我们还介绍了如何配置插件现有的属性并根据项目需求进行自定义。
在下一章结束本书之前,我们将讨论如何借助 Gradle 构建 Groovy 和 Scala 项目。此外,鉴于这是一个移动时代,所有传统的软件或 Web 应用程序现在都在转向应用,我们还将讨论构建 Android 项目。
第九章:多语言项目
我们生活在一个一个语言不足以应对的时代。开发者们被期望成为多语言程序员,并为工作选择合适的工具。虽然这始终是一个主观的决定,但我们尝试根据各种参数来选择语言和生态系统,例如执行速度、开发者生产力、可用的库和资源、团队对语言的舒适度等等。
当我们已经在处理不同语言时承受着认知负荷,Gradle 便成了我们的好朋友,因为我们不需要改变我们的构建工具,即使我们在用其他语言构建项目。我们甚至可以在同一个项目中使用多种语言,由 Gradle 来协调整个项目的构建。除了 JVM 基础语言之外,Gradle 还支持 C、C++、Objective C 等其他语言,以生成原生应用程序。Gradle 也是 Android 平台的官方构建工具。支持的语言列表正在不断增加。除了官方插件之外,还有许多社区支持的编程语言插件。
尽管在整个书中我们主要关注 Java 语言,但我们完全可以使用 Groovy 或 Scala 来编写示例。java 插件(以及由 java 插件应用于项目的 java-base 插件)为 JVM 基础项目提供了基本功能。特定语言的插件,如 scala 和 groovy,以一致的方式扩展了 java 插件以支持常见的编程习惯。因此,一旦我们使用了 java 插件,我们就已经熟悉了 sourceSet 是什么,configuration 如何工作,如何添加库依赖等等,这些知识在我们使用这些语言插件时非常有用。在本章中,我们将看到如何通过添加 Groovy 或 Scala 来轻松地为 Java 项目增添更多色彩。
多语言应用程序
对于代码示例,在本章中,让我们构建一个简单的“每日名言”服务,该服务根据年份的某一天返回一个名言。由于我们存储的名言可能较少,该服务应以循环方式重复名言。再次强调,我们将尽量保持简单,以便更多地关注构建方面而不是应用逻辑。我们将创建两个独立的 Gradle 项目来实现完全相同的功能,一次使用 Groovy,然后使用 Scala。
在深入探讨特定语言细节之前,让我们先定义 QotdService 接口,它仅声明了一个方法,即 getQuote。合同规定,只要我们传递相同的日期,就应该返回相同的名言:
package com.packtpub.ge.qotd;
import java.util.Date;
interface QotdService {
String getQuote(Date day);
}
实现getQuote的逻辑可以使用Date对象以任何方式,例如使用包括时间在内的整个日期来确定引语。然而,为了简单起见,我们将在我们的实现中仅使用Date对象的日期部分。此外,因为我们想让我们的接口对未来的实现开放,所以我们让getQuote接受一个Date对象作为参数。
此接口是一个 Java 文件,我们将在两个项目中都有。这只是为了演示在一个项目中集成 Java 和 Groovy/Scala 源。
构建 Groovy 项目
让我们先在 Groovy 中实现QotdService接口。此外,我们还将编写一些单元测试以确保功能按预期工作。为了启动项目,让我们创建以下目录结构:
qotd-groovy
├── build.gradle
└── src
├── main
│ ├── groovy
│ │ └── com
│ │ └── packtpub
│ │ └── ge
│ │ └── qotd
│ │ └── GroovyQotdService.groovy
│ └── java
│ └── com
│ └── packtpub
│ └── ge
│ └── qotd
│ └── QotdService.java
└── test
└── groovy
└── com
└── packtpub
└── ge
└── qotd
└── GroovyQotdServiceTest.groovy
src/main/java目录是 Java 源文件的默认目录。同样,src/main/groovy默认用于编译 Groovy 源文件。再次强调,这只是一种约定,源目录的路径和名称可以通过sourceSets轻松配置。
让我们先为我们的 Groovy 项目编写构建脚本。在项目根目录中创建一个build.gradle文件,内容如下:
apply plugin: 'groovy'
repositories {
mavenCentral()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.5'
testCompile 'junit:junit:4.11'
}
构建 Groovy 项目就像构建 Java 项目一样简单。我们不是应用java插件,而是应用groovy插件,它会自动为我们应用java插件。除了应用插件之外,我们还需要将 Groovy 添加为库依赖项,以便它在编译时可用,并在运行时也可用。我们还在testCompile配置中添加了junit,以便它可用于单元测试。我们声明 Maven central 作为要使用的仓库,但这也可能被更改为任何可以为我们项目依赖项服务的有效仓库配置。
注意
Gradle 构建脚本是一个 Groovy DSL,Gradle 的部分是用 Groovy 编写的。然而,就像 Gradle 在运行时依赖的任何其他库一样,Groovy 并不是隐式地对我们正在构建的项目可用。因此,我们必须显式地将 Groovy 声明为项目依赖项,具体取决于我们是否在生产或测试源中使用 Groovy。
Groovy 插件负责编译项目中的 Java 源文件。让我们用 Groovy 实现QotdService接口:
package com.packtpub.ge.qotd
class GroovyQotdService implements QotdService {
List quotes
GroovyQotdService(List quotes) {
this.quotes = quotes
}
@Override
String getQuote(Date day) {
quotes[day[Calendar.DAY_OF_YEAR] % quotes.size()]
}
}
服务的实现接受一个包含引语的构造函数中的列表。getQuote方法通过列表中的索引获取引语。为了确保计算出的索引始终保持在引语大小的范围内,我们获取了年份和列表大小的余数。
为了测试服务,让我们用 Groovy 编写非常基本的 JUnit 测试用例:
package com.packtpub.ge.qotd
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertNotSame
public class GroovyQotdServiceTest {
QotdService service
Date today, tomorrow, dayAfterTomorrow
def quotes = [
"Be the change you wish to see in the world" +
" - Mahatma Gandhi",
"A person who never made a mistake never tried anything new" +
" - Albert Einstein"
]
@Before
public void setup() {
service = new GroovyQotdService(quotes)
today = new Date()
tomorrow = today + 1
dayAfterTomorrow = tomorrow + 1
}
@Test
void "return same quote for same date"() {
assertEquals(service.getQuote(today), service.getQuote(today))
}
@Test
void "return different quote for different dates"() {
assertNotSame(service.getQuote(today),
service.getQuote(tomorrow))
}
@Test
void "repeat quotes"() {
assertEquals(service.getQuote(today),
service.getQuote(dayAfterTomorrow))
}
}
我们在设置中准备测试数据,每个测试用例都确保引语服务的契约得到维护。由于引语列表中只有两个引语,它们应该每隔一天重复一次。
我们可以使用以下代码从命令行运行测试:
$ gradle test
构建 Scala 项目
在上一节之后,本节的大部分内容从应用程序构建的角度来看应该是可预测的。所以让我们快速浏览一下要点。目录结构如下:
qotd-scala
├── build.gradle
└── src
├── main
│ ├── java
│ │ └── com/packtpub/ge/qotd
│ │ └── QotdService.java
│ └── scala
│ └── com/packtpub/ge/qotd
│ └── ScalaQotdService.scala
└── test
└── scala
└── com/packtpub/ge/qotd
└── ScalaQotdServiceTest.scala
所有 Scala 源文件都从 src/main/scala 和 src/test/scala 读取,除非使用 sourceSets 进行配置。这次,我们只需要应用 scala 插件,就像 groovy 插件一样,它隐式地将 java 插件应用到我们的项目中。让我们为这个项目编写 build.gradle 文件:
apply plugin: 'scala'
repositories {
mavenCentral()
}
dependencies {
compile 'org.scala-lang:scala-library:2.11.7'
testCompile 'org.specs2:specs2-junit_2.11:2.4.15',
'junit:junit:4.11'
}
在这里,我们必须提供 scala-library 作为依赖项。我们还为测试配置添加了 specs2 作为依赖项。我们正在使用 JUnit 运行器进行测试。
注意
specs2 是一个流行的 Scala 测试库,它支持单元测试和验收测试,以及 BDD/TDD 风格的测试编写。更多信息可以在 etorreborre.github.io/specs2/ 找到。
接下来,我们将实现服务的 Scala 版本,可以按照以下方式实现:
package com.packtpub.ge.qotd
import java.util.{Calendar, Date}
class ScalaQotdService(quotes: Seq[String]) extends QotdService {
def getQuote(day: Date) = {
val calendar = Calendar.getInstance()
calendar.setTime(day)
quotes(calendar.get(Calendar.DAY_OF_YEAR) % quotes.size)
}
}
实现并不是非常符合 Scala 的习惯用法,但这本书的范围之外。该类在构造函数中接受 Seq 引用,并以类似 Groovy 对应方式实现 getQuote 方法。
现在服务已经实现,让我们通过编写单元测试来验证它是否遵循 QotdService 的语义。为了简洁,我们将只涵盖重要的测试用例:
package com.packtpub.ge.qotd
import java.util.{Calendar, Date}
import org.junit.runner.RunWith
import org.specs2.mutable._
import org.specs2.runner.JUnitRunner
@RunWith(classOf[JUnitRunner])
class ScalaQotdServiceTest extends SpecificationWithJUnit {
def service = new ScalaQotdService(Seq(
"Be the change you wish to see in the world" +
" - Mahatma Gandhi",
"A person who never made a mistake never tried anything new" +
" - Albert Einstein"
))
val today = new Date()
val tomorrow = incrementDay(today)
val dayAfterTomorrow = incrementDay(tomorrow)
"Quote service" should {
"return same quote for same day in multiple invocations" in {
service.getQuote(today) must be(service.getQuote(today))
}
"return different quote for different days" in {
service.getQuote(today) must not be (
service.getQuote(tomorrow))
}
"repeat quote if total quotes are less than days in year" in {
service.getQuote(today) must be(
service.getQuote(dayAfterTomorrow))
}
}
def incrementDay(date: Date) = {
val cal = Calendar.getInstance()
cal.setTime(date)
cal.add(Calendar.DATE, 1)
cal.getTime
}
}
运行测试用例的任务与 Groovy 对应的任务相同。我们可以使用以下代码来运行测试:
$ gradle test
联合编译
在本章前面的示例中,我们分别在 Java 中声明了一个接口,并在 Groovy 和 Scala 中分别实现了它。这是可能的,因为由 java 插件编译的类对 Groovy 和 Scala 类是可用的。
如果我们想让 Java 类在编译时能够访问 Groovy 或 Scala 类,那么我们必须使用相应插件支持的 联合编译 来编译 Java 源文件。groovy 和 scala 插件都支持联合编译,并且可以编译 Java 源代码。
在 Java 类中引用 Groovy 类的最简单方法是,将相应的 Java 源文件移动到 src/main/groovy(或为 sourceSets 配置的任何 Groovy srcDirs),Groovy 编译器在编译时使 Groovy 类对 Java 类可用。Scala 联合编译也是如此。我们可以将需要 Scala 类进行编译的 Java 文件放在 Scala 的任何 srcDirs 中(默认为 src/main/scala)。
参考
本章讨论的语言插件的详细官方文档可以在以下 URL 中找到:
-
Groovy 插件:
docs.gradle.org/current/userguide/groovy_plugin.html -
Scala 插件:
docs.gradle.org/current/userguide/scala_plugin.html
可以在以下网址找到各种语言和 Gradle 一起提供的其他插件的官方文档链接:
docs.gradle.org/current/userguide/standard_plugins.html
摘要
我们选取了一个简单的示例问题,并在 Groovy 和 Scala 中实现了解决方案,以展示 Gradle 如何使多语言项目开发变得简单。我们试图专注于 Gradle 带来的共性和一致性,而不是深入到语言和插件特定的细节和差异。



浙公网安备 33010602011771号