精通-Gradle-全-

精通 Gradle(全)

原文:zh.annas-archive.org/md5/f00ea3cb2cb8d986576ff1a526090a0b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书是学习使用 Gradle 的企业级构建系统的实用指南。本书帮助您掌握工具的核心概念,并快速将知识应用于实际项目。全书各章节都配有足够的示例,以便读者轻松跟随并吸收概念。本书分为 10 章。前六章旨在获取关于基本主题的知识,如任务、插件、依赖管理、各种内置插件等。接下来的几章涵盖了各种主题,如持续集成、迁移和部署,使读者能够学习对敏捷软件开发非常有用的概念。本书最后一章专注于使用 Gradle 的 Android 构建系统,这对移动开发者非常有用。

本书涵盖的内容

第一章, Gradle 入门,简要介绍了构建自动化系统、其需求以及 Gradle 如何帮助开发者自动化构建和部署过程。本章除了介绍 Gradle 的安装、配置和功能外,还讨论了一些重要概念,如初始化脚本、Gradle 图形用户界面和 Gradle 命令行选项。

第二章, Gradle 的 Groovy 基础知识,讨论了 Groovy 编程语言的基本概念。本章还讨论了类、Bean 和集合框架。本章为读者提供了关于 Groovy 的预览,这是学习 Gradle 所必需的。

第三章, 任务管理,详细讨论了 Gradle 中的基本操作单元——任务。开发者将了解不同类型的任务,如内置任务和自定义任务。本章还讨论了任务配置、任务排序和任务依赖。

第四章, 插件管理,讨论了 Gradle 的重要构建块之一,插件。读者将学习如何创建简单插件和自定义插件。此外,用户将能够根据其需求配置插件。本章还详细讨论了最实用的插件之一,Java 插件。用户将了解所支持的不同约定以及如何根据项目/组织的需要自定义标准约定。

第五章, 依赖管理,详细讨论了 Gradle 的另一重要功能——依赖管理。它讨论了依赖解析、依赖配置和依赖定制。它还讨论了仓库管理。它提供了用户如何配置不同外部仓库、内部仓库以及如何将本地文件系统用作仓库的深入见解。

第六章, 使用 Gradle,讨论了两个额外的插件,War 和 Scala。它还讨论了诸如属性管理、多项目构建和日志功能等不同主题。用户将了解不同的 I/O 操作,以及使用 JUnit 和 TestNG 在 Gradle 中进行的单元测试功能。

第七章, 持续集成,讨论了持续集成概念和工具,如 Jenkins 和 TeamCity,以及它们与 Gradle 的集成。它还讨论了不同的代码质量插件(Checkstyle、PMD 和 Sonar)与 Gradle 的集成。

第八章, 迁移,满足了一些已经使用其他构建工具(如 Ant 或 Maven)并希望迁移到 Gradle 的用户的关键需求。它讨论了将现有的 Ant 和 Maven 脚本转换为 Gradle 的不同迁移策略。

第九章, 部署,解释了软件工程的部署方面。用户如何顺利地自动化部署过程,这可以节省大量的开发人员和运维团队的时间和精力。它讨论了基于容器的部署自动化流程和工具;Docker。它提供了关于 Docker 安装、有用的 Docker 命令以及如何将 Docker 与持续集成工具和 Gradle 集成以创建构建-部署-测试工作流程的详细信息。

第十章, 使用 Gradle 构建 Android 应用程序,讨论了移动应用程序的开发和部署。Gradle 是 Android 的官方构建工具。本章重点介绍示例 Android 应用程序开发以及不同的部署策略,例如部署调试版本、发布版本、在不同配置上的部署等。

您需要为此书准备的内容

在执行书中提到的代码之前,您的系统必须安装以下软件:

  • Gradle 2.4

  • Java 1.7 或更高版本

  • Jenkins

  • TeamCity

  • Ant 1.9.4

  • Maven 3.2.2

  • Docker 1.5.0

  • Android 5.0

这本书面向的对象

如果您是一位有 Gradle 经验且希望成为专家的 Java 开发者,那么这本书就是为您准备的。对 Gradle 的基本知识是必要的。

规范

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:"Gradle 共享由环境变量JAVA_OPTS设置的相同的 JVM 选项。"

代码块设置如下:

def methodMissing(String name, args) {
  if (name.startsWith("plus") ) {
// write your own implementation
    return "plus method intercepted"
  }
  else {
    println "Method name does not start with plus"
    throw new MissingMethodException(name, this.class, args)
  }
}

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将被设置为粗体:

apply plugin: 'java'
version=1.0
configurations {
  customDep
}
repositories {
  mavenCentral()
}

任何命令行输入或输出都应如下所示:

$ gradle –b build_customconf.gradle showCustomDep
:showCustomDep

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“点击确定以添加存储库。”

注意

警告或重要注意事项以这样的框出现。

小贴士

小技巧和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中受益的书籍。

要向我们发送一般反馈,请简单地发送电子邮件到 <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 入门

考虑一个典型的 IT 公司开发中心场景。不同的团队在一个包含许多组件的企业项目中共同工作。团队在服务器端技术、前端技术、消息层、移动开发等方面工作,可能还有一个单独的团队负责质量保证。每个团队都按照自己的进度工作,开发自己的组件,进行单元测试和提交代码,并且这个周期在多个迭代中重复。到目前为止,每个人都感到很高兴,因为他们能够按照软件发布日期按时完成工作。然后是集成阶段,当团队必须构建完整的项目并将软件(可能是 WAR、JAR 或任何服务)部署到集成/预发布环境中时。然后噩梦开始了。

尽管每个团队都成功地遵循了许多软件工程的最佳实践,例如每天提交代码、对代码进行单元测试以及在开发者的测试环境中验证工作的软件,但在集成或预发布环境中,情况突然发生了变化。团队陷入了配置和互操作问题、本地化问题、环境问题等等。

这可能对任何项目来说都是一个非常常见的场景,如果他们没有使用任何自动化解决方案来构建和部署流程,情况会变得更糟。因此,需要自动化流程,或者我们可以称之为 构建自动化系统BAS),它能够无缝地自动化构建项目的手动任务,并以可重复、可靠和可移植的方式交付软件。BAS 不声称将完全没有问题或错误,但有了 BAS,软件可以更好地管理,最大限度地减少重复犯同样错误的可能性。

Gradle 是市场上可用的先进构建自动化工具之一。在接下来的 10 章中,我们将探讨如何使用 Gradle 和其他相关技术来缓解这些问题。然而,在我们开始学习 Gradle 之前,我们需要了解什么是 BAS 以及为什么我们需要它。

理解构建自动化系统

构建任何软件中最常见的流程包括编译源文件、将编译输出打包成压缩格式(ZIP、JAR 或任何其他格式),并将所需的资源文件和配置文件添加到打包中。除此之外,还可能包括一些其他活动,例如在源代码上运行静态代码分析以提供关于设计和编码模式反馈,另一个重要领域是质量保证,它涉及单元测试、集成测试、回归测试等。

BAS 是软件生命周期的一部分,它自动化了软件的构建和部署阶段。第一阶段是构建软件,这是创建二进制文件或可执行文件的过程。第二阶段是部署阶段,其中我们需要在特定位置安装软件。此阶段还包括各种其他活动,如解包捆绑包、软件本地化、根据环境配置软件以及设置执行软件所需的环境特定属性。下一个重要步骤是功能测试,以检查软件的行为。一旦一切正常,对你来说就是一个快乐和微笑的结局。

因此,作为一名开发者,编写代码和测试用例只是软件开发生命周期SDLC)中的主要任务之一。构建和部署也被视为任何软件生命周期中的另一个重要阶段。如果管理不当,可能会导致重大停机时间和客户不满。

构建自动化使我们能够自动化构建过程中的手动步骤。它还有助于消除冗余任务,减轻手动干预的风险,保持构建的历史记录,并节省手动过程中的成本和时间。这里的目的是每次运行构建脚本时都创建可重复的资产,而如果你每次都手动执行步骤,则不会是这样。

许多开发者将构建自动化与持续集成CI)联系起来。不要混淆。CI 允许执行构建过程、执行部署活动以及许多其他活动。它有助于创建构建和部署自动化的工作流程。它还帮助安排构建并提供按需执行构建。计划可以是每小时一次、每四小时一次、夜间构建或每次用户提交时。一些知名的 CI 工具包括 Jenkins、TeamCity、Bamboo、Hudson、Cruise Control 等,它们与构建工具(如 Ant、Maven 和 Gradle)完全不同。

BAS 的必要性

想象一下,所有前面提到的构建软件的步骤都需要手动完成,每个开发者都必须在不同的机器上执行步骤。现在你可以意识到,在构建问题而不是关注实际业务需求上浪费了多少努力。这就是我们需要 BAS 的原因之一。以下是我们为构建系统自动化的主要活动之一:

  • 将源代码翻译成二进制文件

  • 将二进制文件与配置文件打包以创建可部署的工件

  • 执行测试用例

  • 将工件发布到公共仓库

  • 将工件部署到不同的环境(开发、QA 和生产)

  • 增量构建

  • 总结构建当前状态的状况报告

另一个拥有构建自动化系统(BAS)的原因是减少操作复杂性。如果一个新成员加入团队,并且他必须手动构建软件,如果没有自动化,这可能会成为他的噩梦。与其专注于业务需求,他们的大部分时间将浪费在如何编译它、如何运行单元测试、如何执行集成测试等等。

实际上,他需要知道的是在哪里提交源代码,在哪里放置资源,以及执行构建过程需要运行哪些命令。构建过程应该自动执行编译、打包、运行测试、上传断言等所有任务。

构建和部署过程越自动化,你将越快地将可交付成果提供给客户。它还有助于业务连续性。在系统崩溃或网络故障的情况下,你可以在更短的时间内重建和部署软件到备份基础设施上。

一些开发者认为项目自动化是浪费时间,为什么他们要付出额外的努力,因为他们的 IDE 已经完成了这项工作。他们可以在 IDE 的帮助下构建 JAR、WAR 或任何其他可交付单元,并部署相同的单元。由于他们可以快速构建和测试,它们在本地系统上运行得非常好。问题开始于集成发生时。因此,需要一个自动化系统来避免任何手动干预(除非这是唯一的选择),并使构建可移植、可预测和高效。

Gradle 概述

在深入了解 Gradle 的细节之前,我们需要了解一些与构建系统相关的术语。

建筑工具有两种类型,即命令式构建工具声明式构建工具。命令式构建工具告诉系统做什么以及如何做。换句话说,它提供一组动作语句或命令,系统按相同的顺序执行这些命令并执行这些动作。你可以将 Ant 作为命令式构建系统的例子。

相反,声明式构建工具指导系统,告诉它你想要实现什么,系统将找出如何解释它。使用声明式方法,用户只需要确定什么,而不是如何。这是 Maven 在 Ant 获得一些知名度后为构建世界带来的关键创新之一,我们不需要编写每个动作的每一步,最终创建一个非常庞大且冗长的构建脚本。使用 Maven,我们需要为构建和构建系统本身编写一些配置参数,构建系统自己决定如何解释它。内部,声明式层基于一个强大的命令式层,可以根据需要直接访问。Ant 和 Maven 是非常好的且可靠的构建系统。它们在它们设计和构建的所有领域都是创新的。每个都为构建空间引入了关键的创新。

Gradle 结合了两种工具的优点,并提供了额外的功能,同时使用 Groovy 作为领域特定语言DSL)。它具有 Ant 工具的强大功能和 Maven 的构建生命周期以及易用性。

Gradle 是一个通用、声明式的构建工具。它是通用的,因为你可以用它来构建几乎任何你在构建脚本中想要实现的东西。它是声明式的,因为你不想在构建文件中看到大量的代码,这些代码难以阅读和维护。因此,虽然 Gradle 提供了约定和简单、声明式构建的概念,但它也使工具具有适应性,并赋予开发者扩展的能力。它还提供了一种轻松定制默认行为和添加任何第三方功能的不同挂钩的方法。

主要来说,Gradle 是一个 JVM 语言构建工具,但它也支持 C、C++、Android 等。你可以在docs.gradle.org/current/userguide/nativeBinaries.html找到更多关于此的信息。

它为 Java 项目所需的各个阶段提供自动化,例如编译、打包、执行测试用例等。它将类似的自动化任务分组为插件。当你将任何插件导入 Gradle 脚本文件时,它们都会附带一系列预定义的任务。要开始使用 Gradle,你需要具备基本的 Java 知识。它使用 Groovy 作为其脚本语言,这也是另一种 JVM 语言。我们将在下一章讨论 Groovy。由于构建脚本是用 Groovy 编写的,因此它比用 Ant 或 Maven 编写的脚本要短得多,表达性更强,也更清晰。在 Gradle 中使用 Groovy DSL 时,样板代码的数量要少得多。它还利用了 Maven 的约定以提高熟悉度,同时使其易于根据项目需求进行定制。开发者可以在任何时候添加新功能或扩展现有功能。他们可以覆盖现有任务或插件以提供新功能。

安装和快速入门

Gradle 的安装相当简单。你可以从 Gradle 主页www.gradle.org/downloads下载 Gradle 发行版,它以不同的格式提供。

前置条件

Gradle 需要安装 Java JDK 或 JRE,需要版本 6 或更高(要在你的机器上检查 Java 版本,请使用java -version)。一些功能可能不与 JRE 兼容,因此建议安装 JDK。此外,Gradle 自带其自己的 Groovy 库;因此,不需要安装 Groovy。Gradle 会忽略任何现有的 Groovy 安装。

Gradle 有三种格式可供选择:

  • gradle-[version]-all.zip:此文件包含源代码、二进制文件和文档

  • gradle-[version]-bin.zip:此文件仅包含二进制文件

  • gradle-[version]-src.zip:此文件仅包含源代码,以防你想扩展 Gradle 的功能

或者,你也可以直接下载 gradle-[version]-bin.zip 文件。

下载完成后,你需要解压 zip 文件,并根据你的操作系统进行配置。

Gradle for Windows

以下是在 Windows 上安装 Gradle 的步骤:

  1. 在硬盘上解压 Gradle 发行版。

  2. 将 Gradle 的安装路径(例如,c:\gradle-2.4)添加到 GRADLE_HOME 变量中。请注意,此位置应该是 binlib 文件夹的父目录。

  3. GRADLE_HOME/bin 添加到 PATH 变量中。

当你准备好使用 Gradle 时,通过运行带有 --version-v 命令行参数的 gradle 命令来验证你的安装。

> gradle –version

------------------------------------------------------------
Gradle 2.4
------------------------------------------------------------

Build time:   2015-05-05 08:09:24 UTC
Build number: none
Revision:     5c9c3bc20ca1c281ac7972643f1e2d190f2c943c

Groovy:       2.3.10
Ant:          Apache Ant(TM) version 1.9.4 compiled on April 29 2014
JVM:          1.7.0_79 (Oracle Corporation 24.79-b02)
OS:           Windows 8.1 6.3 amd64

Gradle for Mac/Linux

以下是在 Mac/Linux 操作系统上安装 Gradle 的步骤。

  1. 解压 Gradle 发行版。

  2. 在你的初始化脚本(~/.profile)中添加以下两行。

  3. 导出 GRADLE_HOME = <Gradle_Installation_Dir>

  4. 导出 PATH=$PATH:$GRADLE_HOME/bin

通过执行 source ~/.profile 来重新加载配置文件,并执行 gradle –version 命令。你将能够看到与上一节中提到的类似输出。

Gradle JVM 选项

Gradle 与环境变量 JAVA_OPTS 设置的 JVM 选项相同。如果你不想使用此设置,并想专门将参数传递给 Gradle 运行时,你可以使用环境变量 GRADLE_OPTS

假设你的系统中的 JAVA_OPTS=512MB,并且你想将 Gradle 应用的默认最大堆大小增加到 1024MB。你可以这样设置:

GRADLE_OPTS="-Xmx1024m"

我们可以在项目特定的构建文件中应用此设置。或者,我们也可以通过将变量添加到 Gradle 启动脚本中(这将在本章后面讨论)来将此设置应用于所有 Gradle 构建。

我们的第一脚本

在最后一节中,我们学习了如何安装 Gradle。现在是我们创建第一个 Gradle 脚本的时候了。这个脚本将在控制台上打印 Hello Gradle- This is your first script。只需打开一个文本编辑器,输入以下三行,并将文件保存为 build.gradle

task helloGradle << {
      println 'Hello Gradle- This is your first script'
}

然后按照以下方式执行 gradle helloGradle 命令:

$ gradle helloGradle
:helloGradle
Hello Gradle- This is your first script
BUILD SUCCESSFUL
Total time: 4.808 secs

那么,我们在这里做了什么呢?

  • 我们创建了一个名为 build.gradle 的 Gradle 构建脚本文件。这是构建文件的默认名称。你可以给构建文件起任何名字。然而,为了执行脚本,你必须使用带有文件名的 -b 选项与 gradle 命令一起使用。否则,构建将失败,并显示 "Task '%TASK_NAME%' not found in root project '%PROJECT_NAME'" 错误。

  • 尝试执行 gradle -b <buildfile_name> helloGradle 命令,你应该会得到相同的结果。

  • 使用 gradle 命令,我们已经执行了一个名为helloGradle的任务,该任务在控制台打印一行。因此,我们传递给 gradle 命令的参数是任务名称。您可以使用 Gradle 命令执行一个或多个任务,并且这些任务将按照它们在命令行中出现的顺序执行。

    小贴士

    有一种方法可以使用defaultTasks关键字定义默认任务,如果用户在构建文件中没有提到要执行的具体任务,则默认执行。我们将在第三章管理任务中进一步讨论这一点。

Gradle 命令初始化脚本,读取命令行上提到的所有任务,并执行任务。此外,如果任何任务有多个依赖项,则依赖任务将按字母顺序执行,除非这些任务本身强制执行顺序。您可以在第三章管理任务中找到更多关于任务排序的信息。

请记住,每个 Gradle 构建都包含三个组件:项目、任务和属性。每个构建至少有一个项目和一个或多个任务。项目的名称是构建文件存在的父目录名称。

Gradle 命令行参数

现在您已经创建了第一个可工作的脚本,是时候探索 Gradle 支持的不同命令行选项了。

您已经看到了使用-b选项来指定构建脚本的使用方法。我们将从--help-h-?开始,列出 Gradle 命令行中所有可用的选项。

$ gradle -h
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.
-c, --settings-file   Specifies the settings file.
--configure-on-demand   Only relevant projects are configured in this build run. This means faster build for large multi-project builds. [incubating]
--continue            Continues task execution after a task failure.

在前面的输出中,-h--help显示了许多更多选项。我们已经截断了输出。

您可以在您的系统上执行该命令并检查所有选项。其中大部分都是自我解释的。在本节中,我们将讨论一些最有用选项的用法。

现在我们将在build.gradle脚本中添加两个更多任务,failedTasktest,并将文件保存为sample_build.gradle。名为failedTask的任务预期将始终因断言失败而失败,而test任务依赖于之前创建的任务helloGradle。任务可以成功(在任务中执行所有语句而没有任何异常)或失败(由于任何异常或错误,这些异常或错误在任务的任何一行代码中提到),从而停止脚本的执行。

task failedTask << {
      assert 1==2
}

task test(dependsOn: helloGradle ) << {
      println 'Test case executed'
}

在执行gradle -b sample_build.gradle failedTask test命令时,我们观察到test任务从未被执行。由于 Gradle 按命令行中出现的顺序顺序执行任务,如果任务执行失败,则忽略所有剩余的任务。

$ gradle -b sample_build.gradle failedTask test
:failedTask FAILED
FAILURE: Build failed with an exception.
…
BUILD FAILED

Total time: 6.197 secs

默认情况下,Gradle 如果任何任务执行失败,则会停止构建过程。这个特性有助于快速获得构建过程的反馈。如果您不想因为任何任务的失败而停止构建的执行,并且希望继续执行其他任务,则可以通过使用 --continue 命令行选项来实现。当我们要构建一个多模块项目,其中一些模块可能由于编译错误或测试失败而失败时,这个特性可能很有用。使用 –continue 选项,我们将获得所有模块的完整状态。

$ gradle -b sample_build.gradle failedTask test --continue
:failedTask FAILED
:helloGradle
Hello Gradle- This is your first script
:test
Test case executed

FAILURE: Build failed with an exception.

正如您在前面的输出中看到的,failedTask 任务执行失败。因此,构建被标记为 FAILURE。然而,这次 test 任务执行成功。同时注意,helloGradle 任务在 test 任务之前执行。这是因为我们已将 test 任务定义为依赖于 helloGradle 任务。这是创建任务依赖关系的一种方法。现在,请不要对任务依赖关系感到困惑。我们将在 第三章 管理任务 中详细讨论这个主题。

现在,如果 helloGradle 任务失败会发生什么?只需在 helloGradle 任务中添加一行 assert 1==2。断言语句强制任务失败。当您查看以下输出时,您会发现测试任务没有执行,因为依赖的任务失败了:

$ gradle -b sample_build.gradle failedTask test --continue
:failedTask FAILED
:helloGradle
Hello Gradle- This is your first script
:helloGradle FAILED

FAILURE: Build completed with 2 failures.

在前面的场景中,测试任务依赖于 helloGradle 任务。这意味着每次我们执行 test 任务时,helloGradle 任务都会默认执行。如果您想避免执行 helloGradle 任务,可以使用 -x or --exclude-task 选项。

$ gradle -b sample_build.gradle failedTask --continue test -x helloGradle
:failedTask FAILED
:test
Test case executed

另一个有用的选项是 --dry-run-m,它运行构建但不执行任务。如果您想了解任务执行顺序或验证脚本,这很有用。

$ gradle --dry-run -b sample_build.gradle failedTask test --continue
:failedTask SKIPPED
:helloGradle SKIPPED
:test SKIPPED
BUILD SUCCESSFUL
Total time: 4.047 secs

注意

--dry-run 执行不属于任何任务且定义在任务块之外的语句。为了验证这一点,在任务块定义之外添加一个 println 语句并观察结果。

到目前为止,您可能已经注意到每个输出都显示了除任务输出和错误消息之外的信息。尝试使用命令行选项 -q--quiet 仅显示任务输出:

$ gradle -q -b sample_build.gradle failedTask --continue test
Hello Gradle- This is your first script
Test case executed

选项 --debug (-d)、--info (-i)、--full-stacktrace (-S) 和 --stacktrace (-s) 以不同的日志级别和堆栈跟踪显示输出。--debug 是最详细的日志级别。--full-stacktrace--stacktrace 如果构建因异常失败时,会显示堆栈跟踪。尝试使用这些命令行选项执行之前执行的命令,并观察输出:

$ gradle -d -b sample_build.gradle failedTask --continue test

现在我们将探索 --daemon--stop--no-daemon 选项。在我的机器上,执行前面的脚本大约需要 3.6 秒。对于这个简单的脚本,大部分执行时间都花在了 Gradle 的初始化上。当我们执行 Gradle 命令时,会启动一个新的 Java 虚拟机,然后加载 Gradle 特定的类和库,最后执行实际的构建步骤。可以使用 --daemon 选项来改进 Gradle 的初始化和执行。如果你在进行测试驱动开发,需要频繁执行单元测试或者需要重复运行特定任务,这将非常有用。

要启动守护进程,可以使用 --daemon 选项。守护进程在空闲 3 小时后会自动过期。要检查系统上是否正在运行守护进程,在 UNIX 环境中使用 ps 命令,或在 Windows 系统中使用进程资源管理器。一旦启动了守护进程,再次执行相同的 Gradle 任务。你会发现执行时间有所改善。

或者,你可以使用 gradle.properties 文件来设置系统属性 org.gradle.daemon 以启用守护进程。在这种情况下,执行任务时不需要指定 --daemon 选项。要尝试一下,在创建 sample_build.gradle 文件相同的目录下创建一个名为 gradle.properties 的文件,并添加这一行 org.gradle.daemon=true。现在,运行 gradle 命令并检查守护进程是否正在运行。org.gradle.daemon 是我们设置的属性,用于配置 Gradle 构建环境。我们将在 第六章 中更多地讨论属性和系统变量,即 使用 Gradle

要停止守护进程,请使用 gradle --stop 选项。有时,你可能不想使用守护进程执行 Gradle 任务。使用 --no-daemon 选项与任务一起,忽略任何正在运行的守护进程。

$ gradle -b sample_build.gradle failedtask --continue test 
--daemon

$ ps -ef | grep gradle
root   25395  2596 46 18:57 pts/1  00:00:04 
/usr/local/java/jdk1.7.0_71/bin/java ….. 
org.gradle.launcher.daemon.bootstrap.GradleDaemon 2.4 
/home/root/.gradle/daemon 10800000 93dc0fe2-4bc1-4429-a8e3-
f10b8a7291eb -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -
Xmx1024m -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en -
Duser.variant

$ gradle --stop
Stopping daemon(s).
Gradle daemon stopped.

虽然推荐在开发环境中使用 Gradle 守护进程,但它偶尔可能会损坏。当 Gradle 从多个来源执行用户构建脚本(例如,在持续集成环境中)时,可能会耗尽守护进程,如果资源管理不当,可能会导致内存泄漏。因此,建议不要在预发布或持续集成环境中启用守护进程。除了命令行之外,Gradle 还可以在 图形用户界面GUI)中执行。在下一节中,我们将讨论 Gradle 支持的图形用户界面。其他重要的命令行选项,如 –D--system-prop-P--project-prop,将在 第六章 中讨论,即 使用 Gradle,当我们更深入地探讨使用 Gradle 构建 Java 应用程序时。

Gradle 图形用户界面

除了命令行参数和工具之外,Gradle 还提供了一个图形用户界面。您可以通过以下命令行选项启动它:

$ gradle --gui

它启动了一个图形用户界面GUI),可以直接从 GUI 中执行 Gradle 任务。

Gradle GUI

图 1.1

它包含四个选项卡,以下是对它们的解释:

  • 任务树:您执行此命令的目录被视为父项目目录。如果此目录下有build.gradle文件,任务树将列出build.gradle文件中所有可用的任务。如果没有build.gradle文件在此目录下,它将只列出默认任务。您可以通过双击任务名称来执行任何任务。

    图 1.1显示了我们在早期开发的failedTaskhelloGradletest任务,以及默认的 Gradle 任务。

  • 收藏夹:这类似于您的浏览器收藏夹,您可以在此处保存常用命令。此外,它还提供别名功能。如果您想在命令行上执行多个任务,您可以将其添加到此,并给它一个简单的显示名称。例如,您可以点击加号,在命令行文本框中添加以下任务:clean build

    在显示名称区域添加init。您会看到init出现在收藏夹区域。下次,只需点击init即可执行clean build任务。

  • 命令行:这类似于控制台。在这里,您可以执行单个或多个内联命令。它将执行命令,并在下方的窗口中显示结果。

  • 设置:即使您从特定的项目目录启动了 GUI,您也可以使用此选项卡更改目录。它允许您更改当前目录以执行命令。此外,它有助于更改一些通用设置,例如日志级别、堆栈跟踪输出等。它还允许您通过自定义 Gradle 执行器执行其他 Gradle 版本。

启动脚本

考虑以下场景,对于您的每个 Gradle 项目,您都依赖于本地的内部 jar 文件。此外,您还希望为每个 Gradle 项目设置一些常见的环境变量(如GRADLE_OPTS)。

一个简单的解决方案是将 jar 文件添加到依赖项闭包中。另一个解决方案可以是创建一个通用的构建文件,并将其包含在每个构建文件中。

Gradle 通过引入初始化脚本为这类问题提供了最简单的解决方案。

初始化脚本并不是特殊的文件,而是一个具有.gradle扩展名的 Gradle 脚本。然而,这将在执行任何构建文件之前执行。

注意

可能会有多个初始化脚本。

初始化脚本的一些用途如下:

  • 为您的每个项目下载一些常见的 jar 文件

  • 执行与系统细节和/或用户细节相关的常见环境配置。

  • 注册监听器和记录器。

那么,Gradle 是如何找到这些初始化脚本的呢?定义初始化脚本有多种方式,如下所示:

  • <USER_HOME>/.gradle/init.d目录下所有扩展名为.gradle的文件被视为初始化脚本。Gradle 会在执行任何 Gradle 构建脚本之前,执行此目录下的所有.gradle文件。

  • <USER_HOME>/.gradle/目录下名为init.gradle的文件被视为初始化脚本。

  • <GRADLE_HOME>/init.d/目录下所有扩展名为.gradle的文件。

  • 您甚至可以使用-I <file name>--init-script <file name>指定任何 Gradle 文件作为初始化脚本。

    注意

    即使在前面提到的位置找到多个文件,Gradle 也会在执行任何项目构建脚本之前,将这些文件作为初始化脚本执行。

下面是一个示例init脚本。

println "Hello from init script"
projectsLoaded {
  rootProject.allprojects {
    buildscript {
      repositories {
        maven {
          url "http://central.maven.org/maven2/"
        }
      }
      dependencies {
        classpath group: 'javax.mail', name: 'javax.mail-api', 
          version: '1.4.5'
      }
    }
  }
}

将前面的代码复制并粘贴,保存为init.gradle文件,位于前面提到的任何路径下。println语句有意添加到该文件中,以帮助您理解init脚本的执行周期。每次您从目录中执行任何 Gradle 脚本时,您都会看到Hello from init script。除了打印Hello from init script之外,当脚本首次执行时,此脚本还会在 Gradle 缓存中下载javax.mail-api-1.4.5.jar。除非存储库中的文件发生变化,否则它不会再次下载此库。如果您不了解缓存是什么,请不要担心。您将在本章后面的部分学习更多关于缓存管理的内容。记住,有时在初始化脚本中定义过多的配置可能会出现问题。特别是,调试可能会很困难,因为项目不再自包含。

构建生命周期

Gradle 构建有一个生命周期,包括三个阶段:初始化、配置和执行。理解构建生命周期和执行阶段对于 Gradle 开发者至关重要。Gradle 构建主要是任务集合,用户可以定义任务之间的依赖关系。因此,即使两个任务依赖于同一个任务,例如,任务 C 和任务 B 都依赖于任务 A,Gradle 也会确保任务 A 在整个构建脚本执行过程中只执行一次。

在执行任何任务之前,Gradle 为构建准备了一个所有任务的有向无环图DAG)。它是定向的,因为一个任务直接依赖于另一个任务。它是无环的,因为如果任务 A 依赖于任务 B,而您让任务 B 依赖于任务 A,这将导致错误,因为两个任务之间不能有循环依赖。在执行构建脚本之前,Gradle 配置任务依赖图。

让我们快速讨论三个构建阶段。

初始化

用户可以为单个项目以及多项目构建创建构建脚本。在初始化阶段,Gradle 确定哪些项目将参与构建过程,并为这些项目中的每个项目创建一个项目实例。

配置

这个阶段配置项目对象。所有构建脚本(如果用户正在执行多项目构建),作为构建过程的一部分,都会被执行,而不会执行任何任务。这意味着你在配置块中编写的所有不在任务之外的语句都会在配置阶段执行。这里不会执行任何任务;只会为所有任务创建一个有向无环图。

执行

在这个阶段,Gradle 按照命令行中给出的顺序执行所有任务。然而,如果任务之间存在依赖关系,这些关系将首先得到尊重,然后再按照命令行顺序执行。

缓存管理

任何构建工具的主要重点是不仅自动化构建和部署过程,还要有效地管理缓存。没有软件是孤立工作的。每个软件都依赖于某些第三方库和/或内部库。

任何好的构建工具都应该自动处理软件依赖。它应该能够自动下载依赖项并维护版本。当 Ant 发布时,这个功能是不可用的,开发者需要手动下载依赖项并自己维护其版本。尽管后来通过扩展 Ant 以 Ivy 解决了这个问题。

Gradle 会自动下载构建文件中给出的所有依赖项。它确定项目所需的所有库,从存储库中下载,并将其存储在其本地缓存中。下次您运行构建时,它不需要再次下载这些依赖项(除非需要),因为它可以重用缓存中的库。它还会下载所有传递依赖项。

小贴士

下载示例代码

您可以从www.packtpub.com下载您购买的所有 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

缓存位置

关于缓存,首先出现的问题是 Gradle 在哪个位置维护其缓存。Gradle 使用 <USER_HOME>/.gradle/caches 作为默认目录来存储其本地缓存。如果开发者使用多个版本的 Gradle 来构建软件,则可能包含多个版本目录。实际的缓存分为两部分。所有从仓库下载的 JAR 文件都可以在 modules-2/files-2.1 下找到。此外,你还会找到一些存储已下载二进制文件元数据的二进制文件。如果你查看 modules-2/files-2.1 目录,它具有 group/name/version/checksum 格式的路径,其中包含实际的二进制文件。你可以在 第五章 依赖项管理 中详细了解依赖项管理。

更改缓存位置

如果你想要将缓存位置更改为除默认位置之外的目录,你需要设置以下环境变量。你可以在 Windows 中将其作为环境变量设置,在 Unix/Linux 中在 .profile 文件中设置:

GRADLE_USER_HOME=<User defined location>

缓存功能

现在,让我们讨论 Gradle 缓存的一些重要功能。

减少流量

Gradle 缓存管理的主要功能之一是减少网络流量。当你第一次构建应用程序时,Gradle 将所有依赖项下载到缓存中,这样下次就可以直接从缓存中获取。

如果在构建脚本中配置了多个仓库,并且在一个仓库中找到了 JAR 文件,那么 Gradle 不会在其他仓库中搜索相同的 JAR 文件。在另一种情况下,如果 JAR 文件在第一个仓库中没有找到,但已从第二个仓库中获取,那么 Gradle 将存储关于第一个仓库的元数据信息,这样下次就不会再搜索缺失的 JAR 文件,以节省时间和网络流量。

依赖项位置

每当 Gradle 从仓库下载依赖项时,它也会将其元数据中的仓库位置存储起来。这有助于检测变化,以防二进制文件从仓库中删除或其结构发生变化。

版本集成

如果开发者更新了他机器上的 Gradle 版本,并且他已经下载了旧缓存中的库,那么这些库将被重用。Gradle 还提供了与 Maven 本地仓库的紧密集成。Gradle 通过比较其校验和与本地缓存来确定远程仓库中的工件是否已更改。所有校验和匹配的工件都不会下载。除了校验和之外,Gradle 还会考虑一个额外的参数来比较远程和本地工件;Gradle 使用 HTTP 头参数 content-length 或最后修改日期的值。

关闭远程检查

使用 --offline 命令行选项,开发者可以要求 Gradle 只查看本地缓存,而不是远程缓存。如果用户在没有网络连接的情况下工作,这可能很有用。如果 Gradle 在本地缓存中找不到 JAR 文件,构建将失败。

版本冲突

如果开发者没有指定任何特定的依赖版本,并且有多个版本可供下载,那么默认情况下,Gradle 总是下载最新的工件版本。

Gradle 与 IDE

到目前为止,在本章中,我们已经创建了一些基本的 Gradle 脚本。我们将通过创建一个使用 Gradle 的 Java 应用程序来结束本章。为了创建 Java 应用程序,我们将使用带有 Gradle 插件的 Eclipse IDE。

使用 集成开发环境IDE),应用程序开发变得容易得多。在本节中,我们将探讨如何在 Eclipse 中安装 Gradle 插件、创建简单的 Java 应用程序、探索 Eclipse 插件任务以及从 Eclipse 执行 Gradle 任务。

除了 Eclipse,另一个流行的 IDE 是 JetBrains IntelliJ IDEA。Gradle 也支持 IDEA 插件,它与 Eclipse 插件非常相似。然而,在本书中,我们将只关注 Eclipse 插件,因为它免费提供且是开源的。

在 Eclipse 中安装 Gradle 插件

来自 Spring Source 的 Eclipse Integration Gradle 项目([github.com/spring-projects/eclipse-integration-gradle/](https://github.com/spring-projects/eclipse-integration-gradle/))帮助开发者使用 Eclipse 中的 Gradle。此工具提供以下支持:

  • 使用多项目

  • 使用 Gradle Import Wizard 将 Gradle 项目导入到 Eclipse

  • 使用 New Gradle Project Wizard 创建新的 Gradle 项目

  • 使用依赖管理来配置 Eclipse 项目的类路径

  • 使用 Gradle Task UI 执行 Gradle 任务

  • 通过 DSLD(DSL 描述符)与 Groovy Eclipse 集成

以下是在 Eclipse(3.7.2 或更高版本)更新站点中安装此插件的操作步骤:

  1. 启动 Eclipse。导航到 帮助 | 安装新软件

  2. 安装新软件 对话框中,点击 添加 按钮以添加新站点。

  3. 位置 输入为 http://dist.springsource.com/release/TOOLS/gradle,将 名称 输入为 Gradle。您可以输入任何有意义的名称。

  4. 点击 确定 添加仓库。

  5. 从仓库列表中选择新创建的 Gradle 仓库。

  6. 仅勾选 扩展/Gradle 集成 | Gradle IDE 旁边的框。点击 下一步(参见图 1.2)。

  7. 在下一屏幕上,点击 下一步

  8. 接受条款和条件并点击 完成。Eclipse 应该下载并安装 Gradle IDE。然后重新启动 Eclipse。在 Eclipse 中安装 Gradle 插件

    图 1.2

在 IDE 中使用 Gradle 项目

我们已成功安装 Gradle 插件。现在,我们将创建一个简单的 Gradle 项目,并查看一些与 Eclipse 相关的重要文件,例如,.project.classpath。然后我们将使用 Gradle 任务 UI 构建项目。

以下是创建 Gradle 项目的步骤:

  1. 在 Eclipse 中,导航到 文件 | 新建 | Gradle | Gradle 项目

  2. 新 Gradle 项目 窗口中,将项目名称指定为 FirstGradleProject,并选择样本项目为 Java Quickstart

  3. 点击 完成 并等待构建成功。

你将找到以下控制台输出:

:cleanEclipseClasspath UP-TO-DATE
:cleanEclipseJdt UP-TO-DATE
:cleanEclipseProject UP-TO-DATE
:cleanEclipse UP-TO-DATE
:eclipseClasspath
…
:eclipseJdt
:eclipseProject
:eclipse

BUILD SUCCESSFUL

输出清楚地显示了这里正在发生的事情。Gradle 首先执行一系列清理任务(cleanEclipseClasspathcleanEclipse 等),然后从 Maven 仓库下载一些 jar 文件,最后执行更多任务(eclipseJdteclipse 等)以完成构建过程。

自动生成的 build.gradle 文件包含以下内容:

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.5
version = '1.0'
jar {
  manifest {
    attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
  }
}

repositories {
  mavenCentral()
}

dependencies {
  compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
  testCompile group: 'junit', name: 'junit', version: '4.+'
}

test {
  systemProperties 'property': 'value'
}

uploadArchives {
  repositories {
    flatDir {
      dirs 'repos'
    }
  }
}

这个 build 文件与我们在本章前面创建的文件相当不同。在开头添加了 Java 和 Eclipse 插件声明。添加了项目属性,如 sourceCompatibility 和版本。仓库声明为 mavenCentral()。在 compiletestCompile 上配置了依赖项、common-collections 和 JUnit。我们将在下一章中学习每个组件;现在,让我们专注于 Gradle 项目创建的其他工件。

如果你浏览项目的源代码(查找 src 文件夹),你会发现应用程序已经预填充了一些 Java 源代码和 JUnit 测试用例。

除了源代码和构建文件外,还添加了一些其他文件,即 .project.classpath 和一个文件夹,即 .settings。这些都是 Eclipse 默认创建的文件。正如其名所示,.project 文件包含有关项目的元数据信息,如名称、描述和构建规范。.classpath 文件描述了 Java 依赖项、外部库依赖项和其他项目依赖项。.settings/org.eclipse.jdt.core.prefs 存储有关 Java 编译器版本、源和目标 Java 版本的信息。所有这三个文件都是在执行 eclipse 任务时的构建过程中创建的。

因此,我们声称 Eclipse 插件负责创建所有 Eclipse IDE 特定的文件。为了确认,首先从基本文件夹的项目中执行 gradle cleanEclipse 命令:

$ gradle cleanEclipse
:cleanEclipseClasspath
:cleanEclipseJdt
:cleanEclipseProject
:cleanEclipse

BUILD SUCCESSFUL

cleanEclipse 任务执行了三个额外的依赖任务:cleanEclipseClasspath(删除 .classpath 文件)、cleanEclipseJdt(删除 .settings/org.eclipse.jdt.core.prefs 文件)和 cleanEclipseProject(删除 .project 文件)。

检查这三个文件是否已从项目中删除,最后执行 gradle eclipse 命令以重新创建这些文件。

$ gradle eclipse
:eclipseClasspath
:eclipseJdt
:eclipseProject
:eclipse

BUILD SUCCESSFUL

现在的问题是,如果我有一个 Java 项目,我如何在 Eclipse IDE 中导入该项目?

我们已经学过这个了,你可能已经猜到了。只需要三个步骤:将 Eclipse 插件添加到构建文件中(应用 eclipse 插件),执行 Eclipse 任务(gradle eclipse),最后使用 Eclipse 文件 | 导入 来导入项目。

或者,您可以使用 Gradle IDE。从 Eclipse 中,通过导航到 文件 | 导入 | Gradle | Gradle 项目 来选择项目,然后执行 构建模型 并完成。使用 Gradle IDE 可以帮助避免之前提到的所有手动步骤。

我们将通过探索 Gradle 任务 UI 来结束本节,它使我们能够执行任务。Gradle 任务执行由标准的 Eclipse 启动框架支持。这意味着在执行任何任务之前,我们必须创建一个标准的 Eclipse 启动配置。要创建启动配置,请导航到 Gradle 项目 | 运行方式 | 并点击 Gradle 构建

在文本区域中,输入您想要执行的任务名称,例如 clean build。然后点击 运行 来执行任务。默认情况下,启动配置将保存为项目名称。在 图 1.3 中,配置保存为 FirstGradleProject,这是项目名称。

在 IDE 中使用 Gradle 项目

图 1.3

这个启动配置将被保存在 Eclipse 中,以便可以再次执行。要启动之前保存的配置 FirstGradleProject,您需要导航到 运行方式 | Gradle 构建。这将再次执行 clean build 命令。

概述

在本章中,我们简要讨论了什么是构建自动化系统,为什么我们需要它,以及为什么 Gradle 是一个流行的构建自动化系统。您还学习了如何安装 Gradle,并创建了我们的第一个 Gradle 脚本。然后我们讨论了命令行选项、GUI 支持、缓存管理和启动脚本。最后,我们使用带有 Gradle 插件的 Eclipse IDE 开发了一个简单的 Java 应用程序。

本章中开发的全部构建脚本都是用 Groovy 编写的,但我们还没有讨论它。因此,在下一章中,我们将学习 Groovy 编程语言的一些基本概念。下一章主要面向已经具备一些 Java 和面向对象编程概念基本知识的开发者。

第二章:Gradle 的 Groovy 基础知识

在本章中,我们将学习 Groovy 编程语言的一些基本概念。本章简要介绍了 Groovy 数据类型、控制结构、面向对象概念、集合、闭包和构建器。这只是冰山一角。由于这不是一本 Groovy 书籍,我们无法涵盖所有主题。本章旨在为那些来自 Java 背景并基本了解面向对象编程OOP)概念的初学者提供帮助。这将帮助他们开始使用 Groovy。本章还将作为继续 Gradle 脚本编写的一个工具箱。

概述

Groovy 是 Java 平台上的动态编程语言。你可能想知道为什么我们特别提到 Java 平台。通过 Java 平台,我们的意思是 Groovy 代码编译成字节码,字节码在 JVM 上执行,类似于任何其他 Java 类。除了面向对象(OOP)特性外,它还提供了脚本语言(如 Python 和 Smalltalk)的能力,使得它们可以通过类似 Java 的语法在 Groovy 中使用。

由于 Groovy 运行在 JVM 上,它可以很容易地与 Java 集成,并且很好地融入现有的基础设施中。例如,Groovy 代码的构建和部署与 Java 代码的构建和部署相同,你只需向库中添加另一个 JAR 文件就可以轻松地将 Groovy 和 Java 混合在一起。Groovy 不是唯一运行在 JVM 上的语言。其他一些语言包括 Scala、Clojure、JRuby、Jython 等等。在我看来,如果你有一些 Java 背景,与其它语言相比,Groovy 的学习要容易得多。它具有非常类似 Java 的语法,并且大多数 Java 语法都是有效的 Groovy 语法。它只是简化了编码。Groovy 从未打算取代 Java。它的目的是补充 Java,扩展它使其更容易使用,并且它还使用了现代语言特性,如闭包、构建器和元编程。

以下是一些 Groovy 的关键特性。

与 Java 的集成

许多人认为 Groovy 是一种脚本语言。是的,它提供了脚本支持,但说 Groovy 只是一种脚本语言是不正确的。除了脚本之外,它完美地融入了面向对象的世界。如前所述,Groovy 还提供了与 Java 的无缝集成。从 Groovy 调用 Java 就像编写 Groovy 代码一样简单。每个 Groovy 类型都是java.lang.Object的子类型。

最小化代码

Groovy 的一个优点是它减少了执行一些复杂任务(如解析 XML 文件和访问数据库)所需的代码量。使用 Groovy,你总是可以混合 Java 代码。如果你已经使用 Java 一段时间了,我认为你会欣赏使用 Groovy 的简单性,因为你可以通过编写更少的代码来实现更多的功能。

简化的 I/O 操作

在使用 Java 进行开发时,I/O 操作是开发者面临的主要痛点之一,在 Groovy 中变得简单得多。在 Groovy 中执行 I/O 操作更有趣。Groovy 提供了简单的属性来读取/写入文件。它已经向java.io.File类添加了如此多的实用方法。

与 Ant 的集成

与 Java 一样,Groovy 提供了与 Ant 的无缝集成。Groovy 有一个辅助类AntBuilder,它赋予了 Groovy 使用 Ant 功能的能力,使开发者的生活变得更加简单。无论是计算任何文件的校验和,还是根据任何过滤条件将目录内容从一个位置复制到另一个位置。借助 Ant 功能,Groovy 使开发者更加高效。在第八章“迁移”中,我们将更详细地讨论这个话题。

构建器类

除了 AntBuilder 之外,Groovy 还提供了NodeBuilderMarkupBuilderSwingBuilder的能力。借助这些构建器,开发者能够以更简单的方式完成任务,与没有构建器的生活相比。MarkupBuilder在处理 XML 操作时很有用。SwingBuilder提供了简化了 Swing 框架的 API,有助于构建用户友好的 GUI 应用程序。NodeBuilder在处理对象树结构时很有帮助。

闭包

闭包的引入是 Groovy 的一个大卖点。Groovy 中的闭包是一个匿名代码块,它可以接受参数,返回值,并引用和使用其周围作用域中声明的变量。闭包通常与函数式语言相关联。

Groovy 闭包就像是一个定义后并在稍后执行的代码块。它具有一些特殊属性,例如隐式变量和自由变量。我们将在本章后面的部分详细讨论闭包。

当然,还有很多其他特性需要学习。我们将在本章中讨论其中的一些。更多详细信息,请参阅www.groovy-lang.org/的 Groovy 文档。

Groovy 脚本中的 Hello World

我们已经讨论了 Groovy 是什么以及它的一些重要特性。让我们创建一个 Hello World 程序,并用 Groovy 感受它的魔力。这里我们假设 Groovy 已安装在系统上,GROOVY_HOME指向安装目录,并且<GROOVY_HOME>/bin已添加到 PATH 环境变量中:

file: GroovyTest.groovy
  println "Hello Groovy"

就这些了。是的,对于一个简单的 Groovy 程序,你不需要声明任何包,任何主类,或者任何分号,只需一个简单的println语句就能创建你的第一个 Groovy 程序。

要执行程序,请使用以下命令:

$ groovy GroovyTest.groovy
 Hello Groovy

使用groovy命令来执行 Groovy 脚本。Groovy 脚本的美妙之处在于它可以执行任何文件,而不仅仅是扩展名为.groovy的文件。甚至你可以在Test.text文件中编写前面的println语句,并使用 groovy 命令来执行该文件。在 Groovy 中,文件扩展名并不重要,但为了使文件结构更易于阅读,建议为 Groovy 文件使用.groovy扩展名。

执行 Groovy 文件还有另一种方法。你可以编译 Groovy 文件,生成类似于 Java 的类文件,然后执行这些类文件。请执行以下步骤:

  1. 要编译并生成类文件,请使用以下命令:

    $ groovyc GroovyTest.groovy
    
    
  2. 要在 Windows 上运行生成的类文件,你需要执行以下命令。如果在 Linux/Unix 环境中执行,请使用$GROOVY_HOME

    $ java -cp %GROOVY_HOME%/embeddable/groovy-all-2.3.1.jar;. GroovyTest
    
    

执行编译后的 Groovy 文件与执行 Java 文件相同。开发者需要在类路径中添加groovy-all-<version>.jar。你还需要确保你的编译类所在的目录在类路径中。在先前的示例中,我们将当前目录"."添加到类路径中,以查找GroovyTest.class文件。

执行 Groovy 脚本的方式并不重要。在这两种情况下,Groovy 脚本都在 JVM 内部执行。这两种方法都将 Groovy 脚本编译成字节码。groovy <filename>命令直接将类存储在内存中,而使用groovyc命令编译脚本会创建一个类文件并将其存储在磁盘上,你可以稍后使用 Java 命令执行它。

数据类型

任何编程语言的第一件事就是了解数据类型;任何编程语言如何存储数据。与其它编程语言类似,Groovy 也提供了一套不同的数据类型,用于数值、字符串、字符等。与 Java 相比,Groovy 中没有原始数据类型。Groovy 将一切视为对象,这使得 Groovy 成为一门纯粹的面向对象语言。原始数据类型的问题在于,开发者无法对它们执行任何对象级别的操作,例如调用它们的方法。此外,你也不能将它们作为对象存储在映射和集合(需要对象的集合)中。以下表格显示了原始数据类型和包装类型及其默认值:

数据类型 包装类型 默认值
byte Byte 0
短名 简称 0
int Integer 0
long Long 0L
float Float 0.0f
double Double 0.0d
char Character \u0000
boolean Boolean false
String 不适用 null

Groovy 允许你甚至使用 int、byte、short 等声明变量,它内部将它们转换为相应的类,例如,int 转换为 Integer,char 转换为 Character,等等。

字符串

你可能想知道,为什么我们只在这里讨论字符串?这是因为与 Java 相比,Groovy 提供了不同的变体来表示字符串,如下面的代码所示:

def s1='This is single quote string.'

def s2="This is double quote string."

def s3="""This is multi line String.
You can write multiple lines here."""

def s4 ="Example of Gstring, You can refer to variable also like ${s1}"

def s5='''This is multi line String.
You can write multiple lines here.'''

def s6 =/ This is 'slashy' String.
It can also contains multiple lines $s1
/

在这里,s1包含单引号中的字符串。这个字符串的大小是固定的,因为我们这样写它。

s2变量包含与 Java 字符串类似的被双引号包围的字符串。

变量s3包含被三个双引号包围的字符串,这允许你声明一个多行字符串。

s4中,字符串包含一个嵌入的变量,它将被解析为其值。这正式称为 GString。你可以使用${variable}$variable来声明占位符。

Groovy 还支持另一种格式,即字符串在/(斜杠)内声明。它也支持多行字符串。

Groovy 中的动态类型

Groovy 提供了对静态类型和动态类型特性的支持。静态类型在编译时提供更多的检查,更多的内存优化,以及更好的对 Groovy 使用的 IDE 的支持。它还提供了关于变量或方法参数类型的额外信息。然而,Groovy 的力量在于动态类型。在许多场景中,你不确定变量将存储什么类型的值或函数将返回什么类型的值。在这种情况下,Groovy 提供了使用动态类型的使用灵活性。你可以使用def关键字定义变量或方法,如下面的代码所示:

def var1
var1 ='a'     
println var1.class   // will print class java.lang.String
var1 = 1       
println var1.class   // will print class java.lang.Integer
def method1() {/*method body*/}

动态类型的另一个用途是在没有保证类型的对象上调用方法。这通常被称为鸭子类型。例如,考虑以下场景,在整数、列表和字符串等不同数据类型上调用简单的加法方法。根据不同的输入参数,每次方法返回不同的输出。

def addition(a, b) { return a + b}
addition (1, 2)      // Output: 3
addition ([1,2], [4, 5])  // Output: [1, 2, 4, 5]
addition('Hi ', 3)    // Output: Hi 3

如你所见,当加法方法以整数作为参数被调用时,它执行了算术加法。对于列表类型的参数,加法方法通过合并两个列表创建一个新的列表。同样,对于字符串类型的参数,它执行简单的连接。在这个例子中,+运算符根据输入类型参数被解释为不同的方法调用。

提示

Java 和 Groovy 之间一个主要的不同点在于 Groovy 支持操作符重载。

到目前为止一切顺利。但如果在用户定义的对象,比如Person上调用加法方法会怎样呢?这将在下面的代码中展示:

class Person{
  String name

  @Override
  public String toString() {
    return "Person [name=" + name +"]";
  }
}

p1 = new Person()
p2 = new Person()
addition(p1, p2)  // Output: groovy.lang.MissingMethodException

这是预期的,因为在Person类中我们没有定义加法方法。如果我们定义Person类中的加法方法,对加法(调用p1 + p2p1.plus(p2))方法的调用将会成功。

另一种解决方案是实现methodMissing方法。这是 Groovy 中的一个非常强大的概念。在 Gradle 源代码中,你会多次找到对这个方法的引用。

因此,我们不需要定义一个加法方法,我们可以定义一个methodMissing方法,如下所示:

def methodMissing(String name, args) {
  if (name.startsWith("plus") ) {
// write your own implementation
    return "plus method intercepted"
  }
  else {
    println "Method name does not start with plus"
    throw new MissingMethodException(name, this.class, args)
  }
}

现在,如果我们调用Person对象的加法方法,我们会发现输出为plus method intercepted,如下面的代码所示:

addition(p1, p2)  // Output: plus method intercepted

类、bean 和方法

本节介绍了类、方法和 bean。Groovy 类与使用class关键字声明的 Java 类类似。通常,类定义从包名开始,然后是导入包语句。与 Java 对应物的一个关键区别是,Groovy 默认导入六个包和两个类。所以,如果你创建任何类,这些包和类将自动可用:

import java.lang.* // this is the only default import in Java
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal

Groovy 中的类和方法默认具有公共访问权限,而在 Java 中它被设置为package-private。我们将从一个示例 Groovy 类开始:

class Order {
  int orderNo
  Customer orderedByCustomer
  String description

  static main(args) {
    Order order1 = new Order();
    order1.orderNo = 1;
    order1.orderedByCustomer = new Customer(name: "Customer1", email: "cust1@example.com")
    order1.setDescription("Ordered by Customer1")
    println order1.orderByCustomer.showMail()
  }
}

class Customer{
  String name
  String email
  String address

  String showMail(){
  email
 }
}

在这里,我们创建了两个类,OrderCustomer,并包含了一些字段;在main方法中,我们创建了对象,最后在Customer对象上调用showMail()方法。注意对象是如何用值初始化的。Order对象是通过默认构造函数创建的,然后对象通过在字段上定义的setter方法进行初始化。

然而,对于Customer对象,它是通过具有命名参数的构造函数完成的。Customer对象通过构造函数中的属性值对进行初始化。然而,我们还没有在类定义中定义任何参数化构造函数。那么它是如何工作的呢?

我们在类中创建了没有访问修饰符的字段。如果字段使用默认访问权限创建,那么 Groovy 会自动创建一个具有公共gettersetter方法的字段。如果我们指定了任何访问修饰符(publicprivateprotected),则只会创建字段;不会创建gettersetter方法。在我们前面的例子中,orderNoorderByCustomerdescription字段没有使用访问修饰符声明。因此,我们能够调用Order对象的setDescription方法。其他字段通过字段名进行访问。在这种情况下,Groovy 会在字段上内部调用相应的gettersetter方法。这个特性在 Groovy 中被称为属性。因此,Groovy 中的每个类都有属性,并为这些属性自动创建gettersetter方法。这与 Java bean 方法类似,其中私有字段通过公共gettersetter方法创建,但代码行数更少,因为gettersetter方法是由 Groovy 隐式提供的。这就是为什么 Groovy 对象通常被称为纯旧 Groovy 对象POGO)。

回到构造函数声明,当使用命名参数创建Customer对象时,实际上创建了一个默认构造函数,然后,对于构造函数中的每个属性,分别调用相应的setter方法来初始化属性。

Groovy 中的方法与 Java 类似,但类方法的可见性默认设置为 public。要调用类上的方法,我们需要创建该类的对象。在 Groovy 脚本中,如果您没有提供任何类定义,方法调用将通过按名称调用方法来完成。如果方法支持动态返回类型,则方法声明应从 def 关键字开始。

Groovy 还支持具有默认参数值的方法调用。在以下示例中,sum 方法被定义为具有三个参数 xyz,其中 yz 的值分别为 101sum(1)sum(1,2) 方法应分别返回 124

def sum(x,y=10,z=1) {x+y+z}
// x = 1
sum(1)
// x = 1, y= 2
sum(1, 2)

Groovy 在方法中不需要显式返回语句。默认情况下,最后一个评估的表达式会被作为方法输出返回。在上面的例子中,我们没有提到 return x+y+z。它将默认返回。

控制结构

在本节中,我们将讨论基本控制结构,即 if…else 语句、switch 语句、for 循环和 while 循环。

if-else 条件

Groovy 中的 if…else 条件与 Java 类似,只有一个例外,那就是 Groovy 如何评估逻辑 if 条件。在以下示例中,if 条件对布尔值和整数值都评估为真。在 Groovy 中,非零整数、非空值、非空字符串、初始化的集合和有效的匹配器都被评估为布尔值 true。这被称为 Groovy 真值。让我们看一下以下代码:

def condition1 = true
int condition2 = 0
if(condition1){
  println("Condition 1 satisfied")
  if(condition2){
    println("Condition 2 satisfied")
  }else{
    println("Condition 2 failed")
  }
}else{
  println("Condition 1 failed")
}

Groovy 也支持三元运算符 (x? y: z),类似于 Java,可以用来编写标准的 if-else 逻辑:

(condition2> 0 )? println("Positive") : println("Negative")

Groovy 还提供了一个称为 Elvis 运算符的额外运算符。它可以用作在用户想要验证变量是否为 null 值的场景中三元运算符的简短版本。考虑以下示例:

def inputName
String username = inputName?:"guest"

如果 inputName 不为空,则 username 将会是 inputName,否则将默认值 "guest" 分配给 username

switch 语句

Groovy 支持 Class、Object、Range、Collection、Pattern 和 Closure 作为 switch 语句中的分类器。任何实现了 isCase 方法的对象都可以用作 switch 语句中的分类器。以下示例展示了为各种分类器定义的案例。只需尝试不同的输入值并观察 switch 语句的输出:

def checkInput(def input){
switch(input){    
  case [3, 4, 5]   :   println("Array Matched"); break;
  case 10..15      :   println("Range Matched"); break;
  case Integer     :   println("Integer Matched"); break;
  case ~/\w+/      :   println("Pattern Matched"); break;
  case String      :   println("String Matched"); break;
  default          :   println("Nothing Matched"); break;
}
}
checkInput(3)  // will print Array Matched
checkInput(1)  // will print Integer Matched
checkInput(10)  // will print Range Matched
checkInput("abcd abcd") // will print String Matched
checkInput("abcd")  // will print Pattern Matched

循环

Groovy 支持两种循环类型:for (initialize; condition; increment)for-eachfor-each 风格表示为 for(variable in Iterable) { body}。由于循环在可迭代的对象集合上工作,它可以很容易地应用于数组、范围、集合等。让我们看一下以下代码:

// Traditional for loop
for(int i = 0; i< 3; i++) {/* do something */ }
// Loop over a Range
for(i in 1..5) println(i)
// Array iteration
def arr = ["Apple", "Banana", "Mango"]
for(i in arr) println(i)
// for applied on Set
for(i in ([10,10,11,11,12,12] as Set)) println(i)

while 循环与 Java 的 while 循环类似,尽管 Groovy 不支持 do-while 风格的循环。让我们演示一下 while 循环:

int count = 0
while(count < 5) {
  println count++
}

集合

我们假设你已经对 Java 集合框架JCF)有基本的了解,所以我们不会讨论集合框架的基本原理。我们从 Groovy 提供的集合框架和不同集合对象提供的常用实用方法开始。

Groovy 支持不同的集体数据类型来存储对象组,例如范围、列表、集合和映射。如果你已经是 Java 程序员,你会发现与 Java 相比,在 Groovy 中操作集体数据类型是多么容易。除了集合、列表和映射之外,Groovy 还引入了范围,这在 Java 中是不存在的。

Set

集合是一个无序的对象集合,没有重复元素。它可以被视为一个具有唯一性限制的无序列表,通常由列表构建而成。集合最多可以包含一个 null 元素。正如其名称所暗示的,这个接口模型化了数学集合抽象。

以下代码片段解释了如何创建 Set。可以使用 addaddAllremoveremoveAll 方法向 Set 中添加或删除元素。

你可能在数学课上已经学到了很多关于 Set 的知识,老师会教你不同的集合操作,例如并集和交集。Groovy 也提供了类似的功能。两个集合的并集包含两个集合中所有独特的元素和共同元素,且不重复。交集找出两个集合之间的共同元素。Set1Set2 的补集将包含所有在 Set2 中不存在的 Set1 的元素。

让我们看看以下代码:

// Creating a Set
def Set1 = [1,2,1,4,5,9] as Set
Set Set2 = new HashSet( ['a','b','c','d'] )

// Modifying a Set
Set2.add(1)
Set2.add(9)
Set2.addAll([4,5])        // Set2: [1, d, 4, b, 5, c, a, 9]

Set2.remove(1)        
Set2.removeAll([4,5])    // Set2: [d, b, c, a, 9]

// Union of Set
Set Union = Set1 + Set2     // Union: [1, 2, 4, 5, 9, d, b, c, a]

// Intersection of Set
Set intersection = Set1.intersect(Set2)    // Intersection: [9]

// Complement of Set
Set Complement = Union.minus(Set1)    // Complement: [d, b, c, a]

List

Set 相比,List 是一个有序的对象集合,List 可以包含重复元素。可以使用 List list = [] 创建一个 List,这会创建一个空的列表,它是 java.util.ArrayList 的一个实现。

以下代码片段展示了如何创建 List、从列表中读取值以及列出 List 的一些实用方法:

// Creating a List
 def list1 = ['a', 'b', 'c', 'd']
 def list2 = [3, 2, 1, 4, 5] as List

// Reading a List
println list1[1]          // Output: b
println list2.get(4)      // Output: 5
println list1.get(5)      //Throws IndexOutOfBoundsException

// Some utility method on List
//Sort a List
println list2.sort()      // Output: [1, 2, 3, 4, 5]
// Reserve a list
println list1.reverse()      // Output: [d, c, b, a]
// Finding elements
println ("Max:" + list2.max() + ":Last:" + list1.last())   
// Output: Max:5:Last:d

一些 List 方法接受闭包。以下示例展示了如何使用 find 方法找到第一个偶数,以及使用 findAll 方法列出所有偶数:

println list2.find({ it %2 == 0})    // Output: 2
println list2.findAll({it %2 == 0})  // Output: [2, 4]

不要被花括号内的 "it" 关键字弄混淆。我们将在 闭包 部分讨论这个问题。

Map

Map 是一个键值对集合,其中键是唯一的。在 Groovy 中,键值对由冒号分隔。可以通过 [:] 创建一个空的 Map。默认情况下,Map 的类型是 java.util.HashMap。如果键是 String 类型,你可以在 Map 声明中避免使用单引号或双引号。例如,如果你想创建一个以 name 为键、以 Groovy 为值的 Map,你可以使用以下表示法:

Map m1 = [name:"Groovy"]

在这里,[name: "Groovy"]["name":"Groovy"] 是相同的。默认情况下,Map 的键是字符串。但如果你想要将某个变量作为键,那么请使用括号,如下面的代码所示:

String s1 = "name"
Map m1 = [(s1):"Groovy"]

或者,你可以按以下方式创建一个 Map:

def m2 = [id:1,title: "Mastering Groovy" ] as Map

你可以使用key m2.get("id")m2["id"]从 Map 中获取对象。

小贴士

如果键是字符串,那么要获取值,你需要指定双引号内的键("")。如果你没有指定双引号内的键,它将把它视为变量名并尝试解析它。

现在我们将讨论 Map 的一些实用方法(each、any 和 every),这些方法接受闭包:

Map ageMap = [John:24, Meera:28,Kat:31,Lee:19,Harry:18]

要解析Map中的每个条目,你可以使用each。它接受条目或键值作为参数,如下表所示:

|

ageMap.each {key, value ->
  println "Name is "+key
  println "Age is " + value
}

|

ageMap.each {entry  ->
  println "Name is "+entry.key
  println "Age is " + entry.value
}

|

如果你想验证Map数据,你可以根据需要使用.every.any.every方法检查并确保所有记录都满足所述条件,而.any只是检查是否有任何记录满足条件。例如,如果你想检查是否有任何用户年龄超过 25 岁:

ageMap.any {entry -> entry.value > 25 }

它返回一个布尔值作为输出;在这种情况下,为真,因为 Meera 是 28 岁。

如果你想检查所有用户是否都超过 18 岁:

ageMap.every {entry -> entry.value > 18 }

它将返回 false,因为 Harry 是 18 岁。

你也可以使用与我们在列表部分中使用List相同的模式使用findfindAll方法为Map

范围

除了 Java 集合类型外,Groovy 还支持一种新的集体数据类型Range。它定义为两个值(通常是起点和终点),由两个点分隔。

要创建一个范围,请使用以下代码:

def range1 = 1..10
Range range2 = 'a'..'e'

要从Range读取值,请使用以下代码:

range1.each { println it }

你也可以使用.any.every运算符来验证特定要求的范围。它检查条件并返回一个布尔值。让我们看看以下代码:

range1.any { it > 5 }
range1.every { it > 0 }

要修改范围间隔,请使用以下代码。如果你想将范围间隔从默认的 1 修改为任何其他数字,你可以通过 step 方法设置它。它返回一个列表:

List l1 = range1.step(2)    //Output: [1, 3, 5, 7, 9]

要获取范围的起始元素和结束元素,请使用FromTo元素,如下所示代码:

range1.getFrom()      //Output: 1
range1.getTo()        //Output: 10

isReverse()方法用于检查范围趋势,以查看范围是否使用to value(较高值)到from value(较低值)构建:

range1.isReverse()       // Output: false

闭包

闭包通常与函数式语言相关联。Groovy 提供了一种非常简单的方法来创建闭包对象。Groovy 闭包就像用花括号编写的代码块。许多人将闭包与 Java 中的匿名函数相关联。

Groovy 中的闭包可以接受参数并返回一个值。默认情况下,Groovy 闭包中的最后一个语句是 return 语句。这意味着如果你没有从闭包显式返回任何值,它将默认返回闭包最后一个语句的输出。通常,我们定义闭包如下 {argument list-> closure body}。在这里,参数列表是闭包接受的逗号分隔值。参数是可选的。如果没有指定参数,则在闭包体中将有一个隐式的无类型参数 it 可用。如果闭包调用时没有提供参数,则 it 将为 null。

在下面的例子中,对于闭包 addTwo 的第一次调用,分配给变量 it 的值是 2,但在第二次调用中,it 被分配为 null:

def addTwo = {it+2 }
addTwo(2)          // Output: 4
addTwo()          // NullPointerException

或者,你甚至可以声明一个类型为闭包的变量。在 Groovy 中,闭包是 groovy.lang.Closure 类的子类:

groovy.lang.Closure closure1 = { println it }
closure1("This will be printed") // Output: This will be printed

为了将闭包体与参数列表分开,我们使用 -> 操作符。闭包体由零个或多个 Groovy 语句组成。像方法一样,它也可以在其作用域内引用和声明变量。

在下面的代码片段中,addOne 方法能够在其作用域内引用 constantValue 变量,尽管它是在闭包作用域外定义的。这样的变量被称为 free 变量。在闭包的花括号内定义的变量将被视为局部变量:

int constantValue = 9
def addOne = { Integer a -> constantValue + a }

addOne(1)         // unnamed () invocation. Output: 10
addOne.call(1)    // call() invocation. Output: 10
addOne("One")     // MissingMethodException

在前面的例子中,闭包的参数类型是整数类型。使用闭包时,花括号内的语句不会在显式调用它们之前执行,无论是使用 call() 方法还是通过闭包的 unnamed () 调用语法。在我们的例子中,闭包在第二行被声明,但当时并没有进行评估。只有在显式地对闭包调用 call() 方法时,它才会执行。这是闭包和代码块之间的重要区别。它们看起来可能相同,但实际上并不相同。闭包只有在调用 call() 方法时才会执行;不是在其定义时间。记住,闭包在 Groovy 中是一等对象,可以使用无类型变量或使用闭包变量来引用。在这两种情况下,它都源自 groovy.lang.Closure 类。这个类有重载的 call() 方法,可以没有参数或多个参数来调用闭包。

addOne 闭包以整数作为参数被调用时,它执行成功。然而,对于字符串类型作为参数,它抛出异常。同时观察,当我们将字符串作为参数传递给 addOne 闭包时,编译器并没有抱怨。这是因为所有参数都是在运行时检查的;编译器没有进行静态类型检查。

这个闭包上的 doCall() 方法是动态生成的,它只接受 Integer 类型的参数。因此,任何非 Integer 类型的调用都将抛出异常。doCall() 方法是隐式方法,不能被重写也不能被重新定义。当我们调用闭包的 call 方法或 unnamed () 语法时,这个方法总是隐式调用。

我们将通过讨论代理的概念来结束对闭包的讨论。这个特性在 Gradle 中被广泛使用。例如,当我们定义构建脚本中的存储库闭包或依赖闭包时,这些闭包将在 RepositoryHandlerDependencyHandler 类中执行。这些类作为代理传递给闭包。您可以参考 Gradle API 获取更多详细信息。在这里,我们不妨简单化问题。我们将通过简单的示例来理解这个概念。

考虑以下示例,我们正在尝试打印一个 myValue 变量,该变量在类中未定义。显然,这个调用将抛出异常,因为这个变量在作用域内未定义:

class PrintValue{
  def printClosure = {
    println myValue
  }
}
def pcl = new PrintValue().printClosure
pcl()   //Output: MissingPropertyException: No such property

可能存在一种情况,我们想要在另一个类上执行这个闭包。这个类可以作为代理传递给闭包:

class PrintHandler{
  def myValue = "I'm Defined Here"
}

def pcl = new PrintValue().printClosure
pcl.delegate = new PrintHandler()
pcl()

OUTPUT: I'm Defined Here

在这个例子中,PrintHandler 类已定义了 myValue 变量。我们已经委托并针对 PrintHandler 类执行了闭包。

到目前为止,它按预期工作。现在,如果 myValuePrintValue 类中被重新定义:

class PrintValue{
  def myValue = "I'm owner"
  def printClosure = {
    println myValue
  }
}

在此场景中,在执行闭包时,我们将找到输出为 I'm owner。这是因为,当闭包尝试解析 myValue 变量时,它发现该变量定义在所有者的作用域内(定义闭包的 PrintValue 类),因此它没有将调用委托给 PrintHandler 类。正式来说,这被称为 OWNER_FIRST 策略,这是默认策略。策略的解析方式是这样的——首先检查闭包,然后是闭包的作用域,然后是闭包的所有者,最后是代理。Groovy 非常灵活,它为我们提供了更改策略的能力。例如,要将调用委托给 PrintHandler 类,我们应该指定策略为 DELEGATE_FIRST

def pcl = new PrintValue().printClosure
pcl.resolveStrategy = Closure.DELEGATE_FIRST
pcl.delegate = new PrintHandler()
pcl()

使用 DELEGATE_FIRST 策略时,闭包将首先尝试将属性或方法解析到代理,然后是所有者。其他重要的策略包括:

  • OWNER_ONLY:它尝试仅在所有者内部解析属性或方法,并不进行代理。

  • DELEGATE_ONLY:闭包将解析属性引用或方法到代理。它完全忽略所有者。

  • TO_SELF:它将解析属性引用或方法到自身,并经过常规的 MetaClass 查找过程。

这确实是一个非常简短的描述。我建议您参考 Groovy 文档以获取更多详细信息,链接如下:docs.groovy-lang.org/latest/html/api/groovy/lang/Closure.html

构建器

在 Groovy 中,另一个重要特性是 Builder。Groovy Builders 允许您创建复杂的树状分层对象结构。例如,SwingUI 或 XML 文档可以非常容易地使用 Groovy 中的 DSL 或类似闭包的功能创建,得益于BuilderSupport类及其子类MarkupBuilderSwingBuilder的支持。

让我们通过一个示例来尝试理解。我们在本章的早期创建了Order类。假设我们有一个订单列表,我们想要将详细信息存储在名为orders.xml的文件中。因此,我们列表中的每个Order对象都应该作为 XML 文件中的一个节点保存。这些Order节点再次应该包含子节点、孙子节点,依此类推。如果我们尝试在 Java 中实现类似 DOM 的解析器,创建这种树状结构可能会很复杂:

<orders>
  <order>
    <no>1</no>
    <description>Ordered by customer 1</description>
    <customer>
      <name firstname='Customer1' />
      <email>cust1@example.com</email>
    </customer>
  </order>
  <order>
    <no>2</no>
    <description>Ordered by customer 2</description>
    <customer>
      <name firstname='Customer2' />
      <email>cust2@example.com</email>
    </customer>
  </order>
  ….
</orders>

但在 Groovy 中,这只是一些代码行和一些方法调用,结合闭包和命名参数。在下面的示例中,我们从MarkupBuilder类创建了一个builder对象来创建 XML 文档。然后我们定义orders为文档的根。然而,builder没有定义名为orders的方法。那么这是怎么工作的呢?

如前所述,MarkupBuilder类是BuilderSupport类的子类。BuilderSupport具有createNodeinvokeMethodnodeCompletedsetCurrentsetParent等方法。在运行时,通过在名为orders的构建器上调用createNode方法创建一个对象。以类似的方式,对于每个order对象,创建nodescriptioncustomer节点。最后,通过调用构建器对象的setParent方法将每个order节点附加到父orders节点:

def builder = new groovy.xml.MarkupBuilder(new FileWriter("orders.xml"))

  builder.orders{
    for(i in orderlist){
      order{
        no(i.orderNo)
        description(i.description)
        customer{
          name(firstname : i.orderedBy.name)
          email(i.orderedBy.email)
        }
      }
    }
  }

摘要

在本章中,我们讨论了一些基本的基本概念。我们学习了类、方法、bean、集合框架和闭包的概念。我们还开发了一个标记构建器来生成 XML 文件。这确实是对 Groovy 的非常简短的介绍。然而,在我看来,这个介绍应该足够编写您项目的 Gradle 脚本。

从下一章开始,我们将开始探索 Gradle 的核心功能。在下一章中,我们将学习 Groovy 中的任务管理。我们将仔细研究 Gradle 支持的不同内置任务。我们还将了解任务依赖和任务配置。然后我们将为构建脚本创建一些自定义任务。

第三章:管理任务

在本章中,我们将讨论 Gradle 构建脚本的基元,即任务(Task)。我们将详细探讨任务框架,如何创建自己的任务,覆盖 Gradle 提供的任务,任务配置,以及使用 Gradle 提供的不同方法创建自定义任务。我们还将讨论任务依赖。本章还将提供关于控制任务执行的观点,如何根据某些条件启用或禁用任务执行,以及跳过任务执行。Gradle 提供了一项称为增量构建支持的功能,如果任务是最新的,即任务的输入和输出没有变化,则会跳过任务的执行。如果你反复运行构建,这有助于减少脚本的构建时间。我们将通过一些示例来尝试理解这个功能。Gradle 默认支持此功能。我们将了解如何将此功能扩展到用户定义的任务。此外,我们还将探索 Gradle 提供的Project对象,以控制构建脚本。

构建脚本基础

构建脚本实际上是一组按某些预定义顺序执行并执行某些操作的动作。在 Gradle 中,我们称这些动作或动作组为任务,它是称为项目的父实体的一个部分。Gradle 构建文件中的执行原子单位称为任务。构建文件的结果可能是某些资产,如 JAR、WAR 等,或者它可能执行某些操作,如资产的部署和配置。每个build.gradle文件至少代表一个项目。在多项目或多模块构建的情况下,它也可能包含多个项目。我们将在第六章 与 Gradle 一起工作中讨论多项目构建。构建的执行代表Project对象的执行,该对象内部调用不同的任务以执行操作。

当你执行任何构建脚本时,Gradle 会为构建文件实例化org.gradle.api.Project对象,并给出一个隐式的项目对象。你可以使用此对象通过project.<methodname | property>或简单地<methodname | property>在构建文件中访问项目 API。例如;要在你的构建文件中打印项目的名称,你可以使用以下代码:

println "Project name is "+project.name
println "Project name is "+name // here project object is implicit
println "Project name is $project.name"
println "Project name is $name"

所有的上述语句都将返回相同的输出,即项目名称。项目名称是 build.gradle 文件父目录的名称。假设 build.gradle 位于 Chapter3 目录下;因此,上述语句的输出将是 项目名称是 Chapter3。你可以在 settings.gradle 文件中提供 rootProject.name=<新项目名称> 来更改项目名称。我们将在 第六章 使用 Gradle 中进一步讨论 settings.gradle 文件的用法。

注意

要得到输出 项目名称是 Chapter3,你需要在任务块之外编写语句。如果你在任务内编写它,并且我们使用名称或 $name 变量,它将显示任务名称。这是因为任务块内部,name 变量的作用域将不同。

以下是一些项目对象的属性,可以使用 getter 和 setter 方法来配置构建文件:

  • name // 只读,你只能使用 settings.gradle 来更改

  • parent // 只读

  • 版本

  • description

一些属性是只读的,它们由 Gradle 运行时直接设置。

Gradle 还提供了一些默认任务,可以在不应用任何插件的情况下使用,例如复制任务和压缩任务。你也可以为 项目 对象定义自己的自定义属性和自定义任务。

对于构建文件中的每个任务,Gradle 将实例化 Task 对象的一个实现。Task 接口有不同的实现;你可以在 docs.gradle.org/current/javadoc/org/gradle/api/Task.html 找到更多关于它的详细信息。类似于 Project 对象,你也可以使用 Task API 编程方式控制任务。当我们在后面的部分使用 Groovy 创建自定义任务时,你将看到更多关于这个的细节。总之:

  • 任务是一组动作和属性的集合。它可以依赖于其他一些任务

  • 任务可以接受输入并返回输出

  • 任务还提供了一些预定义属性,如名称和描述已启用

我们将从一个简单的构建文件示例开始,解释现有的项目属性,提供自定义属性,创建任务等。

考虑文件位置 /Chapter3/build.gradle

// Section 1: Project object existing properties
version = '1.0'
description = 'Sample Java Project'
// Section 2: Project level custom properties
ext {
  startDate="Jan 2015"
}
ext.endDate = "Dec 2015"
println "This is project configuration part, description is $description"
// Section 3: Task
task sampleTask1 {
  // Section 3.1: Task existing properties
  description = "This is task level description"
  // Section 3.2: Task level custom properties
  ext {
    taskDetail=" This is custom property of task1"

  }
println "This is sampleTask1 configuration statements, taskDetail is $taskDetail"

// Section 3.3: Task actions
doFirst {
println "Project name is $project.name, description is $project.description"
println "Task name is $name, description is $description"
    println "Project start date is $startDate"
  }
  doLast {
      println "Project endDate is $endDate"
  }

}
// Section 4: Task
task sampleTask2 {
  println "This is sampleTask2 configuration statements"

doFirst {
println "Task getProjectDetailsTask properties are: "+sampleTask1.taskDetail
  }
}

要执行前面的 build.gradle 文件:

$ gradle sampleTask1 sampleTask2
This is project configuration part, description is Sample Java Project
This is sampleTask1 configuration statements, taskDetail is  This is custom property of task1
This is sampleTask2 configuration statements
:sampleTask1
Project name is chapter3, description is Sample Java Project
Task name is sampleTask1, description is This is task level description
Project start date is Jan 2015
Project endDate is Dec 2015
:sampleTask2
Task getProjectDetailsTask properties are:  This is custom property of task1
BUILD SUCCESSFUL

Total time: 6.892 secs

在前面的示例中,在第一节中,我们覆盖了一些项目对象现有的属性。在第二节中,我们向项目对象添加了自定义属性。请注意,添加自定义属性的语法是在ext闭包内添加<name=value>对,或者我们可以将其定义为ext.<propertyname> = value。然后,我们在第三节和第四节中向此构建脚本添加了两个任务,并添加了自定义属性到sampleTask1任务。要添加/更新项目的属性,你不需要添加def关键字。def用于定义用户定义的变量。然而,在这里我们正在定义项目属性。如果你使用def startDate=<Value>,它将被视为变量而不是项目属性。

我们能够在sampleTask1中打印startDateendDate,因为我们将其添加为项目属性,可以在整个构建文件中直接访问。要调用任务方法或使用任务属性在任务外部,我们可以使用task.<property name>task.<method name>。正如前面的示例,在sampleTask2任务内部,我们正在打印sampleTask1.taskDetail

有多种方式可以指定任何项目的属性。当我们在第六章中讨论属性时,我们将详细讨论这一点,使用 Gradle

任务配置

我们在第一章中讨论了构建文件由三个阶段组成:初始化、配置和执行,以下简要解释:

  • 初始化创建项目对象。

  • 配置阶段配置项目对象,根据任务依赖关系创建DAG有向无环图)。它还执行项目和任务配置语句。

  • 执行阶段最终执行任务体中提到的操作。

任务 API 主要定义了两种类型的闭包:doFirst(Closure closure)和doLast(Closure closure),它们内部调用doFirst(Action action)doLast(Action action)。你可以提及一个或两个。

小贴士

在这些操作之外提到的语句是配置的一部分,在配置阶段执行。

要验证任务的配置阶段,你可以使用--dry-run–m选项执行构建脚本。--dry-run(或–m)选项只通过初始化和配置阶段,不通过执行阶段。尝试使用--dry-run选项执行前面的构建文件,你将在控制台上找到所有配置语句:

$ gradle --dry-run
This is project configuration part, description is Sample Java Project
This is sampleTask1 configuration statements, taskDetail is  This is custom property of task1
This is sampleTask2 configuration statements
:help SKIPPED

BUILD SUCCESSFUL

在 Gradle 2.4 版本中,在配置阶段实现了一些性能改进。更多详情请参考发布说明中的重要配置时间性能改进

任务执行

如前所述,任务不过是一系列执行以执行某些操作的单一或多个动作。如果需要,你可以向doFirstdoLast闭包添加多个动作。doFirst闭包将始终在doLast闭包之前执行。你甚至可以在任务定义之后向任务添加动作。

例如,在前面脚本中提到sampleTask2任务之后添加以下语句。

sampleTask2.doFirst { println "Actions added separately" }
sampleTask2.doLast { println " More Actions added " }

前面的语句将为sampleTask2添加两个额外的操作。Gradle 为doLast提供了一个简短的表示法,即<<

在 Groovy 中,<<是左移运算符,用于向列表中添加元素:

task sampleTask3 << {
        println "Executing task3"
}
sampleTask3.doFirst {println "Adding doFirst action" }

尝试执行sampleTask3并查看输出:

$ gradle sampleTask3
...
:sampleTask3
Adding doFirst action
Executing task3

BUILD SUCCESSFUL

如果在命令行上提到了多个任务,它们将按照定义的顺序执行(除非对任务应用了某些依赖关系)。

任务依赖

当我们谈论任何构建工具的构建生命周期或测试生命周期时,实际上内部发生了什么?它不仅仅执行一个任务;它基本上执行一系列任务,这些任务按照一定的顺序定义,而这个顺序就是任务的依赖关系。以构建任何 Java 项目为例。你可以通过执行gradle build任务来构建 Java 项目。这将完成所有工作,例如编译源代码、将类打包成 JAR 文件并将 JAR 文件复制到某个位置。这意味着所有这些过程都只是构建任务的一部分吗?我们在这里想要传达的信息是,Gradle 的build任务不仅仅执行一个任务,而是执行从compileJavaclassescompileTestJava等一系列任务,直到构建 JAR 文件。

任务依赖

图 3.1

前面的图只是应用 Java 插件后 DAG 的表示。它表示不同的任务以及它们是如何相互依赖的。

如果任务 Task1 依赖于另一个任务 Task2,那么 Gradle 会确保 Task2 始终在 Task1 之前执行。在前面的例子中,compileJavaclassesjar任务将始终在构建任务之前执行。一个任务可以依赖于一个或多个任务。两个或多个任务也可以依赖于同一个先决任务。例如,在前面的 DAG 中,javadoccompileTestjavajar任务依赖于classes任务。这并不意味着classes任务将执行三次。它将在构建生命周期中只执行一次。如果一个任务由于某些其他依赖关系已经执行,它将不会再次执行。它只会通知其他依赖任务其状态,以便依赖任务可以继续执行而无需再次调用它。

在构建文件中,任务依赖可以通过以下任何一种方式定义:

task task1(dependsOn: task2)
task task1(dependsOn: [task2,task3]) // in case of more than one dependency
task1.dependsOn task2, task3  //Another way of declaring dependency

许多插件提供了具有默认依赖的任务。正如我们在前面的图中看到的,classes 任务有 compileJava 依赖。如果你向 classes 任务添加任何其他依赖(例如,task1),它将把任务(task1)附加到 compileJava 任务上。这意味着,执行 classes 任务将执行 compileJavatask1。要使用一组新的依赖项专门覆盖现有的依赖项,请使用以下语法:

classes {dependsOn = [task1, task2]
}

在这里,执行 classes 任务将执行作为依赖任务的 task1task2,并且它将忽略 compileJava 任务。

任务排序

如果 任务 1 依赖于 任务 2,那么 Gradle 将确保任务 2始终在任务 1之前执行。然而,它并不确保任务的顺序。也就是说,它不会确保任务 2将在任务 1之前立即执行。在任务 2任务 1的执行之间,可能会执行其他任务。

任务排序

图 3.2

如前图所示,任务 1 依赖于 任务 2任务 3任务 4 是一个独立任务。如果你执行 gradle Task1 Task4,执行流程将是 任务 2任务 3任务 1,然后是 任务 4,就像一个任务依赖于多个任务一样。Gradle 以字母顺序执行依赖任务。

除了 dependsOn 之外,Gradle 还提供了一些额外的排序类别。例如,在最后一个任务执行之后,你可能想要清理在构建过程中创建的临时资源。为了启用这种排序,Gradle 提供了以下选项:

  • shouldRunAfter

  • mustRunAfter

  • finalyzedBy(性质上更严格)

让我们看看以下示例。创建 build_ordering.gradle 文件:

(1..6).each {
  task "sampleTask$it" << {
        println "Executing $name"
    }
  }

sampleTask1.dependsOn sampleTask2
sampleTask3.dependsOn sampleTask2

sampleTask5.finalizedBy sampleTask6
sampleTask5.mustRunAfter sampleTask4

在脚本中,我们创建了六个带有整数后缀的 sampleTask 任务。现在,为了理解任务排序,使用不同的任务名称执行前面的构建脚本:

$ gradle –b build_ordering.gradle sampleTask1

这将执行 sampleTask2sampleTask1

$ gradle –b build_ordering.gradle sampleTask1 sampleTask3

这将执行 sampleTask2sampleTask1sampleTask3。任务 sampleTask2 只会执行一次:

$ gradle –b build_ordering.gradle sampleTask5

这将执行 sampleTask5sampleTask6

注意,sampleTask5 任务将不会执行 sampleTask4,因为当这两个任务(sampleTask4sampleTask5)都是执行过程的一部分时,mustRunAfter 排序将会生效。这将在下面的命令中解释。这里,你也看到了 finalizedBy 操作的使用。它提供了结论顺序,即 sampleTask5 应立即由 sampleTask6 接替:

$ gradle –b build_ordering.gradle sampleTask5 sampleTask4

这将按顺序执行 sampleTask4sampleTask5sampleTask6。这是因为 sampleTask5 必须在 sampleTask4 之后运行,并且 sampleTask5 应由 sampleTask6 结束。

mustRunAftershouldRunAfter 之间的区别在于 mustRunAfter 是严格的排序,而 shouldRunAfter 是宽松的排序。考虑以下代码:

sampleTask1.dependsOn sampleTask2
sampleTask2.dependsOn sampleTask3
sampleTask3.mustRunAfter sampleTask1

在这种情况下,对于前两个语句,执行顺序是sampleTask3sampleTask2,然后是sampleTask1。下一个语句sampleTask3.mustRunAfter sampleTask1,表示sampleTask3必须在sampleTask1之后执行,这引入了循环依赖。因此,sampleTask1的执行将失败:

$ gradle –b build_ordering.gradle sampleTask1
FAILURE: Build failed with an exception.

* What went wrong:
Circular dependency between the following tasks:
:sampleTask1
\--- :sampleTask2
     \--- :sampleTask3
          \--- :sampleTask1 (*)

(*) - details omitted (listed previously)
. . .

如果你将mustRunAfter替换为shouldRunAfter,则不会抛出任何异常,并且在这种情况下会忽略严格的顺序。

任务操作

如果你厌倦了在命令行中输入完整的任务名称;这里有一个适合你的好选项。如果你已经以驼峰式(camelCase)格式定义了任务名称,你只需提及每个单词的首字母即可执行任务。例如,你可以使用缩写sT1来执行sampleTask1任务:

$ gradle -q –b build_ordering.gradle sT1 sT2

这将执行sampleTask1sampleTask2

如果驼峰式的缩写与多个任务匹配,将会导致歧义:

 $ gradle -q -b build_ordering.gradle sT

FAILURE: Build failed with an exception.

* What went wrong:
Task 'sT' is ambiguous in root project 'Chapter3'. Candidates are: 'sampleTask1', 'sampleTask2', 'sampleTask3', 'sampleTask4', 'sampleTask5', 'sampleTask6'.

* Try:
Run gradle tasks to get a list of available tasks. Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

现在,我们将探讨一些其他任务操作,例如条件执行、构建优化和强制执行。

条件执行

当你想要根据某些属性执行某些任务时,会有不同的场景。例如,你在build文件中有一个名为environment的属性。如果属性的值设置为prod,你想要执行特定于生产的任务;如果它是qa,你想要执行特定于测试的任务。创建一个build文件build_condition.gradle,包含以下代码片段:

ext {
  environment='prod'
// can set this value from property file or command line using -Pname=value option
}

task prodTask << {
  println 'Executing prod tasks '+ environment
}
prodTask.onlyIf {project.hasProperty('environment') && project.environment=='prod' }

task qaTask << {
  println 'Executing qa tasks '+ environment
}
qaTask.onlyIf { project.hasProperty('environment') && project.environment== 'qa '}

使用这两个任务执行先前的build文件:

$ gradle -b build_condition.gradle prodTask qaTask
:prodTask
Executing prod tasks prod
:qaTask SKIPPED

BUILD SUCCESSFUL

在这里,Gradle 跳过了qaTask并仅根据build文件中设置的环境属性执行了prodTask。你也可以在先前的ext闭包中移除环境值,并直接从命令行选项设置属性,然后尝试执行以下命令:

$ gradle -b build_condition.gradle -Penvironment=qa qaTask prodTask
:qaTask
Executing qatasks qa
:prodTask SKIPPED

BUILD SUCCESSFUL

可能存在另一种情况,当一个任务过时且你不想执行它时,即使其他任务依赖于这个任务。这个特性在任务配置阶段通过enabled选项得到支持:

task sampleTask12 << {
println " This task is disabled"
}
task sampleTask13 (dependsOn: sampleTask12) << {
println "This task depends on sampleTask12"
}
sampleTask12.enabled = false
$ gradle -b build_enabled.gradle sT12 sT13

:sampleTask12 SKIPPED
:sampleTask13
This task depends on task12

BUILD SUCCESSFUL

注意,你可以在配置阶段本身设置enabled。它不应该成为doFirstdoLast闭包的一部分:

task sampleTask12 {
   //enabled = false    // valid statement
   doLast {
      enabled = false   // Invalid statement
      println 'Task execution' 
   }   
}

在先前的例子中,如果我们尝试在doLast闭包中设置enabled = false,任务将不会执行。构建将失败,并出现Cannot call Task.setEnabled(boolean) on task ':sampleTask12' after task has started execution错误。

构建优化

考虑这样一个场景,你的build文件由 10 个任务组成,这些任务按照任务依赖关系顺序执行。在 10 个任务中,有五个任务正在修改文件系统上的五个不同文件。假设这五个文件是某些属性文件,这些构建任务正在设置属性值:

envproperty.txt
  env=prod
sysproperty.txt
  memory=1024
……

第一次执行后,属性文件被修改为相应的值。当你再次运行build脚本时,尽管文件已经被修改,构建脚本还是会再次修改这些文件。

Gradle 提供了一种基于任务输入和输出参数跳过这些类型任务执行机制,这被称为增量构建。这有助于减少构建时间。你可能已经注意到,当你应用 Java 插件并多次构建你的项目时,一些任务被标记为 UP-TO-DATE 关键字(执行时没有 -q 选项)。这意味着与上次执行这些任务相比,输入和输出没有变化,因此这些任务被忽略。

默认情况下,Gradle 为其内置任务提供此功能。你也可以通过任务输入和输出增强你的任务,输入和输出类型为 TaskInputsTaskOuputs。我们将通过一个示例来解释这种行为:

考虑 PropDetails.xml 文件:

<properties>
  <property>
    <filedetail>
      <name>envproperty.txt</name>
      <key>env</key>
      <value>prod</value>
    </filedetail>
  </property>
  <property>
    <filedetail>
      <name>sysproperty.txt</name>
      <key>memory</key>
      <value>1024</value>
    </filedetail>
  </property>
</properties>

考虑 build_optimization.gradle 文件:

task updateExample {
ext {
propXml = file('PropDetails.xml')
}
File envFile = file('envproperty.txt')
File sysFile = file('sysproperty.txt')

inputs.file propXml
outputs.files (envFile, sysFile)

doLast {
println "Generating Properties files"
def properties = new XmlParser().parse(propXml)
properties.property.each { property ->
def fileName = property.filedetail[0].name[0].text()
def key = property.filedetail[0].key[0].text()
def value = property.filedetail[0].value[0].text()
def destFile = new File("${fileName}")
destFile.text = "$key = ${value}\n"
}
}
}

$ gradle –b build_optimization.gradle updateExample

如果你第一次运行此任务,它将读取 PropDetail.xml 文件,并创建两个文件 envproperty.txtsysproperty.txt,其中包含 property 文件中提到的 key=value 对。现在,如果你再次运行此命令,你将看到以下输出:

:updateExample UP-TO-DATE
BUILD SUCCESSFUL

这意味着此任务的输入和输出没有变化;因此,无需再次执行任务。

尝试更改 XML 文件或生成的属性文件或删除输出文件。如果你再次运行 Gradle 命令,这次,任务将执行并重新创建文件。Gradle 内部生成输入参数和输出参数的快照(Gradle 生成一个哈希码以避免重复)并存储它。从下一次开始,Gradle 生成输入和输出参数的快照,如果两者相同,则避免执行任务。

另外,还有一个重要点需要记住,如果任务没有定义输出,则它将不会被考虑进行优化(UP-TO-DATE)。任务将始终执行。可能存在一种情况,即任务的输出不是文件或目录,它可能是其他逻辑构建步骤或系统相关的检查。在这种情况下,你可以使用 TaskOutputs.upToDateWhen() 方法或 outputs.upToDateWhen 闭包来检查特定情况并标记任务为 UP-TO-DATE

要跳过优化技术并强制完整执行任务,可以使用 --rerun-tasks 命令行选项。它将强制执行任务,即使它是 UP-TO-DATE

$ gradle –b build_optimization.gradle updateExample --rerun-tasks

--rerun-tasks 选项将始终执行任务,而不会检查输入和输出参数。

任务规则

我们在 Groovy 中讨论了 methodMissing 概念。你可以在 Groovy 中定义一些方法模式,这些模式可以在运行时响应预定义的模式的方法调用。任务规则为任务提供了相同的灵活性。它允许执行一个不存在的任务。Gradle 会检查任务规则,如果规则已被定义,则会创建任务。我们将通过一个简单的示例来查看其用法。例如,你有来自不同仓库服务器的不同资产,而不是为每个同步创建不同的任务,你可以创建如下任务规则:

tasks.addRule("Pattern: sync<repoServer>") { String taskName ->
  if (taskName.startsWith("sync")) {
    task(taskName) << {
      println "Syncing from repository: " + (taskName - 
'sync')
      }
    }
}

在这里,你可以为每个仓库服务器调用不同的任务,例如 gradle sync<repoServer>,它将从该仓库同步资产。

一个非常常见的任务规则示例可以在 Java 插件中找到。在 build 文件的第一行添加 apply plugin: 'java' 并运行以下命令:

$ gradle -b build_rule.gradle tasks

…………….
Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.
Pattern: sync<repoServer>

To see all tasks and more detail, run with --all.

BUILD SUCCESSFUL

Total time: 4.021 secs

目前,不必过多担心插件。我们将在第四章插件管理中详细讨论插件。

在上述输出中,你可以找到在 Java 插件中定义的规则。Gradle 提供了三个内置规则 clean<TaskName>build<sConfigurationName>upload<ConfigurationName> 以及新创建的 sync<repoServer> 规则。对于你在 build 文件中可用的所有任务(Java 插件任务和用户定义的任务),你可以使用 clean<TaskName> 执行一个额外的任务。例如,Java 插件中提供了 assemble、classes 和 jar 任务。除了执行正常的 clean 任务,删除构建目录外,你还可以执行 cleanClassescleanJar 等任务,这些任务仅清理特定任务的输出。

Gradle 的内置任务

对于日常的构建相关活动,Gradle 提供了各种任务。我们将查看一些 Gradle 的内置任务。

复制任务

此任务用于将文件(或目录)从一个位置复制到另一个位置:

task copyTask(type: Copy) {
  from "."
  into "abc"
  include('employees.xml')
}

copyTask 中,我们已配置了 from 位置和 into 位置,并添加了仅包含 employees.xml 的条件。

重命名任务

此任务是一个扩展的复制任务,用于重命名文件或目录:

task copyWithRename(type: Copy) {
  from "."
  into "dir1"
  include('employees.xml')
  rename { String fileName ->
  fileName.replace("employees", "abc")
  }
}

copyWithRename 任务中,添加了一个额外的 rename 闭包。

Zip 任务

此任务用于将一组文件(或目录)压缩并复制到目标目录:

task zipTask(type: Zip) {
  File destDir = file("dest")
  archiveName "sample.zip"
  from "src"
  destinationDir destDir
}

ziptask 任务中,添加了另一个 destinationDir 配置。你可以参考在线文档以获取这些任务的更详细 API。

注意

注意,这里我们没有提到这些任务的动作。任务本身知道该做什么。我们只需要配置任务来定义它们。

大多数时候,你使用的是插件的一部分任务。通常,插件是一组绑定在一起以实现某些特定功能的任务集合。例如;我们使用 java 插件来构建 Java 项目,使用 war 插件来创建 Web 存档,等等。当你将 java 插件应用到构建脚本中时,Java 任务会自动包含在内。我们将在第四章插件管理中详细讨论插件。

要执行 Java 任务,我们甚至不需要提及配置。对于这些任务,Gradle 应用约定,即默认配置。如果一个项目遵循某种约定,它可以直接执行这些任务而无需任何配置。如果不遵循,它应该定义自己的配置。要将 java 插件添加到 build 文件中,只需添加以下代码行:

apply plugin: 'java'

默认情况下,java 插件假定项目的源文件位于 src/main/java。如果源文件存在于该目录中,你可以执行 gradle compileJavagradle build 任务而无需任何配置。我们将在下一章中更多讨论 Java 插件和任务。

到目前为止,在本章中,我们已经对如何创建任务以及如何使用 Gradle 的内置任务有了一些了解。在下一节中,我们将探讨如何创建自定义任务。

自定义任务

Gradle 支持各种用于构建自动化的任务,无论是来自 Gradle 内置插件还是第三方插件。正如我们所知的软件谚语,变化是软件中唯一不变的事物;需求和复杂性会随着时间的推移而变化。很多时候,我们会遇到不同的自动化需求,而在 Gradle 中没有可用的任务或插件。在这种情况下,你可以通过添加自定义任务来扩展 Gradle。

自定义任务是一个增强的任务,你将其添加到 Gradle 中以满足自定义需求。它可以具有输入、输出、配置等。它的作用域不仅限于定义它的 build 文件;通过在类路径中添加自定义任务 JAR,它可以在其他项目中重用。你可以用 Groovy、Java 和 Scala 编写自定义任务。在本节中,我们将创建 Groovy 的自定义任务示例。

Gradle 提供了不同的方法在构建脚本中添加自定义任务:

  • build 文件

  • 项目目录内的 buildSrc 目录

  • 创建一个独立的 Groovy 项目

自定义任务是一个扩展自 DefaultTask 的 Java 或 Groovy 类。我们可以使用 @TaskAction 注解来定义任务操作。你可以在单个任务中添加多个操作。它们将按照定义的顺序执行。让我们从 build 文件中的一个简单的自定义任务开始。

考虑位于 Chapter3/Customtask/build.gradle 的文件:

println "Working on custom task in build script"

class SampleTask extends DefaultTask {
  String systemName = "DefaultMachineName"
  String systemGroup = "DefaultSystemGroup"
  @TaskAction
  def action1() {
    println "System Name is "+systemName+" and group is "+systemGroup
  }
  @TaskAction
    def action2() {
      println 'Adding multiple actions for refactoring'
    }

}

task hello(type: SampleTask)

hello {
  systemName='MyDevelopmentMachine'
  systemGroup='Development'
}
hello.doFirst {println "Executing first statement "}
hello.doLast {println "Executing last statement "}

以下文件的输出将是:

$ gradle -q hello
Executing first statement
System Name is MyDevelopmentMachine and group is Development
Adding multiple actions for refactoring
Executing last statement

BUILD SUCCESSFUL

在前面的示例中,我们定义了一个自定义任务类型SampleTask。我们添加了两个操作方法action1()action2()。您可以按需添加更多操作。我们添加了两个任务变量systemNamesystemGroup,并赋予了一些默认值。我们可以在配置任务(hello)时再次在项目范围内重新初始化这些变量。Gradle 还提供了使用doFirstdoLast闭包向任务添加更多操作的灵活性,就像其他任务一样。

一旦定义了任务类型,您就可以使用task <taskname>(type: <TaskType>)创建一个任务。

您可以在配置闭包中配置任务,无论是声明任务时还是作为一个单独的闭包,如前述文件中所述。

使用 buildSrc

如果您想将自定义任务代码与构建文件分开,但又不想为它创建一个单独的项目,您可以通过在buildSrc目录中添加自定义任务来实现这一点。

在项目基本目录中创建一个buildSrc目录,并创建以下提到的文件夹层次结构:buildSrc/src/main/groovy/ch3/SampleTask.groovy

将前面的SampleTask类移动到文件中。您还需要导入两个包:org.gradle.api.DefaultTaskorg.gradle.api.tasks.TaskAction。现在,build文件剩下以下代码片段:

task hello(type: com.test.SampleTask)
hello {
  systemName='MyDevelopmentMachine'
  systemGroup='Development'
}
hello.doFirst {println "Executing first statement "}
hello.doLast {println "Executing last statement "}

在执行hello任务时,您将找到之前显示的相同输出。

执行后,您将在项目中找到以下文件夹结构。请注意,您不需要编译SampleTask类。所有必要的步骤将由 Gradle 执行。它将编译类,创建 JAR 文件,并将所需的类自动添加到构建类路径中。您只需定义任务并执行它。

使用 buildSrc

图 3.3

限制是SampleTask任务仅在当前项目和其子项目中可用。您不能在其他项目中使用此任务。

独立的任务

为了克服创建自定义任务时buildSrc方式的限制,您需要创建一个独立的 Groovy 项目。将SampleTask类移动到新项目(SampleTaskProj)中,然后编译和打包项目。您甚至可以使用 Gradle 来构建此 Groovy 项目。只需将以下语句的build.gradle文件添加到SampleTaskProj项目中:

apply plugin: 'groovy'
apply plugin: 'eclipse'
version=1.0 // to generate jar with version
dependencies {
compile gradleApi() // It creates dependency on the API of current Gradle version
compile localGroovy() // it will use the Groovy shipped with Gradle
// these dependencies comes along with groovy plugin
}

如果您在 Eclipse 中创建项目,可以使用以下命令生成 Eclipse 类路径:

$ gradle clean cleanEclipse eclipse

现在,执行gradle build命令来构建项目。在构建目录中将创建一个 JAR 文件。要使用任务,在构建文件(将其视为另一个项目中的新build.gradle文件)中,我们需要在repositories闭包中引用 JAR 文件路径。

创建一个新项目,并更新build.gradle文件,内容如下:

buildscript {
repositories {
  // relative path of sampleTaskProject jar file
  flatDir {dirs "../SampleTaskProj/build/libs"}
}
dependencies {
classpath group: 'ch3', name: 'SampleTaskProj',version: '1.0'
}
}
task hello(type: ch3.SampleTask)

hello {
  systemName='MyDevelopmentMachine'
  systemGroup='Development'
}

hello.doFirst {println "Executing first statement "}
hello.doLast {println "Executing last statement "}

再次执行hello任务,您将找到相同的输出:

$ gradle hello
:hello
Executing first statement
Adding multiple actions for refactoring
System Name is MyDevelopmentMachine and group is Development
Executing last statement

BUILD SUCCESSFUL

摘要

在本章中,我们详细讨论了 Gradle 任务。我们学习了如何在 Gradle 中创建简单任务并向其添加操作。同时,我们还探讨了任务依赖关系。如果需要,我们还研究了任务的严格排序,使用mustRunAfterFinalyzedBy。我们还讨论了 Gradle 中的增量构建功能,该功能可以提高构建执行时间。其中一个重要的扩展是自定义任务。我们还看到了如何创建自定义任务以及如何在不同的项目中重用相同的任务。

如前所述,一个任务可以满足简单的构建需求。然而,需求不断增长,我们需要更多的任务。还需要将某些相关任务分组以执行特定行为。这种任务的分组是在插件中完成的。插件是一组不同任务结合在一起。因此,我们下一章将专门讨论插件管理。我们将讨论如何将任务绑定到插件以及如何利用插件来增强构建能力。

第四章 插件管理

在上一章中,我们讨论了 Gradle 任务,它是 Gradle 中的执行原子单位。在大多数情况下,任务在模块中只提供单一的工作单元。我们可以选择将任务捆绑在一起,并按特定顺序执行它们,以提供完整的功能。这种任务、属性和配置的组合称为插件。插件是任务的逻辑组合,可能具有生命周期。你可以根据需求配置插件以改变行为。你可以扩展它以提供额外的功能。在更广泛的意义上,Gradle 提供了两种类型的插件;脚本插件和二进制插件。Gradle 将构建脚本视为脚本插件,并且你可以通过将构建脚本导入到当前项目中,在项目中使用其他构建脚本。

二进制插件是我们使用诸如 Java 或 Groovy 之类的编程语言创建的插件。Gradle 为不同的构建功能提供了内置的二进制插件。在 Gradle 中创建二进制插件有不同的方法,我们将在自定义插件部分进行讨论。首先,我们将探索脚本插件。

脚本插件

脚本插件实际上就是一个 Gradle 文件,我们将其导入到其他构建文件中。这就像在不同的类之间模块化你的代码一样。当一个构建文件的大小超过一定限制,或者将不同的功能组合到一个文件中时,将连贯的任务分割到不同的构建文件中可能是一个更好的选择。然后,你可以将这些文件导入到主构建文件中以使用新的功能。

要导入构建文件,你可以使用以下代码:

apply from: <Path of otherfile.gradle>

这里,路径可以是本地文件,也可以是项目目录的相对位置或有效的 URL。然而,如果你提到 URL,缺点是文件将每次都下载。一旦构建文件被导入,你就可以使用构建文件中定义的任务,而无需任何额外的配置。

如果你正在主构建文件中添加多个构建文件,确保导入的构建文件中没有具有相同名称的任务。在导入过程中,如果 Gradle 发现两个具有相同名称的任务,它将抛出以下异常:

* What went wrong:
A problem occurred evaluating script.
> Cannot add task ':<TASK_NAME>' as a task with that name already exists.
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

考虑以下目录结构:

/Chapter4/scriptplugin.gradle

task scriptPluginTask1 << {
  println "this is scplugin1"
}

/Chapter4/build.gradle

apply from: 'scriptplugin.gradle'

task mainTask << {
  println "This is main task"
}

执行以下命令:

$ gradle mainTask scriptPluginTask1
:mainTask
This is main task
:scriptPluginTask1
this is scplugin1

BUILD SUCCESSFUL

在这里,我们在scriptplugin.gradle文件中定义了scriptPluginTask1,并将此构建文件导入到主脚本build.gradle中。因此,将scriptplugin.gradle文件导入到build.gradle中会使scriptPluginTask1在主构建文件中可用,并且你可以直接调用它而无需提及任何构建文件名。

二进制插件

二进制插件是实现 Plugin 接口的类,你可以将其嵌入到构建脚本中。或者,你可以创建一个单独的项目,将其打包成一个 jar 文件,并将该 jar 文件作为类路径条目添加到项目中。第二种方法使其更具可重用性。每个二进制插件都有一个 ID 来唯一标识它。要使用二进制插件,你需要使用 apply plugin 语句来包含它:

apply plugin: '<pluginid>'

例如,要使用 Java 插件,你可以编写以下代码:

apply plugin: 'java'

你也可以使用类类型来添加插件。例如,如果你正在创建一个自定义类,DisplayPlugin,作为一个插件,你可以应用以下代码:

apply plugin: DisplayPlugin

在使用这种方法之前,请确保你在构建文件中使用导入语句导入此类。所有 Gradle 核心插件默认都可用。你不需要任何额外的配置就可以使用它们。对于第三方或社区插件,在使用之前,你需要确保它们在类路径中可用。你可以通过在 buildscript{} 闭包中添加插件到类路径来实现。当你将任何插件应用到构建文件时,该插件的所有任务都会自动添加。你可以直接使用默认配置的任务,或者如果需要,你可以自定义任务配置。

Gradle 的内置插件

Gradle 提供了不同的内置插件来自动化构建过程。Gradle 不仅提供了不同的插件来构建项目,还提供了用于测试项目、代码分析、IDE 支持、Web 容器支持等的插件。

以下是一些不同类别中常用的插件。你可以在 Gradle 文档的docs.gradle.org/current/userguide/userguide中找到关于核心插件的更多详细信息。

构建和测试插件

这些插件也支持测试功能,可以执行 Junit 和 TestNG 测试:

  • Java 插件

  • Groovy 插件

  • Scala 插件

  • War 插件

代码分析插件

以下是一些代码分析插件:

  • Checkstyle 插件

  • FindBugs 插件

  • Sonar 插件

  • Sonar Runner 插件

  • PMD 插件

IDE 插件

以下是一些 IDE 插件:

  • Eclipse 插件

  • IDEA 插件

这些是一些常用的插件。除了核心插件之外,你还可以在plugins.gradle.org/找到第三方插件。它允许使用 Gradle Plugin Publishing 插件发布二进制插件。考虑花一些时间学习如何发布插件以及如何使用 Plugin Publishing 插件。在接下来的章节中,我们将学习一些核心插件。在下一节中,我们将探索 Java 插件。

Java 插件

在 第一章,使用 Gradle 入门 中,我们已创建了一个名为 FirstGradleProject 的 Java 项目。然而,讨论仅限于 Eclipse 插件任务。我们没有讨论关于 Java 插件的内容。Java 插件是 Gradle 核心 API 的一部分,它使我们能够使用支持编译 Java 代码、测试代码、组装二进制文件以创建库等任务来构建 Java 项目。它支持约定而非配置。这意味着,如果我们使用此插件,一些默认配置已经对开发者可用,例如源代码的位置、编译后的类文件的位置以及 jar 命名约定。除非我们想要覆盖这些配置,否则我们不需要编写大量代码来与默认任务和属性一起工作。

要应用 Java 插件,我们只需在构建文件中添加一个语句:

apply plugin: 'java'

在内部,Java 插件的 apply 方法使用 project 对象作为参数调用,并且启用构建脚本以便使用 Java 插件提供的所有任务和属性。为了理解 Java 插件,我们将创建一个新的 Java 应用程序(项目名称 Ch04-Java1),类似于我们在 第一章 中开发的 Java 项目 FirstGradleProject使用 Gradle 入门。我们将添加两个新的类,Customer 和 Order;我们还将添加一个新的 JUnit 或 TestNG 库依赖项,以支持项目的单元测试功能。

通过这个示例的帮助,我们将探索不同的 Java 插件约定。更准确地说,我们将尝试理解不同的任务是如何工作的,以及 Java 插件支持哪些默认约定。然后,在下一节中,我们将学习如何自定义不同的属性,以便我们可以在构建文件中创建自己的配置。

约定

为了理解约定,让我们从 Java 插件任务开始。一旦我们将 Java 插件应用到项目中以显示所有可用的任务(项目名称 Ch04-Java1),我们就可以使用任务命令:

$ gradle tasks --all
...

Build tasks
-----------
assemble - Assembles the outputs of this project. [jar]
build - Assembles and tests this project. [assemble, check]
buildDependents - Assembles and tests this project and all projects that depend on it. [build]
buildNeeded - Assembles and tests this project and all projects it depends on. [build]
classes - Assembles classes 'main'.
 compileJava - Compiles Java source 'main:java'.
 processResources - Processes JVM resources 'main:resources'.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes. [classes]
testClasses - Assembles classes 'test'. [classes]
 compileTestJava - Compiles Java source 'test:java'.
 processTestResources - Processes JVM resources 'test:resources'.

...

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.[classes]

...

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.

To see all tasks and more detail, run with --all.

BUILD SUCCESSFUL

上述输出显示了 Java 插件中的不同构建任务、测试任务、文档任务和其他可用任务。输出还显示了不同任务之间的任务依赖关系。例如,任务类内部依赖于compileJavaprocessResources任务,分别编译和处理src/main/javasrc/main/resources中的源代码和资源。同样,compileTestJava任务和processTestResources任务分别编译和处理src/test/javasrc/test/resources中的资源。所有这些任务的输出是编译后的类和资源,按照惯例将在build目录下创建,并在程序执行期间添加到classpath中。现在,让我们通过一个示例来探索这些任务的意义以及默认可用的约定。

要仅在src/main下编译类,我们应该使用classes任务。编译后的类将创建在build/classes/目录下。

$ gradle classes
:compileJava
:processResources UP-TO-DATE
:classes

BUILD SUCCESSFUL

testClasses任务编译并处理测试类和资源,并且还会执行类任务。在以下输出中,你可以看到compileJavaprocessResourcesclasses任务再次执行,但任务被标记为UP-TO-DATE。这是因为这些任务的输入和输出没有变化,因为我们已经在上一条命令中执行了classes任务。执行成功后,你将在build/classes文件夹下找到一个测试目录:

$ gradle testClasses
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses

BUILD SUCCESSFUL

另一个重要的任务是test任务。这个任务可以帮助执行在src/test目录下编写的单元测试代码。执行成功后,你将在build/test-results目录下找到测试结果:

$ gradle test
: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

BUILD SUCCESSFUL

你可以使用assemble任务或jar任务将类和资源打包到 JAR 文件中。jar任务只会创建 JAR 文件,而assemble任务可以帮助你生成其他工件,包括 JAR 文件。例如,当你应用 war 插件时,jar任务被禁用,并替换为 war 任务。默认情况下,JAR 文件命名为<project-name>.jar,并创建在build/libs下。如果你在构建文件中没有设置<project-name>,你将得到名为<project-folder-name>.jar的 JAR 文件名。如果 JAR 文件不包含任何版本,这并不是一个好的做法。你可以在构建文件中为你的项目添加版本属性,这将生成<name>-<version>.jar。在我们的示例中,项目名称是Ch04-Java1,版本属性在构建文件中设置为1.0。因此,JAR 文件将被命名为Ch04-Java1-1.0.jar。执行以下命令,你将在build/libs下找到 JAR 文件:

$ gradle assemble
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar
:assemble

BUILD SUCCESSFUL

小贴士

JAR 文件中不会打包任何测试类。

另一个任务是build任务,它一起执行checkassemble任务。clean任务删除另一个任务创建的所有工件。它实际上删除了完整的build/文件夹。这意味着,clean任务删除了所有任务生成的输出,即checkassemble。要删除特定任务的输出,我们可以应用clean<TaskName>规则。例如,要删除由build任务创建的 jar 文件,我们可以执行gradle cleanJar命令。

Java 插件中的所有任务都是基于诸如源目录位置、构建文件夹名称、测试结果文件夹等约定来执行的。为了更好地理解这一点,以下示例展示了 Gradle 支持的一些约定:

task displayJavaPluginConvention << {

  println "Lib Directory: $libsDir"
  println "Lib Directory Name: $libsDirName"
  println "Reports Directory: $reportsDir"
  println "Test Result Directory: $testResultsDir"

  println "Source Code in two sourcesets: $sourceSets"
  println "Production Code: ${sourceSets.main.java.srcDirs}"
  println "Test Code: ${sourceSets.test.java.srcDirs}"
println "Production code output: ${sourceSets.main.output.classesDir} & ${sourceSets.main.output.resourcesDir}"
println "Test code output: ${sourceSets.test.output.classesDir} & ${sourceSets.test.output.resourcesDir}"
}

输出显示了 Java 插件支持的各个约定。您可以在官方 Gradle 文档中找到完整的列表,网址为docs.gradle.org/current/userguide/java_plugin.html

$ gradle displayJavaPluginConvention
:displayJavaPluginConvention
Lib Directory: <path>/build/libs
Lib Directory Name: libs
Reports Directory: <path>/build/reports
Test Result Directory: <path>/build/test-results
Source Code in two sourcesets: [source set 'main', source set 'test']
Production Code: [<path>/src/main/java]
Test Code: [<path>/src/test/java]
Production code output: <path>/build/classes/main & <path>/build/resources/main
Test code output: <path>/build/classes/test & <path>/build/resources/test

BUILD SUCCESSFUL

有时,这些默认配置可能不够用。我们可能需要配置一些默认属性以支持我们的需求。在下一节中,我们将探讨如何配置一些默认配置。

配置

在前面的示例中,我们了解了 Java 插件中可用的默认属性或约定。现在,我们将配置一些这些属性。当我们想要更改构建目录名称、库文件夹名称或项目的源文件位置时,这很重要。

与源相关的配置更改可以在sourceSets闭包中设置。即将到来的代码片段(项目名称Ch04-Java2)显示,源代码位置已从src/main/java更改为src/productioncode,分别用于源代码位置和src/test/javasrc/testcode用于测试代码位置。因此,编译后的类现在将分别存储在classes/productioncodeclasses/testcode位置,分别对应源代码和测试代码。这不会替换mainproductioncode的源目录,但 Gradle 现在将在mainproductioncode目录中查找源代码,在testtestcode目录中查找测试代码。如果您希望 Gradle 仅在productioncode目录中查找源代码,您可以设置java.srcDirs属性。

这些 Java 插件约定是在JavaPluginConventionBasePluginConvention类中编写的。其中一个这样的属性,testResultsDirName,也可以在build文件中设置:

buildDir = 'buildfolder'
libsDirName = 'libfolder'

sourceSets {
  main {
    java {
      srcDir 'src/productioncode/java'
    }
    resources {
      srcDir 'src/productioncode/resources'
    }
  }
  test{

    java {
      srcDir 'src/testcode/java'
    }
    resources {
      srcDir 'src/testcode/resources'
    }
  }
}

testResultsDirName = "$buildDir/new-test-result"
sourceSets.main.output.classesDir "${buildDir}/classes/productioncode/java"
sourceSets.main.output.resourcesDir "${buildDir}/classes/productioncode/resources"
sourceSets.test.output.classesDir "${buildDir}/classes/testcode/java"
sourceSets.test.output.resourcesDir "${buildDir}/classes/testcode/resources"

这些更改将确保buildfolderlibfoldertest-result文件夹已被替换为buildfolderlibfoldernew-test-result文件夹。

图 4.1 显示了src文件夹和新的buildfolder的目录结构:

配置

图 4.1

所有这些新更改都可以通过执行之前创建的displayJavaPluginConvention任务来验证。执行任务后,你会发现输出已更新为新的配置:

$ gradle displayJavaPluginConvention
:displayJavaPluginConvention
Lib Directory: <path>/buildfolder/libfolder
Lib Directory Name: libfolder
Reports Directory: <path>/buildfolder/reports
Test Result Directory: %path%/buildfolder/new-test-result
Source Code in two sourcesets: [source set 'main', source set 'test']
Production Code: [<path>/src/main/java, <path>/src/productioncode/java]
Test Code: [<path>/src/test/java, <path>/src/testcode/java]
Production code output: <path>/buildfolder/classes/productioncode/java & <path>/buildfolder/classes/productioncode/resources
Test code output: <path>/buildfolder/classes/testcode/java & <path>/buildfolder/classes/testcode/resources

BUILD SUCCESSFUL

自定义插件

在本节中,我们将讨论如何创建自定义插件。可以通过实现org.gradle.api.Plugin<T>接口来创建插件。此接口有一个名为apply(T target)的方法,必须在插件类中实现。通常,我们为 Gradle 项目编写插件。在这种情况下,T 变为 Project。然而,T 可以是任何类型的对象。

实现插件接口的类可以放置在多个位置,例如:

  • 相同的构建文件

  • buildSrc目录

  • 独立项目

这与我们在上一章中讨论的创建自定义任务类似。当我们在一个构建文件中定义一个插件时,其作用域仅限于定义的项目。这意味着,此插件不能在其他任何项目中重用。如果我们想将我们的插件分发给其他项目,这并不是一个好主意。对于多项目 Gradle 构建,插件代码可以放置在根项目的buildSrc文件夹或根项目的构建文件中。所有子项目都将能够访问此自定义插件。创建插件最优雅的方式是创建一个独立的 Groovy 项目,从它创建一个 jar 文件,并在项目之间和团队之间共享插件。现在,我们将通过示例探索如何创建一个自定义插件。

构建文件

在以下示例中,我们添加了一个FilePlugin类,该类在构建文件中实现了 Plugin 接口。在 apply 方法中,我们添加了两个任务,copymove。这些任务是简单的任务,它们在控制台打印一行。现在,如果我们想执行copymove任务,我们需要将此插件添加到构建文件中。在这个例子中,插件名称是FilePlugin。我们使用apply plugin语句添加此插件。如果没有添加插件,当你尝试执行复制任务时,你会找到Could not find property 'copy' on root project 'PROJECT_NAME'.的错误信息:

apply plugin: FilePlugin

class FilePlugin implements Plugin<Project> {
  void apply(Project project) {
    project.task('copy') << {
      println "Task copy is running"
        //....
      }
    project.task('move') << {
      println "Task move is running"
      //...
    }
  }
}
copy.doLast { println "Copy Task ending .." }

在命令行中执行复制任务(针对Ch04_CustomPlugin1项目)时,我们发现在控制台按预期打印出以下两行:

$ gradle copy
:copy
Task copy is running
Copy Task ending ..
BUILD SUCCESSFUL

buildSrc目录

与任务类似,为了将插件代码与构建文件分开,我们可以在项目根目录内创建一个 buildSrc 文件夹,并将任何通用代码、任务或插件放置在这个文件夹中。在以下示例中,插件是在 buildSrc 文件夹中创建的,它可以在根构建文件和所有子项目中重用。我们在 buildSrc/src/main/groovy 下创建了一个 FilePlugin.groovy 类。这个类实现了插件接口,并在 apply 方法中添加了两个任务:copy 任务和 move 任务。这个 FilePlugin.groovy 类与上一个示例中所做的工作类似。对于这个示例,我们将创建一个项目 Ch04_CustomPlugin2。此外,在 FilePlugin.groovy 类中,我们需要添加包声明和导入语句(import org.gradle.api.*)。

在构建执行期间,此插件类将由 Gradle 自动编译并添加到项目的类路径中。由于插件定义不在构建文件中,我们需要一种机制在构建文件中声明插件信息。这是通过导入 Plugin 类并使用 apply plugin 语句添加插件来实现的。以下代码片段显示了主构建文件的内容。此外,我们在 copy 任务中添加了一个 doLast 方法,仅用于日志记录目的:

import ch4.FilePlugin
apply plugin: FilePlugin

copy.doLast {   
println "This is main project copy dolast"
}

接下来,我们创建了两个子项目:project1project2。每个项目都有一个简单的构建文件。这个构建文件与主构建文件类似。构建文件导入了并应用了 FilePlugin,并将 doLast 方法添加到 copy 任务中用于日志记录。以下代码显示了 project1build.gradle 文件内容。project2 的构建文件也与此类似:

import ch4.FilePlugin
apply plugin: FilePlugin

copy.doLast {
  println "Additional doLast for project1"
}

我们需要另一个 settings.gradle 文件,它包括主项目中的子项目:

include 'project1', 'project2'

不要与 settings.gradle 文件混淆。我们将在第六章“与 Gradle 一起工作”中详细讨论多项目构建。与 Gradle 一起工作

为了方便起见,Ch04_CustomPlugin2 项目的目录结构如图 4.2 所示:

构建 Src 目录

图 4.2

当我们执行 copy 任务时,我们发现有三个 copy 任务正在执行:一个来自主项目,另外两个来自子项目 project 1project 2

$ gradle copy
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes
:buildSrc:jar
:buildSrc:assemble
: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
:copy
Task copy is running
This is main project copy dolast
:project1:copy
Task copy is running
Additional doLast for project1
:project2:copy
Task copy is running
Additional doLast for project2

BUILD SUCCESSFUL

独立项目

在最后一节中,我们将插件代码放在了 buildSrc 目录中,并在根构建文件和所有子项目的构建文件中使用了该插件。这只是将插件代码模块化从构建逻辑中分离出来的一步。然而,此插件在其他项目中不可重用。理想情况下,插件应该在一个独立的 Groovy 项目中创建。然后我们创建一个 JAR 文件,并将该 JAR 文件包含在其他构建文件的类路径中。在本节中,我们将探讨如何创建一个独立的插件项目。

我们将从一个简单的 Groovy 项目开始。我们将在src/main/groovy中添加一个插件类FilePlugin.groovy和两个任务CopyTaskMoveTask。我们还会在资源文件夹中添加一个属性文件。项目的快照(Ch04_CustomPlugin3)显示在图 4.3 中:

独立项目

图 4.3

FilePlugin.groovy类通过引用CopyTaskMoveTask类创建了两个名为copymove的任务。这些任务是通过在TaskContainer对象上调用create(...)方法,并将tasknametask类作为方法参数来创建的。这两个任务都扩展了DefaultTask并定义了自己的实现。这只是一个例子,展示了我们在上一章中学到的创建自定义任务的方法。我们创建了一个额外的任务customTask,它将打印sourceFile属性的值。sourceFile属性是通过扩展对象定义的。插件扩展是普通的 Groovy 对象,用于向插件添加属性。你可以使用extension对象向Plugins提供属性/配置信息。你可以在插件中创建多个扩展对象来将相关的属性分组在一起。Gradle 为每个扩展对象添加一个配置闭包块。

FilePlugin.groovy类的代码片段如下:

package ch4.custom.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import ch4.custom.tasks.CopyTask
import ch4.custom.tasks.MoveTask

class FilePlugin implements Plugin<Project> {

  @Override
  public void apply(Project project) {

    def extension = project.extensions.create("simpleExt", FilePluginRootExtension)

    project.tasks.create("copy", CopyTask.class)
    project.tasks.create("move", MoveTask.class)
    project.task('customTask') << {
    println "Source file is "+project.filePluginExtension.sourceFile
    }
  }
}

下面的代码是AbstractTaskCopyTaskMoveTaskextension类的源代码。

文件:AbstractTask.groovy

package ch4.custom.tasks

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class AbstractTask extends DefaultTask {

}

文件:CopyTask.groovy

package ch4.custom.tasks

import org.gradle.api.tasks.TaskAction

class CopyTask extends AbstractTask {

  @TaskAction
  def action1() {
    println "Copy Task Running"
  }
}

文件:MoveTask.groovy

package ch4.custom.tasks

import org.gradle.api.tasks.TaskAction

class MoveTask extends AbstractTask {

  @TaskAction
  def action1() {
    println "Move Task Running"
  }

}

文件:FilePluginRootExtension.groovy

package ch4.custom.plugin

class FilePluginRootExtension {

  def sourceFile = "/home/tmp"
  def destinationFile

}

现在,我们需要一个插件 ID,这样 Gradle 才能找到这个插件信息。这是通过在src/main/resources/META-INF/gradle-plugins下创建一个属性文件来完成的。文件名成为插件 ID。在我们的例子中,我们给文件命名为fileplugin.properties。因此,插件 ID 是fileplugin。在任何其他构建文件中,我们现在可以应用这个插件,如下所示:

apply plugin: 'fileplugin'

fileplugin.properties文件中,我们需要添加implementation-class属性,它映射到实现类中的主插件:

implementation-class=ch4.custom.plugin.FilePlugin

这就是你需要的一切。现在,我们可以构建这个项目来创建一个 jar 文件,然后我们可以在任何其他项目中使用这个 jar。在我们的例子中,jar 文件被命名为Ch04_CustomPlugin3-1.0.jar。如果你希望将插件发布到plugins.gradle.org/,你需要确保插件 ID 是唯一的。在这种情况下,你可能想要将fileplugin.properties重命名为类似mastering.gradle.ch4.properties的东西,以确保插件 ID 的唯一性。

一旦创建了 jar 文件,该插件就可以在任何其他构建文件中使用。代码片段显示了如何通过 buildscript 闭包将本地目录定义为仓库。插件 jar 文件可以通过依赖闭包包含在类路径中。在示例中,我们正在使用本地目录中的插件。理想情况下,我们应该将插件 jar 发布到私有或公共仓库,并通过 Maven 或 Ivy URL 引用它:

buildscript {
  repositories {
    flatDir {dirs "../Ch04_CustomPlugin3/build/libs/"}
  }
dependencies {
  classpath group: 'ch4.custom.plugin', name: 'Ch04_CustomPlugin3',version: '1.0'
}
}
apply plugin: 'fileplugin'

copy.doLast {
  println "This is from project $project.name"
}

我们在复制任务中添加了一个 dolast,它打印项目名称。尝试执行以下命令:

$ gradle copy cT
:copy
Copy Task Running
This is from project UsingPlugin
:customTask
Source file is /home/tmp

BUILD SUCCESSFUL

Total time: 3.59 secs

从输出中,你可以理解复制任务有两个语句。一个我们在插件定义中提到,另一个我们在 build.gradle 文件中添加。customTask 的输出打印了源文件的默认值,即 /home/tmp。这个值是在 FilePluginRootExtension.groovy 类中设置的。如果你想将属性更新为其他值,请在构建文件中添加以下配置闭包:

filePluginExtension {
  sourceFile = "/home/user1"
}

在添加前面的闭包之后,尝试执行以下命令:

$ gradle cT
:customTask
Source file is /home/user1

BUILD SUCCESSFUL

Total time: 3.437 secs

现在,输出已更改为 filePluginExtension 闭包中提到的新的值。

摘要

在本章中,我们主要讨论了两个主题:Java 插件和自定义插件。在 Java 插件中,我们学习了 Gradle 支持的默认约定和属性。然后我们讨论了如何自定义和配置这些属性。在自定义插件中,我们展示了创建插件的不同方法。然而,Gradle 中有如此多的插件需要讨论。我们将在第六章 Working with Gradle 和第七章 Continuous Integration 中讨论一些重要的插件。然而,我们无法在本书中涵盖所有插件。我们请求读者查阅 Gradle 文档以获取更多详细信息。

在下一章中,我们将介绍 Gradle 中的另一个重要主题,即依赖管理。我们将学习在构建文件中各种仓库配置、不同的依赖解析策略、在仓库中发布工件等内容。

第五章:依赖管理

任何软件最重要的特性之一就是依赖管理。正如我们所知,没有任何软件是独立工作的,我们通常依赖于第三方或开源库。这些库在编译和运行时执行过程中是必需的,并且它们必须存在于类路径中。Gradle 对依赖管理提供了出色的支持。我们只需在构建文件中编写几行代码,Gradle 就会在内部完成所有繁重的配置管理工作。

在本章中,我们将深入探讨 Gradle 的依赖管理细节。我们将讨论不同的特性,例如如何管理项目依赖、解决冲突以及解决策略。我们还将讨论如何在不同的仓库中发布工件。

概述

依赖管理是任何构建工具最重要的特性之一。它有助于以更好的方式管理软件依赖。如果你使用的是Ant,它最初不支持任何依赖管理,你需要将每个依赖的 jar 文件及其位置写入build.xml。对于没有太多依赖的小型应用程序,这种方法可能效果不错。然而,对于企业应用程序,软件依赖于数百个其他库,这些库内部可能还依赖于其他库(传递依赖),在这种情况下,需要在build.xml中配置每个 jar 文件的方法可能可行,但需要巨大的维护工作量。此外,管理它们的版本冲突对于任何开发者来说都是一个巨大的痛苦,可能会将构建过程变成一场噩梦。为了解决 Ant 中的这些缺点,Maven 引入了内置的依赖管理解决方案。

之后,Ant 也集成了 Apache Ivy(一个依赖管理解决方案)以提供相同的功能。Gradle 带来了自己的依赖管理实现。它有助于定义一级依赖,逻辑地将它们分组到不同的配置中,定义多个仓库,并在构建文件执行后提供发布资产的任务。它还支持 Ivy、Maven 和平文件仓库。在本章中,除了依赖管理之外,我们还将探讨仓库配置和资产发布,即如何配置不同的仓库并将资产上传到仓库。

依赖配置

在开始依赖配置之前,让我们讨论一下如何在 Java 中发布打包的软件。您可以将软件打包并发布为.jar.war.ear文件格式到仓库中。目标是共享这些资产在组织内部的团队或开源开发者之间。考虑一个场景,您正在将一个实用项目(messageutil.jar)发布到仓库中。尽管发布过程主要取决于组织的政策,但常见的做法是,您计划发布的所有资产都应该版本化并存储在中央仓库中,以便其他团队可以共享它。这种版本化有助于跟踪库的不同版本。有了版本化的库,您还可以在出现任何功能问题时回滚到旧版本。每次您将任何资产发布到仓库时,请确保它已版本化。要了解更多关于版本化的信息,请查看此链接:semver.org/

依赖类型

除了内部或外部的 JAR 文件外,项目还可以依赖于:

  • 文件系统上的文件

  • 同一构建中的其他一些项目(在多项目构建的情况下)

  • Gradle API(用于自定义任务和插件)

  • Gradle 使用的 Groovy 版本(用于自定义任务和插件)

在我们开发自定义任务和插件时,在前几章中我们看到了 Gradle API 和 Groovy 版本的示例。项目依赖关系将在第六章 使用 Gradle 中讨论。在本章中,我们将讨论全局和本地仓库上的其他模块依赖关系,以及本地系统上的文件依赖关系。

我们将从一个简单的示例开始依赖管理。假设您正在构建一个名为SampleProject的项目,该项目依赖于第三方库log4j-1.2.16.jar

构建项目时,您需要在编译时使用这个 jar 文件。Gradle 提供了一种非常简单且系统化的方式来定义项目的依赖关系,即通过以下方式使用dependencies闭包:

dependencies {
  <configuration name> <dependencies>
}

Gradle 将依赖关系分组到不同的配置中。如果您将Java 插件应用到项目中,它将提供六个不同的配置,如下表所示:

名称 详情
compile 这里提到的依赖关系将在源代码(src/main/java)的编译过程中添加到类路径中
runtime 这里提到的依赖关系在执行源代码(src/main/java)时需要运行时
testCompile 这里提到的依赖关系将在测试代码(src/main/test)的编译过程中添加到类路径中
testRuntime 这里提到的依赖关系在执行测试代码(src/main/test)时需要运行时
archives 这用于告诉构建文件关于项目生成的工件
default 这包含在运行时使用的工件和依赖关系

要定义前面的依赖项,您需要将以下详细信息传递给 Gradle 的依赖项管理器:

  • JAR 文件组(或命名空间)

  • JAR 文件名

  • JAR 文件版本

  • 分类器(如果 JAR 有类似分类器的特定 JDK 版本)

依赖项可以以下列方式之一定义:

  • 单个依赖项:

    compile group: 'log4j', name: 'log4j', version: '1.2.16'
    
  • 作为 Arraylist 的依赖项:

    compile 'log4j:log4j:1.2.16','junit:junit:4.10'
    
  • 作为配置闭包的依赖项:

    compile ('log4j:log4j:1.2.16') ) {
        // extra configurations
    }
    
  • 以键值格式作为配置闭包的依赖项:

    compile (group:'log4j',name:'log4j',version:'1.2.16') {
        // extra configurations
    }
    

    提示

    在平面目录(本地或远程文件系统)的情况下,不需要依赖项组名。

要配置项目依赖项,您需要在 dependencies 闭包中提及所有库。因此,构建文件将看起来像这样:

apply plugin: 'java'
repositories {
  mavenCentral()
}
dependencies {
  compile group: 'log4j', name: 'log4j', version: '1.2.16'
}

不要与我们在示例中添加的 repositories 闭包混淆。我们将在下一节中讨论这个问题。

仓库

当我们说依赖项已识别和定义时,工作已经完成了一半。Gradle 将如何知道从哪里获取这些依赖项呢?这就是 repositories 概念的出现。Gradle 提供了 repositories 闭包来定义可以从哪里下载依赖项的仓库。您可以在项目中配置任意数量的仓库和任意类型的仓库。对于在 dependencies 闭包中列出的依赖项,Gradle 将按顺序搜索仓库。如果它在配置的仓库之一中找到一个库或依赖项(如果配置了多个仓库),它将跳过搜索其他仓库。在下一节中,我们将学习如何配置不同的仓库。

仓库配置

您可以使用以下方法来配置仓库。Gradle 允许您在构建文件中使用多个配置。

  • Maven Central 仓库:此配置用于直接从 Maven Central 仓库 下载您的依赖项。您不需要记住仓库 URL。您可以直接将 mavenCentral() 添加到此处提到的 repositories 闭包中:

    repositories {
      mavenCentral()
    }
    
  • Maven JCenter 仓库:Gradle 还通过在 repositories 中使用 jcenter() 直接连接到 jCenter 仓库。

    repositories {
      jcenter()
    }
    
  • Maven 本地仓库:可能存在一种情况,本地 Maven 缓存中包含所有必需的依赖项,并且您不想连接到 Maven 中央仓库。相反,您将需要使用 Maven 的本地缓存中的 JAR 文件。在这种情况下,您可以在 repositories 闭包中使用 mavenLocal()。默认情况下,Maven 的本地缓存路径将是 <USER_HOME>/.m2/repository。如果您想将其更改为另一个位置,您可以在 <USER_HOME>/.m2<USER_HOME>/.m2/conf 下的 settings.xml 中配置路径。拥有此配置可以轻松地在本地构建另一个项目的“SNAPSHOT”版本并将其包含在内。

    repositories {
      mavenLocal()
    }
    
  • Ivy 仓库:如果您想引用 Ivy 仓库,可以按以下方式定义:

    repositories {
      ivy {
        url "http://<ivyrepositorylocation>"
        layout "ivy"  // valid values are maven, gradle, ivy
      }
    }
    

    你还可以为你的 Ivy 仓库定义自定义布局。由于 Ivy 不允许本地发布如 Maven 这样的工件,因此没有等效的ivyLocal()

  • 组织仓库:无论有多少开源仓库,你都需要一个私有仓库来进行软件开发,因为你拥有这个仓库,并且私有仓库可以更好地跟踪和管理变更。要使用你组织的私有仓库,你可以按照以下格式配置Repositories位置:

    repositories {
      maven {
        url "http://private.repository/path"
        credentials {
          username 'guest'
          password '123123'
        }
      }
      ivy { // For Ivy repositories
      url "http://private.repository/path"
      }
    }
    

    如果你的私有仓库需要认证,你也可以提供凭证。你还可以将凭证添加到~/.gradle/gradle.properties中,并从那里使用它,因为将凭证直接添加到构建文件中并不是一个好的做法。

    对于 Maven 格式的仓库,jar 文件总是附带有pom.xml作为元数据。可能存在一种情况,即 POM 文件和 JAR 文件位于两个不同的位置。在这种情况下,你可以如下提及这两个位置:

    repositories {
      maven {
        url "http://private.repository/pompath"
        artifactUrls "http://private.repository/jardir"
      }
    }
    

    小贴士

    如果前面提到的 URL 包含 JAR 文件,Gradle 将从该位置下载 JAR 文件;否则,它将在artifactUrls中搜索。你可以提及多个artifactUrls

  • 扁平目录仓库:可能存在一种情况,当你引用本地文件系统中的仓库(不是mavenLocal()位置)时。这种情况可能发生在其他项目或团队在不同的位置创建 JAR 文件并将这些 JAR 文件发布到中央位置时。你希望你的项目仅将本地目录用于依赖项。这可以通过以下代码实现:

    repositories {
      flatDir {
      dirs '/localfile/dir1', '/localfile/dir2'
      }
    }
    

    这种方法并不推荐,因为这会导致不一致。推荐的方法是始终使用私有或全局仓库。

依赖项解析

我们已经看到了定义依赖项和仓库的标准方式,这可以帮助你快速入门这些概念。现在是时候深入了解了,了解如何自定义标准配置,以适应你的特定需求。

传递依赖

假设你的应用程序依赖于commons-httpclient-3.1.jar,这是一个一级依赖项。然而,这个 JAR 文件又依赖于以下其他 JAR 文件,commons-codec-1.2.jarcommons-logging-1.0.4.jar。如果我们尝试查找更多细节,commons-logging jar又依赖于其他一些 JAR 文件。

在这里,commons-httpclient-3.1是一个一级依赖项;前面提到的两个 JAR 文件是二级依赖项,依此类推。然而,使用 Gradle,你不需要管理所有这些级别的依赖项。想象一下,如果你必须在构建文件中找出并提及每个级别的依赖项,这将是非常繁琐且耗时的。如果你遇到一些版本冲突,这会变得更加痛苦。

使用 Gradle,你不需要担心任何此类依赖项相关的问题。Gradle 为依赖项管理提供完全自动化。你只需定义第一级依赖项,Gradle 就会处理所有传递性依赖项。默认情况下,它将下载所有传递性依赖项,直到最后一级。

排除传递性

对于某些场景,你可能不希望 Gradle 获取所有传递性依赖项。相反,你希望对构建文件中提到的库有完全的控制权,只下载这些库。要关闭传递性特性,你可以在构建文件 (build_transitive.gradle) 中设置传递性标志 off

apply plugin:'java'
repositories {
  mavenCentral()
}
dependencies {
  compile group:'commons-httpclient', name:'commons-httpclient', version:'3.1', transitive: false
}

清理 Gradle 缓存 (~/.gradle/caches) 并再次尝试构建项目。这次它将只下载一个 JAR 包,即 commons-httpclient-3.1.jar

$ gradle –b build_transitive.gradle build
…..
:compileJava
Download https://repo1.maven.org/maven2/commons-httpclient/commons-httpclient/3.1/commons-httpclient-3.1.pom
Download https://repo1.maven.org/maven2/commons-httpclient/commons-httpclient/3.1/commons-httpclient-3.1.jar
:processResources UP-TO-DATE
…….

如果你需要第二级依赖项的其他版本,或者第二级依赖项在仓库中缺失,而你希望手动复制它,这个特性可能很有用。

选择性排除

可能存在一种情况,当你想要部分使用传递性特性时,也就是说,你不想阻止 Gradle 获取传递性依赖项,但你又知道这可能会导致版本冲突。因此,你可能希望某些特定的 JAR 包在第二级或下一级依赖中被排除。为了从第二级开始选择性排除依赖项,你可以使用以下配置:

dependencies{
  compile('commons-httpclient:commons-httpclient:3.1') {
    exclude group:'commons-codec' // exclude by group
    // exclude group:'commons-codec',module:'commons-codec'
  }
}

小贴士

排除标准需要将组作为必填字段,但模块可以是可选的。

版本冲突

版本冲突是一个非常常见的场景,其中项目依赖于特定版本的 JAR 包。例如,你的项目依赖于 commons-httpclient-3.1 JAR 包和 commons-codec-1.1 JAR 包。commons-httpclient-3.1 JAR 包有一个对 commons-codec-1.2 JAR 包的传递性依赖。在构建过程中,Gradle 将找到对同一 JAR 包的两个不同版本的依赖。你的构建文件 (build_versionconflict.gradle) 将看起来像这样:

apply plugin:'java'

repositories {
  mavenCentral()
}
dependencies {
  compile group:'commons-httpclient', name:'commons-httpclient', version:'3.1'
  compile group:'commons-codec',name:'commons-codec', version:'1.1'
}

注意

由于版本冲突引起的问题甚至需要相当长的时间才能被发现。

Gradle 支持不同的策略来解决版本冲突场景,如下所示:

  • 最新版本:默认情况下,Gradle 在发现同一 JAR 文件的不同版本时会应用 获取最新版本 策略来解决版本冲突问题。在前面的场景中,它将跳过版本 1.1 并下载版本 1.2 的 commons-codec JAR 包。

    执行 gradle –b build_versionconflict.gradle clean build 命令后,输出将如下所示:

    Download https://repo1.maven.org/maven2/commons-codec/commons-codec/1.1/commons-codec-1.1.pom
    Download https://repo1.maven.org/maven2/commons-codec/commons-codec/1.2/commons-codec-1.2.pom
    ......
    Download https://repo1.maven.org/maven2/commons-codec/commons-codec/1.2/commons-codec-1.2.jar
    :processResources UP-TO-DATE
    ......
    
    BUILD SUCCESSFUL
    
    
  • 在冲突时失败:获取最新版本的策略并不总是有效。有时,你可能会希望构建失败以便进一步调查,而不是获取最新版本。要启用此功能,你可以通过添加以下闭包来应用 failOnVersionConflict() 配置:

    configurations.all {
    resolutionStrategy {
      failOnVersionConflict()
    }
    }
    

    你可以使用前面的配置更新你的构建文件。如果你想将此策略应用于所有构建,你可以将其添加到你的 init 脚本中。

  • 强制指定版本:在冲突情况下,另一种选择是,而不是使构建失败,你可以下载特定版本的 JAR。这可以通过使用 强制标志 实现:

    dependencies {
      compile group:'commons-httpclient', name:'commons-httpclient', version:'3.1'
      compile group:'commons-codec',name:'commons-codec', version:'1.1', force:true
    }
    

    现在,尝试执行 gradle -b build_versionconflict.gradle build 并观察输出:

    Download https://repo1.maven.org/maven2/commons-codec/commons-codec/1.1/commons-codec-1.1.pom
    Download https://repo1.maven.org/maven2/commons-codec/commons-codec/1.1/commons-codec-1.1.jar
    :processResources UP-TO-DATE
    :classes
    ….
    BUILD SUCCESSFUL
    
    

动态依赖项

要使构建对 JAR 版本更灵活,你可以使用 latest.integration 占位符,或者你可以定义一个版本范围,例如 1.+。使用此选项,你不必坚持特定的版本。使用 1.+2.+ 格式,它将固定主版本为 1 或 2(可以是任何数字),并选择次版本的最新版本(例如,1.9 或 2.9)。

compile group:'commons-codec',name:'commons-codec', version: '1.+'
compile group:'commons-codec',name:'commons-codec', version: 'latest.integration'

你可以使用其中任何一个来获取最新的依赖项。

依赖项定制

每当 Gradle 在仓库中搜索依赖项时,首先它会搜索一个模块描述符文件(例如,pom.xmlivy.xml)。Gradle 解析此文件并下载模块描述符中提到的实际 JAR 文件及其依赖项。可能存在一种情况,即没有模块描述符文件。在这种情况下,Gradle 直接查找 JAR 文件并下载它。

Gradle 允许你以不同的方式处理依赖项。不仅你可以下载其他文件格式,如 ZIP 和 WAR,如果需要,还可以指定不同的分类器。

下载非 JAR 文件

默认情况下,Gradle 下载具有 .jar 扩展名的文件。有时,你可能需要下载 ZIP 文件或 WAR 文件,这些文件没有模块描述符。在这种情况下,你可以明确指定文件的扩展名:

Dependencies {
  runtime group: 'org.mywar', name: 'sampleWeb', version: '1.0', ext: 'war'
}

带有分类器的文件依赖项

有时你可能会以特殊的标记(称为分类器)发布工件,例如 sampleWeb-1.0-dev.warsampleWeb-1.0-qa.jar。要下载带有分类器的工件,Gradle 提供了 classifier 标签:

dependencies {
  runtime group: 'org.mywar', name: 'sampleWeb', version: '1.0', classifier: 'qa', ext:'war'
}

替换传递依赖项

如果你不想下载现有的传递依赖项并想用你定制的传递依赖项替换它们,Gradle 提供了以下方法:

dependencies {
  compile module(group:'commons-httpclient', name:'commons-httpclient', version:'3.1') {
    dependencies "commons-codec:commons-codec:1.1@jar"
  }
}

在这里我们使用了 @jar,它可以替代前面示例中使用的 ext 标签。此代码片段将不会下载 commons-httpclient 的现有传递依赖项,但它将下载花括号内提到的 JAR 文件。

依赖项的定制配置

当我们应用 Java 插件时,Gradle 会自动为你提供一些默认配置,例如编译和运行时配置。我们可以扩展这个功能,并使用自己的配置来管理依赖项。这是一种将仅在构建时需要的依赖项分组以实现特定任务(如代码生成器(依赖于模板库)、xjc、cxf wsdl 转换为 Java 等)的绝佳方式。我们可以将这些依赖项分组在我们的用户定义配置下。在使用依赖项闭包下的自定义配置之前,我们需要在配置闭包内定义它。以下是从 build_customconf.gradle 文件中的代码片段:

apply plugin: 'java'
version=1.0
configurations {
  customDep
}
repositories {
  mavenCentral()
}
dependencies {
  customDep group: 'junit', name: 'junit', version: '4.11'
  compile group: 'log4j', name: 'log4j', version: '1.2.16'
}

task showCustomDep << {
  FileTree deps  = project.configurations.customDep.asFileTree
  deps.each {File file ->
    println "File names are "+file.name
  }
}

以下是从前面的代码中得到的输出:

$ gradle –b build_customconf.gradle showCustomDep
:showCustomDep
….
Download https://repo1.maven.org/maven2/junit/junit/4.11/junit-4.11.jar
Download https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar
File names are junit-4.11.jar
File names are hamcrest-core-1.3.jar

BUILD SUCCESSFUL

依赖项报告

Gradle 提供了一种非常方便的方式来列出从第一级到第 n 级的所有项目依赖项。它包括所有你的传递依赖项,包括手动更改、覆盖和强制依赖项。依赖项树按配置(如编译、测试编译等)分组。以下是从 build_depreport.gradle 文件中的代码片段:

apply plugin: 'java'
version=1.0
repositories {
  mavenCentral()
}
dependencies {
  compile group: 'log4j', name: 'log4j', version: '1.2.16'
  compile 'commons-httpclient:commons-httpclient:3.1'
  compile 'dom4j:dom4j:1.6.1'
}
$ gradle –b build_depreport.gradle dependencies
…
Root project
….

+--- log4j:log4j:1.2.16
+--- commons-httpclient:commons-httpclient:3.1
|    +--- commons-logging:commons-logging:1.0.4
|    \--- commons-codec:commons-codec:1.2
\--- dom4j:dom4j:1.6.1
 \--- xml-apis:xml-apis:1.0.b2

default – Configuration for default artifacts.
+--- log4j:log4j:1.2.16
+--- commons-httpclient:commons-httpclient:3.1
|    +--- commons-logging:commons-logging:1.0.4
|    \--- commons-codec:commons-codec:1.2
\--- dom4j:dom4j:1.6.1
 \--- xml-apis:xml-apis:1.0.b2

runtime – Runtime classpath for source set 'main'.
+--- log4j:log4j:1.2.16
+--- commons-httpclient:commons-httpclient:3.1
|    +--- commons-logging:commons-logging:1.0.4
|    \--- commons-codec:commons-codec:1.2
\--- dom4j:dom4j:1.6.1
 \--- xml-apis:xml-apis:1.0.b2

testCompile – Compile classpath for source set 'test'.
+--- log4j:log4j:1.2.16
+--- commons-httpclient:commons-httpclient:3.1
|    +--- commons-logging:commons-logging:1.0.4
|    \--- commons-codec:commons-codec:1.2
\--- dom4j:dom4j:1.6.1
 \--- xml-apis:xml-apis:1.0.b2

testRuntime – Runtime classpath for source set 'test'.
+--- log4j:log4j:1.2.16
+--- commons-httpclient:commons-httpclient:3.1
|    +--- commons-logging:commons-logging:1.0.4
|    \--- commons-codec:commons-codec:1.2
\--- dom4j:dom4j:1.6.1
 \--- xml-apis:xml-apis:1.0.b2

BUILD SUCCESSFUL

它将显示所有配置的所有依赖项的子级。你可能会惊讶地看到为什么其他配置(如运行时和测试运行时)也会显示,尽管只定义了编译配置。以下表格显示了不同配置之间的关系:

Dependency Extends
compile -
runtime compile
testCompile compile
testRuntime runtime, testCompile
default runtime

如果你只想列出某个配置的依赖项,可以使用 –configuration <配置名称> 来指定:

$ gradle –b build_depreport.gradle dependencies –configuration compile
:dependencies

Root project

compile – Compile classpath for source set 'main'.
+--- log4j:log4j:1.2.16
+--- commons-httpclient:commons-httpclient:3.1
|    +--- commons-logging:commons-logging:1.0.4
|    \--- commons-codec:commons-codec:1.2
\--- dom4j:dom4j:1.6.1
 \--- xml-apis:xml-apis:1.0.b2

BUILD SUCCESSFUL

依赖项特定细节

有时候,在下载某些传递依赖项时可能会遇到问题,而你不知道哪个依赖项正在下载那个 JAR 文件。

假设你在执行前面的 build_depreport.gradle 脚本时,在获取 commons-logging JAR 文件时遇到问题。它不是一级依赖项,你不知道哪个一级依赖项对此负责。要获取这些详细信息,请使用 dependencyInsight 命令:

$ gradle –b build_depreport.gradle dependencyInsight –dependency commons-logging –configuration runtime
:dependencyInsight
commons-logging:commons-logging:1.0.4
\--- commons-httpclient:commons-httpclient:3.1
 \--- runtime

BUILD SUCCESSFUL

如果你没有指定 –configuration 选项,它将默认应用 compile 配置。其他选项包括 runtimetestCompile 等,如前例所述。

发布工件

到目前为止,我们已经讨论了很多关于依赖项的内容。我们如何定义项目依赖项、自定义它们以及配置仓库以下载库。现在,让我们尝试构建工件(JAR、WAR 等)并将其发布到工件仓库(可能是本地文件系统、远程位置或 Maven 仓库),以便所有其他团队可以共享。

默认工件

当我们应用 Java 插件时,Gradle 会向项目添加一些默认配置,例如编译、运行时、测试编译。Java 插件还添加了一个额外的配置archive,用于定义你的项目工件。Gradle 通过一些插件提供默认工件。例如,Java、Groovy 插件将 JAR 作为默认工件发布,war 插件将 WAR 作为默认工件。这个 JAR 可以使用uploadArchives任务上传或发布到仓库。

以下代码片段展示了如何通过build_uploadarchives.gradle文件配置仓库以上传归档:

apply plugin: 'java'
version=1.0

repositories {
  mavenCentral()
}
dependencies {
  compile group: 'log4j', name: 'log4j', version: '1.2.16'
  compile 'commons-httpclient:commons-httpclient:3.1'
  compile 'dom4j:dom4j:1.6.1'
}
uploadArchives {
  repositories {
    maven {
      credentials {
        username "guest"
        password "guest"
      }
      url "http://private.maven.repo"
    }
    //flatDir {dirs "./temp1" }
  }
}

我们可以使用平面目录作为仓库,而不是 Maven 仓库。在先前的示例中,将 Maven 闭包替换为flatDir {dirs "./temp1" }配置。现在,如果你执行gradle uploadArchives命令,你将在temp1目录中找到已发布的 JAR 文件。

自定义工件

对于每个配置,Gradle 默认提供Upload<配置名称>,它将组装并上传指定配置中的工件。Java 插件提供的UploadArchives任务将默认工件(jar)上传到仓库。

有时,你可能需要与 JAR 文件一起生成一些额外的工件,如 ZIP 和 XML 文件。这可以通过归档任务来定义工件。

自定义工件

图 5.1

在前面的图中,assemble任务依赖于jar任务,这仅仅是你的 Java 插件项目的默认工件。你可以使用archives配置来配置额外的工件。归档配置的输入可以是一个工件本身或创建工件的任务。

让我们看看以下两个示例:

与你的 JAR 文件一起生成额外的 XML 文件

在本例中,我们将生成一个额外的 XML 文件与 JAR 文件一起,并将其上传到仓库。以下是CustomArtifact/build.gradle文件的内容:

apply plugin: 'java'
archivesBaseName="MySample" // to customize Jar Name
version=1.0
repositories {
  mavenCentral()
}
def confFile = file('configurations.xml') // artifact2
artifacts {
  archives confFile
}
uploadArchives {
repositories {
   flatDir {dirs "./tempRepo"}
}
}

这里,我们已将configurations.xml作为一个单独的 XML 文件添加到归档中,这样我们就可以将文件与 JAR 文件一起上传到仓库。

执行 Gradle 的uploadArchives命令后,你将在tempRepo目录中找到以下文件:

与你的 JAR 文件一起生成额外的 XML 文件

图 5.2

Gradle 还会生成校验和以及部署描述符(这里为ivy-1.0.xml),与工件一起生成。

在以下部分,我们将学习如何上传 ZIP 文件作为工件。

与你的 JAR 文件一起生成额外的 ZIP 文件

如果你想要上传一个额外的 ZIP 文件与 JAR 文件一起,你可以在artifacts闭包中提及额外的归档。以下是CustomArtifact/build_zip.gradle文件:

apply plugin: 'java'
archivesBaseName="MySample" // to customize Jar Name
version=1.0
repositories {
  mavenCentral()
}
task zipSrc(type: Zip) {
  from 'src'
}
artifacts {
  archives zipSrc
}
uploadArchives {
  repositories {
    flatDir {dirs "./temp1" }
  }
}

执行gradle -b build_zip.gradle uploadArchives命令后,验证temp1目录中的文件:

生成与 JAR 文件一起的附加 ZIP 文件

图 5.3

这里,除了 JAR 文件外,还生成了一个额外的 MySample-1.0.zip 文件。您可能已经注意到,我们没有对 zipSrc 任务进行任何额外的调用,这是创建 ZIP 文件所必需的。在这里,Gradle 采用了一种声明式方法。无论您在 artifacts 闭包中配置了哪些存档,Gradle 都会创建这些存档。在此闭包内,您可以分配不同类型的任务,例如 JAR、ZIP、TAR (org.gradle.api.tasks.building.AbstractArchiveTask) 或任何要存档的文件。

自定义配置

与自定义依赖项一样,您也可以为您的存档定义自定义配置。考虑以下示例 (CustomArtifacts/build_customconf.gradle):

apply plugin: 'java'

archivesBaseName="MySampleZip" // to customize Jar Name
version=1.0
configurations {
  zipAsset
}
repositories {
  mavenCentral()
}
task zipSrc(type: Zip) {
  from 'src'
}
artifacts {
  zipAsset zipSrc
}
uploadZipAsset {
  repositories {
    flatDir {dirs "./temp1" }
  }
}

现在,执行 gradle –b build_customconf.gradle uploadZipAsset 命令以创建和上传文件到仓库。在示例中,我们定义了一个自定义配置 zipAsset。我们在 artifacts 闭包内部使用了该配置。如前例所述,Gradle 自动为每个配置提供 upload<configname> 任务。因此,我们有 cuploadZipAsset 任务可用于上传所需的 ZIP 文件到仓库。

Maven 发布插件

在上一节中,我们讨论了 Maven 插件和其他仓库配置。在这里,我们将讨论 Gradle 引入的新插件(maven-publish plugin)。

为了对发布过程有更多的控制,Gradle 提供了 'maven-publish' 插件。通过以下示例,您将看到它如何帮助我们使用 MavenPublish/build.gradle 文件进行发布:

您可以使用以下闭包来配置发布:

publishing {
  publications {
    customPublicationName(MavenPublication) {
      // Configure the publication here
    }
  }
}

以下文件是 MavenPublish/build.gradle

apply plugin: 'java'
apply plugin: 'maven-publish'

publishing {
  publications {
    mavenJava(MavenPublication) {
      from components.java
      groupId 'org.mygroup'
      artifactId 'MySampleProj'
      version '1.0'
    }
  }

}

此插件添加以下任务:

  • publish: 这会发布此项目产生的所有发布物

  • publishToMavenLocal: 这会将此项目产生的所有 Maven 发布物发布到本地 Maven 缓存

当您在 publishing 内部添加前面提到的 publications 闭包时,它将添加两个额外的任务,generatePomFileFor<publicationName>Publicationpublic<publicationName>PublicationToMavenLocal。您可以在任务列表中找到以下附加任务:

  • generatePomFileForPluginPublication: 这为发布 'plugin' 生成 Maven POM 文件

  • publishPluginPublicationToMavenLocal: 这会将 Maven 发布的 'plugin' 发布到本地 Maven 仓库

要在本地 Maven 仓库中发布存档,请执行以下命令:

$ gradle –i publishToMavenLocal

:publishMavenJavaPublicationToMavenLocal
Executing task ': publishMavenJavaPublicationToMavenLocal' (up-to-date check took 0.001 secs) due to:
 Task has not declared any outputs.
Publishing to repository org.gradle.api.internal.artifacts.repositories.DefaultMavenLocalArtifactRepository_Decorated@4a454218
[INFO] Installing /Chapter5/sent/MavenPublish/build/libs/MavenPublish.jar to <%USER_HOME>/.m2/repository/org/mygroup/MySampleProj/1.0/MySampleProj-1.0.jar
: publishMavenJavaPublicationToMavenLocal (Thread[main,5,main]) completed. Took 1.079 secs.

BUILD SUCCESSFUL

如果您浏览本地 Maven 仓库,您也会发现 POM 文件具有以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" 
>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.mygroup</groupId>
  <artifactId>MySampleProj</artifactId>
  <version>1.0</version>
</project>

默认情况下,它为 Java 项目生成 JAR 文件。如果您想与 JAR 文件一起添加额外的存档,您可以通过在以下格式中添加额外的存档声明来自定义前面的配置。

下面是MavenPublish/build_zip.gradle的示例代码:

apply plugin: 'java'
apply plugin: 'maven-publish'

task zipSrc(type: Zip) {
  baseName = 'SampleSource'
  from 'src'
}

publishing {
  publications {
    mavenJava(MavenPublication) {
      from components.java
      groupId 'org.mygroup'
      artifactId 'MySampleProj'
      version '1.0'

      artifact zipSrc {
        classifier "sources"
      }
      // artifact can be <Jar,Zip tasks which will generate jar,zip file>

    }
  }
}
$ gradle –b build_zip.gradle – i publishToMavenLocal
. . .
Publishing to repository org.gradle.api.internal.artifacts.repositories.DefaultMavenLocalArtifactRepository_Decorated@434d54de
[INFO] Installing /Chapter5/MavenPublish/build/libs/MavenPublish.jar to <USER_HOME>/.m2/repository/org/mygroup/MySampleProj/1.0/MySampleProj-1.0.jar
[INFO] Installing /Chapter5/MavenPublish/build/distributions/SampleSource-source-.zip to <USER_HOME> /.m2/repository/org/mygroup/MySampleProj/1.0/MySampleProj-1.0-source-.zip
:publishPluginPublicationToMavenLocal (Thread[main,5,main]) completed. Took 0.85 secs.
. . .

BUILD SUCCESSFUL

现在,在本地仓库中,除了 JAR 文件外,你还会找到一个额外的 ZIP 文件。

注意

请记住,对于你发布的每个额外工件,你都需要提及一个分类器。Gradle 只允许一个没有分类器的工件。

发布到本地仓库

要使用maven-publish插件将工件发布到本地仓库,我们可以使用与在 Maven 插件中讨论的相同配置。仓库的闭包与之前完全相同,但它必须被一个发布闭包所包围。你可以按照以下方式定义闭包:

Publishing {
  repositories {
    maven {
      name "localRepo"
      url "http://local.maven.repo"
      credentials { // if required
        username  = 'username'
        password = 'password'
      }
    }
  }
}

你甚至可以通过指定 URL ./localrepo 将工件发布到本地文件仓库。Maven 将自动为你创建目录结构,你可以在localrepo/<group>/<version>/artifact-<version>.jar下找到工件。

如果你提到maven { ….}闭包中的名称属性,Gradle 将自动创建一个名为publishPluginPublicationTo<name>Repository的新任务:

publishing {
  repositories {
    maven {
      name "localRepo"
      url "./localrepo"
    }
  }
}

现在,你将能够使用publishMavenJavaPublicationToLocalRepoRepository任务或简单地使用publish任务来发布到仓库;例如gradle -b build_localrepo.gradle publish

自定义 POM

默认情况下,Gradle 使用默认参数为工件生成 POM 文件。如果你想使用pom.withXml闭包修改 POM 文件以包含额外的详细信息,你可以这样做。你可以在 XML 文件中添加任意数量的新节点,也可以更新一些现有细节。请记住,groupIdartifactIdversion是只读的。你不能修改这些细节。考虑文件build_custompom.gradle

apply plugin: 'java'
apply plugin: 'maven-publish'
publishing {
  publications {
    mavenCustom(MavenPublication) {
      from components.java
      groupId 'org.mygroup'
      artifactId 'MySampleProj'
      version '1.0'

    pom.withXml {
      def root = asNode()
      root.appendNode('name', 'Sample Project')
      root.appendNode('description', 'Adding Additional details')
      def devs = root.appendNode('developers')
      def dev = devs.appendNode('developer')
      dev.appendNode('name', 'DeveloperName')
      }
    }
  }
}

现在,执行publishToMavenLocal任务,你将在仓库中找到生成的pom.xml文件。

摘要

本章介绍了 Gradle 提供的依赖管理细节。我们探讨了依赖配置、依赖解析中涉及的战略以及配置传递依赖。我们还学习了 Gradle 提供的不同版本的冲突策略,以及我们如何配置它以获得最大利益。

我们还讨论了仓库。我们介绍了如何使用不同的仓库,例如平面文件、本地Maven 仓库和托管在 HTTPS 服务器上的远程仓库。最后,我们讨论了项目的发布。通过使用不同的插件,你可以将工件发布到中央位置,如本地或远程Maven 仓库。我们还讨论了如何利用新的 maven-publish 插件以及如何配置它,以便它符合我们的要求。

在下一章中,我们将讨论一些重要的插件,如 War 和 Scala。我们还将讨论其他重要概念,例如文件管理、多项目和属性管理。

第六章. 使用 Gradle

本章涵盖了更多插件,如 WarScala,这些插件在构建 Web 应用程序和 Scala 应用程序时将非常有用。此外,我们还将讨论诸如 属性管理多项目构建日志 等多个主题。在 多项目构建 部分,我们将讨论 Gradle 如何通过根项目的构建文件支持多项目构建。它还提供了将每个模块视为独立项目的同时,将所有模块作为一个单一项目来处理的灵活性。在本章的最后部分,我们将学习使用 Gradle 的自动化测试方面。你将学习如何使用不同的配置执行单元测试。在本节中,我们将通过两个常用测试框架 JUnit 和 TestNG 的示例来了解测试概念。

War 插件

War 插件用于构建 Web 项目,并且像任何其他插件一样,可以通过在构建文件中添加以下行来添加:

apply plugin: 'war'

War 插件扩展了 Java 插件,并有助于创建 war 归档。war 插件会自动将 Java 插件应用于构建文件。在构建过程中,插件创建一个 war 文件而不是 jar 文件。War 插件禁用了 Java 插件的 jar 任务,并添加了一个默认的 war 归档任务。默认情况下,war 文件的内容将是来自 src/main/java 的编译类;来自 src/main/webapp 的内容以及所有运行时依赖项。内容可以通过使用 war 闭包进行自定义。

在我们的示例中,我们创建了一个简单的 servlet 文件来显示当前日期和时间,一个 web.xml 文件和一个 build.gradle 文件。项目结构在以下屏幕截图中进行展示:

War 插件

图 6.1

SimpleWebApp/build.gradle 文件的内容如下:

apply plugin: 'war'

repositories {
  mavenCentral()
}

dependencies {
  providedCompile "javax.servlet:servlet-api:2.5"
  compile("commons-io:commons-io:2.4")
  compile 'javax.inject:javax.inject:1'
}

war 插件在 Java 插件之上添加了 providedCompileprovidedRuntime 依赖配置。providedCompileprovidedRuntime 配置的范围分别与 compileruntime 相同,但唯一的不同是这些配置中定义的库将不会成为 war 归档的一部分。在我们的示例中,我们已将 servlet-api 定义为 providedCompile 时间的依赖项。因此,这个库不包括在 war 文件的 WEB-INF/lib/ 文件夹中。这是因为这个库由如 Tomcat 这样的 Servlet 容器提供。因此,当我们在一个容器中部署应用程序时,它将由容器添加。你可以通过以下方式展开 war 文件来确认这一点:

SimpleWebApp$ jar -tvf build/libs/SimpleWebApp.war
 0 Mon Mar 16 17:56:04 IST 2015 META-INF/
 25 Mon Mar 16 17:56:04 IST 2015 META-INF/MANIFEST.MF
 0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/
 0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/classes/
 0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/classes/ch6/
1148 Mon Mar 16 17:56:04 IST 2015 WEB-INF/classes/ch6/DateTimeServlet.class
 0 Mon Mar 16 17:56:04 IST 2015 WEB-INF/lib/
185140 Mon Mar 16 12:32:50 IST 2015 WEB-INF/lib/commons-io-2.4.jar
 2497 Mon Mar 16 13:49:32 IST 2015 WEB-INF/lib/javax.inject-1.jar
 578 Mon Mar 16 16:45:16 IST 2015 WEB-INF/web.xml

有时,我们可能还需要自定义项目的结构。例如,webapp 文件夹可能位于根项目文件夹下,而不是在 src 文件夹中。webapp 文件夹还可以包含新的文件夹,如 confresource,用于存储属性文件、Java 脚本、图像和其他资产。我们可能希望将 webapp 文件夹重命名为 WebContent。建议的目录结构可能如下所示:

The War plugin

图 6.2

我们可能还希望创建一个具有自定义名称和版本的 war 文件。此外,我们可能不想将任何空文件夹,如 imagesjs,复制到 war 文件中。

要实现这些新更改,请按照此处所述,将附加属性添加到 build.gradle 文件中。webAppDirName 属性将新的 webapp 文件夹位置设置为 WebContent 文件夹。war 闭包定义了版本和名称等属性,并将 includeEmptyDirs 选项设置为 false。默认情况下,includeEmptyDirs 被设置为 true。这意味着 webapp 目录中的任何空文件夹都将被复制到 war 文件中。通过将其设置为 false,空文件夹如 imagesjs 将不会被复制到 war 文件中。

以下将是 CustomWebApp/build.gradle 的内容:

apply plugin: 'war'

repositories {
  mavenCentral()
}
dependencies {
  providedCompile "javax.servlet:servlet-api:2.5"
  compile("commons-io:commons-io:2.4")
  compile 'javax.inject:javax.inject:1'
}
webAppDirName="WebContent"

war{
  baseName = "simpleapp"
  version = "1.0"
  extension = "war"
  includeEmptyDirs = false
}

构建成功后,将创建名为 simpleapp-1.0.warwar 文件。执行 jar -tvf build/libs/simpleapp-1.0.war 命令并验证 war 文件的内容。你会发现 conf 文件夹被添加到了 war 文件中,而 imagesjs 文件夹则没有被包含。

你可能也会对用于网络应用程序部署的 Jetty 插件感兴趣,该插件允许你在嵌入式容器中部署网络应用程序。此插件会自动将 War 插件应用到项目中。Jetty 插件定义了三个任务:jettyRunjettyRunWarjettyStop。任务 jettyRun 在嵌入式 Jetty 网络容器中运行网络应用程序,而 jettyRunWar 任务帮助构建 war 文件,并在嵌入式网络容器中运行它。任务 jettyStop 停止容器实例。关于 War 配置的更多内容超出了本书的范围,因此,更多信息请参考 Gradle API 文档。以下是链接:docs.gradle.org/current/userguide/war_plugin.html

Scala 插件

Scala 插件可以帮助你构建 Scala 应用程序。像任何其他插件一样,Scala 插件可以通过添加以下行应用到构建文件中:

apply plugin: 'scala'

Scala 插件也扩展了 Java 插件并添加了一些更多任务,如 compileScalacompileTestScalascaladoc,以处理 Scala 文件。任务名称几乎都是根据它们的 Java 等价物命名的,只需将 java 部分替换为 scala。Scala 项目的目录结构也与 Java 项目的结构类似,其中生产代码通常写在 src/main/scala 目录下,测试代码保存在 src/test/scala 目录下。图 6.3 展示了 Scala 项目的目录结构。您还可以从目录结构中观察到,Scala 项目可以包含 Java 和 Scala 源文件的混合。HelloScala.scala 文件的内容如下。控制台输出为 Hello, Scala...。这是一个非常基础的代码,我们无法对 Scala 编程语言进行太多详细讨论。我们请求读者参考可在 www.scala-lang.org/ 找到的 Scala 语言文档。

package ch6

object HelloScala {
    def main(args: Array[String]) {
      println("Hello, Scala...")
    }
}

为了支持 Scala 源代码的编译,应在依赖配置中添加 Scala 库:

dependencies {
  compile('org.scala-lang:scala-library:2.11.6')
}

Scala 插件

图 6.3

如前所述,Scala 插件扩展了 Java 插件并添加了一些新任务。例如,compileScala 任务依赖于 compileJava 任务,而 compileTestScala 任务依赖于 compileTestJava 任务。这可以通过执行 classestestClasses 任务并查看输出轻松理解。

|

$ gradle classes
:compileJava
:compileScala
:processResources UP-TO-DATE
:classes

BUILD SUCCESSFUL

|

$ gradle testClasses
:compileJava UP-TO-DATE
:compileScala UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:compileTestScala UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE

BUILD SUCCESSFUL

|

Scala 项目也被打包成 jar 文件。jar 任务或 assemble 任务在 build/libs 目录下创建一个 jar 文件。

$ jar -tvf build/libs/ScalaApplication-1.0.jar
0 Thu Mar 26 23:49:04 IST 2015 META-INF/
94 Thu Mar 26 23:49:04 IST 2015 META-INF/MANIFEST.MF
0 Thu Mar 26 23:49:04 IST 2015 ch6/
1194 Thu Mar 26 23:48:58 IST 2015 ch6/Customer.class
609 Thu Mar 26 23:49:04 IST 2015 ch6/HelloScala$.class
594 Thu Mar 26 23:49:04 IST 2015 ch6/HelloScala.class
1375 Thu Mar 26 23:48:58 IST 2015 ch6/Order.class

Scala 插件不会向 Java 插件添加任何额外的约定。因此,Java 插件中定义的约定,如 lib 目录和 report 目录,可以在 Scala 插件中重用。Scala 插件仅添加了一些 sourceSet 属性,如 allScalascala.srcDirsscala,以处理源集。以下任务示例显示了 Scala 插件可用的不同属性。此示例类似于我们在第四章第四章。插件管理中创建的约定示例任务。

以下是从 ScalaApplication/build.gradle 文件中的代码片段:

apply plugin: 'java'
apply plugin: 'scala'
apply plugin: 'eclipse'

version = '1.0'

jar {
  manifest {
  attributes 'Implementation-Title': 'ScalaApplication', 'Implementation-Version': version
  }
}

repositories {
  mavenCentral()
}

dependencies {
  compile('org.scala-lang:scala-library:2.11.6')
  runtime('org.scala-lang:scala-compiler:2.11.6')
  compile('org.scala-lang:jline:2.9.0-1')
}

task displayScalaPluginConvention << {
  println "Lib Directory: $libsDir"
  println "Lib Directory Name: $libsDirName"
  println "Reports Directory: $reportsDir"
  println "Test Result Directory: $testResultsDir"

  println "Source Code in two sourcesets: $sourceSets"
  println "Production Code: ${sourceSets.main.java.srcDirs}, ${sourceSets.main.scala.srcDirs}"
  println "Test Code: ${sourceSets.test.java.srcDirs}, ${sourceSets.test.scala.srcDirs}"
  println "Production code output: ${sourceSets.main.output.classesDir} & ${sourceSets.main.output.resourcesDir}"
  println "Test code output: ${sourceSets.test.output.classesDir} & ${sourceSets.test.output.resourcesDir}"
}

任务 displayScalaPluginConvention 的输出如下所示:

$ gradle displayScalaPluginConvention
…
:displayScalaPluginConvention
Lib Directory: <path>/ build/libs
Lib Directory Name: libs
Reports Directory: <path>/build/reports
Test Result Directory: <path>/build/test-results
Source Code in two sourcesets: [source set 'main', source set 'test']
Production Code: [<path>/src/main/java], [<path>/src/main/scala]
Test Code: [<path>/src/test/java], [<path>/src/test/scala]
Production code output: <path>/build/classes/main & <path>/build/resources/main
Test code output: <path>/build/classes/test & <path>/build/resources/test

BUILD SUCCESSFUL

最后,我们将通过讨论如何从 Gradle 执行 Scala 应用程序来结束本节;我们可以在构建文件中创建一个简单的任务,如下所示。

task runMain(type: JavaExec){
  main = 'ch6.HelloScala'
  classpath = configurations.runtime + sourceSets.main.output + sourceSets.test.output
}

HelloScala 源文件有一个主方法,它在控制台打印 Hello, Scala...runMain 任务执行主方法并在控制台显示输出:

$ gradle runMain
....
:runMain
Hello, Scala...

BUILD SUCCESSFUL

日志记录

到目前为止,我们在构建脚本中到处使用 println 来向用户显示消息。如果您来自 Java 背景,您知道 println 语句不是向用户提供信息的正确方式。您需要日志记录。日志记录帮助用户对显示在不同级别的消息进行分类。这些不同的级别帮助用户根据情况打印正确的消息。例如,当用户想要对您的软件进行完整的详细跟踪时,他们可以使用调试级别。同样,每当用户在执行任务时想要非常有限的有用信息时,他们可以使用安静或信息级别。Gradle 提供以下不同类型的日志记录:

日志级别 描述
ERROR 这用于显示错误信息
QUIET 这用于显示有限的有用信息
WARNING 这用于显示警告信息
LIFECYCLE 这用于显示进度(默认级别)
INFO 这用于显示信息消息
DEBUG 这用于显示调试信息(所有日志)

默认情况下,Gradle 的日志级别是 LIFECYCLE。以下是从 LogExample/build.gradle 的代码片段:

task showLogging << {
  println "This is println example"
  logger.error "This is error message"
  logger.quiet "This is quiet message"
  logger.warn "This is WARNING message"
  logger.lifecycle "This is LIFECYCLE message"
  logger.info "This is INFO message"
  logger.debug "This is DEBUG message"
}

现在,执行以下命令:

$ gradle showLogging

:showLogging
This is println example
This is error message
This is quiet message
This is WARNING message
This is LIFECYCLE message

BUILD SUCCESSFUL

这里,Gradle 打印了所有生命周期级别的日志语句(包括生命周期),这是 Gradle 的默认日志级别。您也可以从命令行控制日志级别。

-q 这将显示直到安静级别的日志。它将包括错误和安静信息
-i 这将显示直到信息级别的日志。它将包括错误、安静、警告、生命周期和信息消息。
-s 这将打印出所有异常的堆栈跟踪。
-d 这将打印出所有日志和调试信息。这是最表达性的日志级别,它还将打印所有细节。

现在,执行 gradle showLogging -q

This is println example
This is error message
This is quiet message

除了常规的生命周期之外,Gradle 还提供了一个额外的选项,在发生任何异常时提供堆栈跟踪。堆栈跟踪与调试不同。在发生任何失败的情况下,它允许跟踪所有嵌套函数,这些函数按顺序调用,直到生成堆栈跟踪的点。

为了验证,在先前的任务中添加 assert 语句并执行以下操作:

task showLogging << {
println "This is println example"
..
assert 1==2
}
$ gradle showLogging -s
……
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':showLogging'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
 at
….
org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
 at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
 at org.gradle.api.internal.AbstractTask.executeWithoutThrowingTaskFailure(AbstractTask.java:305)
...

使用 stracktrace,Gradle 还提供了两个选项:

  • -s--stracktrace:这将打印截断的堆栈跟踪

  • -S--full-stracktrace:这将打印完整的堆栈跟踪

文件管理

任何构建工具的关键特性之一是 I/O 操作以及你执行这些 I/O 操作(如读取文件、写入文件和目录相关操作)的容易程度。具有 Ant 或 Maven 背景的开发者知道在旧构建工具中处理文件和目录操作是多么痛苦和复杂;有时你不得不编写自定义任务和插件来执行这些类型的操作,因为 Ant 和 Maven 中的 XML 限制。由于 Gradle 使用 Groovy,它将使你在处理文件和目录相关操作时生活变得更加容易。

读取文件

Gradle 提供了简单的方式来读取文件。你只需要使用文件 API(应用程序编程接口),它提供了处理文件所需的一切。以下是从 FileExample/build.gradle 的代码片段:

task showFile << {
  File file1 = file("readme.txt")
  println file1    // will print name of the file
  file1.eachLine {
    println it  // will print contents line by line
  }
}

要读取文件,我们使用了 file(<文件名>)。这是 Gradle 引用文件的默认方式,因为 Gradle 由于文件的绝对和相对引用而添加了一些路径行为 ($PROJECT_PATH/<filename>)。在这里,第一个 println 语句将打印文件名,即 readme.txt。为了读取文件,Groovy 为 File API 提供了 eachLine 方法,该方法逐行读取文件的所有行。

要访问目录,你可以使用以下文件 API:

def dir1 = new File("src")
println "Checking directory "+dir1.isFile() // will return false for directory
println "Checking directory "+dir1.isDirectory() // will return true for directory

写入文件

要写入文件,你可以使用 append 方法将内容添加到文件末尾,或者使用 setTextwrite 方法覆盖文件:

task fileWrite << {
  File file1 = file ("readme.txt")

  // will append data at the end
  file1.append("\nAdding new line. \n")

  // will overwrite contents
  file1.setText("Overwriting existing contents")

  // will overwrite contents
  file1.write("Using write method")
}

创建文件/目录

你可以通过向其中写入一些文本来创建一个新文件:

task createFile << {
  File file1 = new File("newFile.txt")
  file1.write("Using write method")
}

通过向文件中写入一些数据,Groovy 将自动创建文件(如果不存在的话)。

要向文件写入内容,你也可以使用左移运算符 (<<),它将在文件末尾追加数据:

file1 << "New content"

如果你想要创建一个空文件,你可以使用 createNewFile() 方法创建一个新文件。

task createNewFile << {
  File file1 = new File("createNewFileMethod.txt")
  file1.createNewFile()
}

可以使用 mkdir 命令创建一个新的目录。Gradle 还允许你使用 mkdirs 命令在一个命令中创建嵌套目录:

task createDir << {
  def dir1 = new File("folder1")
  dir1.mkdir()

  def dir2 = new File("folder2")
  dir2.createTempDir()

  def dir3 = new File("folder3/subfolder31")
  dir3.mkdirs() // to create sub directories in one command
}

在前面的例子中,我们创建了两个目录,一个使用 mkdir(),另一个使用 createTempDir()。区别在于当我们使用 createTempDir() 创建目录时,该目录会在构建脚本执行完成后自动删除。

文件操作

我们将看到一些在处理文件时经常使用的示例,这将有助于你在构建自动化中。

task fileOperations << {
  File file1 = new File("readme.txt")
  println "File size is "+file1.size()
  println "Checking existence "+file1.exists()
  println "Reading contents "+file1.getText()
  println "Checking directory "+file1.isDirectory()
  println "File length "+file1.length()
  println "Hidden file "+file1.isHidden()

  // File paths
  println "File path is "+file1.path
  println "File absolute path is "+file1.absolutePath
  println "File canonical path is "+file1.canonicalPath

// Rename file
file1.renameTo("writeme.txt")

// File Permissions
file1.setReadOnly()
println "Checking read permission "+ file1.canRead()+" write permission "+file1.canWrite()
file1.setWritable(true)
println "Checking read permission "+ file1.canRead()+" write permission "+file1.canWrite()

}

大多数前面的方法都是不言自明的。尝试执行前面的任务并观察输出。如果你尝试执行 fileOperations 任务两次,你会得到异常 readme.txt (No such file or directory),因为你已经将文件重命名为 writeme.txt

过滤文件

某些文件方法允许用户传递一个正则表达式作为参数。正则表达式可以用来过滤出所需的数据,而不是获取所有数据。以下是一个eachFileMatch()方法的示例,它将仅列出目录中的 Groovy 文件:

task filterFiles << {
  def dir1 = new File("dir1")
  dir1.eachFileMatch(~/.*.groovy/) {
    println it
  }
  dir1.eachFileRecurse { dir ->
    if(dir.isDirectory()) {
      dir.eachFileMatch(~/.*.groovy/) {
        println it
      }
    }
  }
}

输出如下:

$ gradle filterFiles

:filterFiles
dir1\groovySample.groovy
dir1\subdir1\groovySample1.groovy
dir1\subdir2\groovySample2.groovy
dir1\subdir2\subDir3\groovySample3.groovy

BUILD SUCCESSFUL

删除文件和目录

Gradle 提供了delete()deleteDir() API 来分别删除文件和目录:

task deleteFile << {
  def dir2 = new File("dir2")
  def file1 = new File("abc.txt")
  file1.createNewFile()
  dir2.mkdir()
  println "File path is "+file1.absolutePath
  println "Dir path is "+dir2.absolutePath
  file1.delete()
  dir2.deleteDir()
  println "Checking file(abc.txt) existence: "+file1.exists()+" and Directory(dir2) existence: "+dir2.exists()
}

输出如下:

$ gradle deleteFile
:deleteFile
File path is Chapter6/FileExample/abc.txt
Dir path is Chapter6/FileExample/dir2
Checking file(abc.txt) existence:  false and Directory(dir2) existence:  false

BUILD SUCCESSFUL

前面的任务将创建一个目录dir2和一个文件abc.txt。然后它将打印绝对路径,最后删除它们。您可以通过调用exists()函数来验证是否已正确删除。

文件树

到目前为止,我们已处理了单个文件操作。Gradle 提供了许多用户友好的 API 来处理文件集合。其中一个这样的 API 是FileTree。FileTree 表示文件或目录的层次结构。它扩展了FileCollection接口。Gradle 中的多个对象,如sourceSets,实现了FileTree接口。您可以使用fileTree()方法初始化 FileTree。以下是可以初始化fileTree方法的不同方式:

task fileTreeSample << {
  FileTree fTree = fileTree('dir1')
  fTree.each {
    println it.name
  }
  FileTree fTree1 = fileTree('dir1') {
    include '**/*.groovy'
  }
  println ""
  fTree1.each {
    println it.name
  }
  println ""
FileTree fTree2 = fileTree(dir:'dir1',excludes:['**/*.groovy'])
  fTree2.each {
    println it.absolutePath
  }
}

执行gradle fileTreeSample命令并观察输出。第一次迭代将打印dir1中的所有文件。第二次迭代将仅包括 Groovy 文件(扩展名为.groovy)。第三次迭代将排除 Groovy 文件(扩展名为.groovy)并打印其他带有绝对路径的文件。

您还可以使用文件树来读取诸如 ZIP、JAR 或 TAR 等存档文件的内容:

FileTree jarFile = zipTree('SampleProject-1.0.jar')
jarFile.each {
  println it.name
}

上述代码片段将列出jar文件中包含的所有文件。

属性管理

我们无法在不进行动态配置的情况下在不同的操作系统或不同的环境中提供软件。配置软件的一种方法是通过使用属性文件或环境属性。以下列出了 Gradle 提供配置属性到build.gradle的不同方式:

  • ext闭包

  • gradle.properties

  • 命令行

  • 自定义属性文件

ext 闭包

我们在第三章“管理任务”中看到了许多示例,使用ext闭包向项目中添加自定义属性。因此,我们不会在本章中讨论此主题。

gradle.properties

Gradle 提供了一个默认机制,使用gradle.properties来读取属性文件。您可以将gradle.properties文件添加到以下任何位置:

  • <USER_HOME>/.gradle:在此目录下定义的 gradle.properties 可供所有项目访问。您可以使用此文件定义全局属性,并且可以使用 $project.<propertyname> 访问这些属性。如果您已将 GRADLE_USER_HOME 定义为其他目录,则 Gradle 将跳过 <USER_HOME>/.gradle 目录,并从 GRADLE_USER_HOME 目录读取 gradle.properties。默认情况下,<USER_HOME>/.gradle 将被视为读取 gradle.properties 文件。如果 <USER_HOME>/.gradle/gradle.properties 中定义了属性,但用户未设置,则会导致异常。如果不希望出现这种情况,应使用 projecthasProperty 方法检查这些属性,如果没有设置,则应使用默认值初始化。此属性文件也可用于存储密码。

  • <ProjectDir>:在此目录下定义的 gradle.properties 可供当前项目访问。您无法从任何其他项目访问这些属性。因此,所有项目特定的属性都可以在项目的 gradle.properties 文件中定义。

    除了项目级别的属性外,您还可以在 gradle.properties 文件中定义系统级别的属性。要定义系统级别的属性,您可以使用 systemProp 将属性追加。因此,systemProp.sProp1=sVal1sProp1 设置为具有值 sVal1 的系统级别属性。

我们将在下一节中看到一个示例。

命令行

您也可以使用 -P-D 选项在命令行上定义运行时属性。使用 -P,您可以定义项目特定的属性。使用 -D,您可以定义系统级别的属性。要访问系统级别的属性,您可以使用 System.properties['<propertyname>']。请注意,命令行属性会覆盖 gradle.properties。当您在多个位置配置属性时,以下顺序适用,最后一个具有最高优先级:

  • 项目 build 目录中的 gradle.properties

  • Gradle 用户主目录中的 gradle.properties

  • 命令行设置的系统属性。

自定义属性文件

您可能希望为属性文件使用自定义文件名,例如,login.propertiesprofile.properties。要使用自定义属性,只需使用 FileInputStream 读取文件并将其转换为属性对象:

task showCustomProp << {
  Properties props = new Properties()
  props.load(new FileInputStream("login.properties"))
  println props
  println props.get('loginKey1')
}

上述代码将读取 login.properties 文件,第一个 println 语句将打印所有属性,而第二个 println 语句将显示 loginKey1 属性的值。

让我们来看一个综合示例。我们将在 <USER_HOME>/.gradle 目录中创建一个 gradle.properties 文件,并在项目目录中创建另一个 gradle.properties 文件:

<USER_HOME>/.gradle/gradle.properties

globalProp1=globalVal1
globalProp2=globalVal2

Chapter6/PropertyExample/Proj1/gradle.properties

Proj1Prop1=Proj1Val1
Proj1Prop2=Proj1Val2
systemProp.sysProp1=sysVal1

这里是我们的构建脚本,Chapter6/PropertyExample/Proj1/build.gradle

task showProps << {
  println "local property "+Proj1Prop1
  println "local property "+Proj1Prop2
  println "local property via command line: "+projCommandProp1
  println "global property "+globalProp1
  println "global property "+globalProp2
  println "System property "+System.properties['sysProp1']
  println "System property via command line: "+System.properties['sysCommandProp1']
}

现在,执行以下命令:

$gradle -PprojCommandProp1=projCommandVal1-DsysCommandProp1=sysCommandVal1 showProps

:showProps
local property Proj1Val1
local property Proj1Val2
local property via command line: projCommandVal1
global property globalVal1
global property globalVal2
System property sysVal1
System property via command line: sysCommandVal1

BUILD SUCCESSFUL

在这里,你可以看到前两行包含在项目的 gradle.properties 文件中定义的属性。第三行显示了用户使用 -P 选项初始化的属性。第四和第五行显示了在 <USER_HOME>/.gradle/gradle.properties 中定义的属性。第六行显示了在项目的 gradle.properties 文件中定义的系统属性,最后,示例显示了使用 -D 选项在命令行中传递的系统属性。

多项目构建

我们已经探索了 Gradle 的许多特性,例如任务、插件和依赖管理。我们看到了许多涉及内置任务、自定义任务和任务之间依赖关系的构建脚本示例。然而,我们还没有涵盖 Gradle 的一个主要特性,即多项目构建。直到现在,我们只看到了单个项目的构建文件。单个项目构建文件仅代表一个项目或一个模块。在任何软件世界中,最初通常从一个模块开始,随着软件随着时间的推移成熟和增长,它变成一个大项目。然后我们需要再次将其划分为不同的子模块,但总体来说,我们只使用一个文件来构建项目。Gradle 提供了将不同的模块视为不同项目的能力,这些项目可以归入根项目之下。它还提供了独立构建子模块而不构建完整项目的灵活性。

多项目构建不是一个新概念。Gradle 提供的唯一附加功能是分别将模块作为单独的子项目构建,并且每当需要时,可以使用根项目构建整个模块。子项目具有 Gradle 中项目对象的所有属性和功能。你可以定义模块之间的模块依赖关系。Gradle 允许你定义子项目任务对其他子项目的依赖关系。你可以仅构建一个子项目(及其依赖关系)以优化构建性能时间等。

多项目结构

考虑一个简单的用户管理 Java 应用程序,该应用程序验证和授权用户,允许用户管理其个人资料,并执行交易。假设我们将这个应用程序划分为三个不同的子项目或模块:登录模块、个人资料模块和交易模块。

可能还会出现另一个问题,当我们已经定义了三个子项目时,为什么还需要根项目 UserManagement,因为它不包含任何源代码?根项目的一个目的就是在子项目之间进行协调,定义项目之间的依赖关系(如果有的话),定义公共行为以避免在每个项目中重复构建配置,等等。

这三个模块的目的是独立工作于它们,分别构建它们,如果需要,可以无任何依赖关系地发布其工件。

目录结构将类似于以下图表:

多项目结构

图 6.4

在这里,我们创建了三个子项目:loginprofiletransaction,每个模块都有自己的src/main/java层次结构。我们将子项目分组在根项目UserManagement下。此外,根项目包含一个build.gradle文件和一个settings.gradle文件。

settings.gradle文件是多项目构建中的关键文件之一。此文件需要存在于根项目的目录中。它列出了所有子项目。以下代码显示了settings.gradle文件的内容:

settings.gradle:
include 'login', 'profile', 'transactions'

在这里,我们已包含所有属于根项目的子项目。执行以下命令后,我们将获得所有项目详情作为输出:

$ gradle projects
……
Root project 'UserManagement'
+--- Project ':login'
+--- Project ':profile'
\--- Project ':transactions'
……

BUILD SUCCESSFUL

输出显示了根项目UserManagement以及所有位于根项目下的子项目。现在,尝试删除settings.gradle文件或在settings.gradle文件中删除包含语句,然后再次运行此命令。这次,它将仅显示根项目详情。settings.gradle是一个重要的文件,它使根项目了解它应该包含的所有子项目。也可以使用'subproject:subsubproject','subproject:subsubproject:subsubsubproject'等来声明多级子项目。

我们讨论了 Gradle 构建生命周期的三个阶段:初始化、配置和执行。在初始化阶段使用settings.gradle文件,Gradle 会将所有子项目实例添加到构建过程中。您也可以通过使用include(String[])方法向此对象添加项目。

settings.gradle文件还可以访问在构建的设置目录中定义的gradle.properties文件或<USER_HOME>/.gradle目录中的属性,以及使用-P选项在命令行上提供的属性。settings.gradle文件还可以执行 Gradle 任务,包括插件和其他操作,这些操作可以在任何.gradle文件中完成。

多项目执行

为了确定当前构建过程是否是多项目构建的一部分,它首先在当前目录中搜索settings.gradle文件,然后在其父级层次结构中搜索。如果在同一目录中找到settings.gradle,它将自己视为父项目,然后检查子项目。在另一种情况下,如果在其父级层次结构中找到settings.gradle文件,它会检查当前子目录是否是找到的根项目的子项目。如果当前项目是根项目的一部分,那么它作为多项目构建的一部分执行,否则作为单一项目构建。

以下是在UserManagement目录下的示例build.gradle

println "Project name is $name"

project(':login') {
  apply plugin: 'java'
  println "Project name is $name"
  task loginTask << {
    println "Task name is $name"
  }
}

project(':profile') {
  apply plugin: 'java'
  println "Project name is $name"
  task profileTask << {
    println "Task name is $name"
  }
}
project(':transactions') {
  apply plugin: 'java'
  println "Project name is $name"
  task transactionTask << {
    println "Task name is $name"
  }
}

现在,尝试从UserManagement目录执行以下命令:

/UserManagement$ gradle

Project name is UserManagement
Project name is login
Project name is profile
Project name is transactions
:help

...

现在,转到 login 目录并执行相同的命令;你会找到类似的输出。区别在于,在子项目中,帮助任务会被替换为:login:help,因为 Gradle 会自动检测你所在的子项目。

在第一种情况下,Gradle 在同一目录中找到了 settings.gradle 文件,并找到了三个子项目。Gradle 初始化了三个子项目,并在配置阶段执行了配置语句。我们没有提到任何任务,所以没有执行任何任务。

在第二种情况下,当我们从登录模块执行 Gradle 命令时,Gradle 再次开始搜索 settings.gradle 文件,并在父目录中找到了这个文件,并且也发现当前项目是多项目构建的一部分,因此作为多项目构建执行了构建脚本。

你可能已经注意到这里我们没有为任何子项目定义任何 build.gradle 文件。我们将所有子项目添加到了根项目的构建文件中。这是定义多项目构建的一种方法。另一种方法是,在每个子项目中创建单独的 build.gradle 文件。只需从主构建文件中移除项目闭包,并将其复制到相应的项目构建文件中。新的项目结构如图 6.4 所示:

多项目执行

图 6.5

任务执行

在执行多项目构建中的任务之前,Gradle 将在根项目和所有子项目中搜索该任务。如果任务在多个项目中找到,它将依次执行所有任务。从 UserManagement 目录执行以下命令:

$ gradle loginTask

Project name is UserManagement
Project name is login
Project name is profile
Project name is transactions
:login:loginTask
Task name is loginTask

BUILD SUCCESSFUL

现在,将 loginTask 复制到交易项目,并尝试执行相同的命令:

$ gradle loginTask
….
:login:loginTask
Task name is loginTask
:transactions:loginTask
Task name is loginTask

BUILD SUCCESSFUL

在这里,你可以看到 Gradle 在 logintransactions 项目中执行的 loginTask。要执行特定项目的任务,请在任务名称前加上项目名称,并使用冒号(:)作为分隔符——gradle project:task。要为 login 模块执行 loginTask,使用 $ gradle login:loginTask 命令。

多项目构建有助于避免冗余配置,并允许适当地优化和组织构建文件结构。

在前面的例子中,我们有三个子项目,它们都依赖于 Java 插件。这些子项目可能还依赖于一些公共库。我们可以在根项目中定义一个公共配置,而不是在每个子项目的构建文件中定义依赖关系。这样做,整个子项目将继承这个公共配置。这可以通过使用两个闭包:allprojectssubprojects来实现。在allprojects下定义的配置将由所有子项目共享,包括根项目,而在subprojects下定义的配置将由所有子项目共享,但不包括根项目。添加以下subprojects{}allprojects{}闭包,这些闭包用于构建文件并从每个子项目中移除apply plugin: 'java'语句:

println "Project name is $name"
allprojects {
  version = '2.0'
}
subprojects { // for all subprojects
  apply plugin: 'java'
  repositories {
    mavenCentral()
  }
  dependencies {
    compile 'log4j:log4j:1.2.16'
  }
}

在这里,我们已将 Java 插件、repositories闭包和公共依赖项添加到subprojects闭包中。因此,它将由所有子项目共享。我们在allprojects中添加了一个版本,这将由所有子项目共享,包括根子项目。

现在,尝试执行以下命令:

$ gradle clean
Project name is UserManagement
Project name is login
Project name is profile
Project name is transactions
:login:clean
:profile:clean
:transactions:clean

BUILD SUCCESSFUL

它已执行了所有子项目的 clean 任务,但未执行根项目的任务。即使你尝试显式执行UserManagement:clean任务,它也会抛出异常。如果你在allprojects闭包中添加apply plugin: 'java',它将把 clean 任务添加到根项目和子项目中。

平级层次结构

除了父/子层次结构之外,你还可以在同一级别创建子项目,可以使用includeFlat '<projectname>'语法来包含它们。

让我们在与UserManagement模块同一级别的位置添加一个额外的子项目部门。

可以通过在settings.gradle文件中添加以下代码,将department模块作为子项目添加到UserManagement项目中:

includeFlat 'department'
// adding same level project as sub project

项间依赖

当你在多项目构建上执行一些常见任务,如cleancompile(在添加 Java 插件后),默认的执行顺序是基于它们的字母顺序:

$ gradle clean

Project name is UserManagement
Project name is department
Project name is login
Project name is profile
Project name is transactions
:department:clean UP-TO-DATE
:login:clean UP-TO-DATE
:profile:clean UP-TO-DATE
:transactions:clean UP-TO-DATE

BUILD SUCCESSFUL

首先评估第一个根项目,然后按照字母顺序评估所有子项目。为了覆盖默认行为,Gradle 为你提供了不同级别的依赖管理。

配置级别的依赖

配置级别的依赖在依赖于它的项目执行之后评估或配置项目。例如,你想要在配置项目中设置一些属性,并想要在登录项目中使用这些属性。你可以通过使用evaluationDependsOn来实现这一点。为了启用此功能,你应该为每个子项目有单独的build.gradle文件。让我们为每个子项目创建独立的build.gradle文件。

你可以按照以下模式创建每个子项目和build.gradle

/<project name>/build.gradle
println "Project name is $name"
task <projectName>Task << {
  println "Task name is $name "
}

根项目build.gradle将如下所示:

UserManagement_confDep/build.gradle

println "Project name is $name"
allprojects {
  version = '2.0'
}
subprojects { // for all sub projects
  apply plugin: 'java'
  repositories {
    mavenCentral()
  }
}

现在,执行以下 Gradle 命令:

/UserManagement_confDep$ gradle

Project name is UserManagement_confDep
Project name is login
Project name is profile
Project name is transactions
...
BUILD SUCCESSFUL

我们没有执行任何任务就执行了 Gradle 命令。它执行到了配置阶段,你可以看到之前的配置顺序按字母顺序排列(在根项目配置之后)。

现在,在你的登录项目build.gradle文件中添加以下语句:

evaluationDependsOn(':profile')

然后,执行 Gradle 命令:

/UserManagement_confDep$ gradle

Project name is UserManagement_confDep
Project name is profile    // Order is changed
Project name is login
Project name is transactions
……
BUILD SUCCESSFUL

现在,你可以看到配置配置在登录配置之前被评估。

任务级别依赖

可能存在一种情况,一个项目的任务可能依赖于另一个项目的任务。Gradle 允许你在子项目之间维护任务级别的依赖关系。以下是一个示例,其中loginTask依赖于profileTask

project(':login') {
  println "Project name is $name"
  task loginTask (dependsOn: ":profile:profileTask")<< {
    println "Task name is $name"
  }
}

现在的输出显示了任务之间的依赖关系:

/UserManagement_taskDep$ gradle loginTask
….
:profile:profileTask
Task name is profileTask
:login:loginTask
Task name is loginTask

BUILD SUCCESSFUL

如果你使用dependsOn在不同项目之间声明执行依赖,此方法的默认行为是在两个项目之间创建配置依赖。

库依赖

如果一个子项目需要另一个子项目的类文件或 JAR 文件来编译,这可以作为一个编译时依赖引入。如果登录项目需要在它的类路径中包含配置 JAR,你可以在编译级别引入依赖:

project(':login') {
  dependencies {
    compile project(':profile')
  }
  task loginTask (dependsOn: ":profile:profileTask")<< {
    println "Task name is $name"
  }
}
/UserManagement_libDep$ gradle clean compileJava
...
:login:clean
:profile:clean
:transactions:clean
:department:compileJava UP-TO-DATE
:profile:compileJava
:profile:processResources UP-TO-DATE
:profile:classes
:profile:jar
:login:compileJava
:transactions:compileJava

BUILD SUCCESSFUL

从输出中,我们可以意识到所有依赖模块都在执行登录编译任务之前被编译。

部分构建

在开发过程中,你可能需要反复构建项目。有时你并没有对你的依赖子项目进行任何更改,但 Gradle 默认总是先构建依赖项,然后再构建依赖的子项目。这个过程可能会影响整体构建性能。为了解决这个问题,Gradle 提供了一个名为部分构建的解决方案。部分构建允许你只构建所需的项目,而不是其依赖项目。在上面的示例中,我们有登录模块对配置项目的编译依赖。要编译不依赖配置子项目的登录项目,可以使用命令行选项-a

$ gradle :login:compileJava -a
:login:compileJava

BUILD SUCCESSFUL

buildDependents

在企业项目中,我们有项目依赖。当你想构建一个项目,同时你又想构建其他依赖项目时,Java 插件提供了buildDependents选项。

在上一个示例中,登录项目对配置项目有编译时依赖。我们将尝试使用buildDependents选项构建配置:

/UserManagement_libDep$ gradle :profile:buildDependents
. . .
:profile:compileJava UP-TO-DATE
:profile:processResources UP-TO-DATE
:profile:classes UP-TO-DATE
:profile:jar UP-TO-DATE
:login:compileJava UP-TO-DATE
:login:processResources UP-TO-DATE
:login:classes UP-TO-DATE
:login:jar
:login:assemble
:login:compileTestJava UP-TO-DATE
:login:processTestResources UP-TO-DATE
:login:testClasses UP-TO-DATE
:login:test UP-TO-DATE
:login:check UP-TO-DATE
:login:build
:login:buildDependents
:profile:assemble UP-TO-DATE
:profile:compileTestJava UP-TO-DATE
:profile:processTestResources UP-TO-DATE
:profile:testClasses UP-TO-DATE
:profile:test UP-TO-DATE
:profile:check UP-TO-DATE
:profile:build UP-TO-DATE
:profile:buildDependents

BUILD SUCCESSFUL

由于登录模块依赖于配置模块,执行配置项目也会构建登录项目。

buildNeeded

当你构建项目时,它只编译代码并准备 JAR 文件。如果你对其他项目有编译时依赖,它只编译其他项目并准备 JAR 文件。为了检查完整组件的功能,你可能还想执行测试用例。要执行子项目和依赖项目的测试用例,使用buildNeeded

/UserManagement_libDep$ gradle :login:buildNeeded
. . .
:login:processTestResources UP-TO-DATE
:login:testClasses UP-TO-DATE
:login:test UP-TO-DATE
:login:check UP-TO-DATE
:login:build UP-TO-DATE
:profile:assemble UP-TO-DATE
:profile:compileTestJava UP-TO-DATE
:profile:processTestResources UP-TO-DATE
:profile:testClasses UP-TO-DATE
:profile:test UP-TO-DATE
:profile:check UP-TO-DATE
:profile:build UP-TO-DATE
:profile:buildNeeded UP-TO-DATE
:login:buildNeeded UP-TO-DATE

BUILD SUCCESSFUL

在这里,buildNeeded不仅执行登录测试用例,还执行配置文件测试用例。

使用 Gradle 进行测试

除非软件通过了适当的质量检查,否则没有任何软件是生产就绪的。这有助于以最低的缺陷交付软件,并节省大量的维护工作。然而,手动测试执行需要大量时间来执行测试,因此软件发布周期被延迟。如果测试自动化,可以改善发布时间和生产力。

Gradle 提供了一种自动执行测试代码的方法,特别是对于单元测试。在下一节中,我们将探讨如何将 JUnit 和 TestNG 与 Gradle 集成。

JUnit

Gradle 的 Java 插件提供了一个预定义的结构来保持测试代码和测试资源以进行配置和执行。与源代码结构一样,默认的测试代码位置是src/test/java/<test_package>。如果你遵循此约定,你只需执行test任务即可运行单元测试用例,如下面的命令所示:

$ gradle test
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources
:testClasses
:test
BUILD SUCCESSFUL

这个测试任务将执行所有必要的操作,例如编译源代码、编译测试代码、处理资源,最后执行测试用例并创建报告。

JUnit 提供了一个用户友好的格式来理解结果。在执行测试任务后,你会找到以下层次结构:

JUnit

图 6.6

reports文件夹包含一个名为index.html的 HTML 格式的tests子目录,其中包含测试摘要结果。如果你打开index.html文件,你会看到以下输出:

JUnit

图 6.7

它提供了对测试用例场景的完整分析,例如执行测试用例的数量、失败的测试用例、忽略的测试用例等。从报告中,你可以通过跟随报告页面上的超链接进一步深入到单个测试用例级别。如果发生错误/异常,报告将显示详细的解释,并以表格格式显示执行时间。

到目前为止,我们只讨论了使用 Gradle 执行测试用例。为了编译和执行测试用例,我们需要一个测试框架库。像任何其他配置一样,你需要将 JUnit JAR 作为项目的依赖项提及。通常,依赖项作为testCompile配置添加:

repositories {
  mavenCentral()
}
dependencies {
  testCompile 'junit:junit:4.12'
}

此配置将从 Maven 仓库下载junit-4.12.jar,JAR 文件将在编译和执行阶段添加到类路径中。

测试配置

可以为测试配置设置不同的配置参数,这有助于优化资源并根据项目需求自定义行为。

有时,测试目录结构不遵循默认约定,即src/test/java。与源代码目录配置类似,你可以按照以下方式配置新的测试代码位置:

sourceSets {
  test {
    java {
      srcDir 'testSrc'
    }
  }
}

maxParallelForks

Gradle 在单独的 JVM 中执行测试用例。默认情况下,Gradle 在单个进程中执行所有测试。您可以通过在test闭包中配置maxParallelForks属性来指定并行进程的数量。其默认值是1

test {
maxParallelForks = 3
}

要了解其确切的工作方式,我们可以修改我们之前的例子。只需在src/test/java中创建测试类的多个副本。在我们的例子中,在TestUsingJunitParallel项目中,我们创建了总共五个与LoginTest相同的副本,例如LoginTest1LoginTest2等。现在,使用--info选项执行 Gradle 命令:

TestUsingJunitParallel$ gradle clean --info test | grep 'Test Executor'
. . . .
. . . .
Successfully started process 'Gradle Test Executor 2'
Successfully started process 'Gradle Test Executor 1'
Successfully started process 'Gradle Test Executor 3'
Gradle Test Executor 2 started executing tests.
Gradle Test Executor 3 started executing tests.
Gradle Test Executor 1 started executing tests.
Gradle Test Executor 3 finished executing tests.
Gradle Test Executor 2 finished executing tests.
Gradle Test Executor 1 finished executing tests.

命令行输出显示 Gradle 创建了三个进程,并且所有测试用例都在这些进程中执行。

forkEvery选项

此选项允许设置每个进程的测试类数。默认值是0,即无限。如果您将此选项设置为非零值,则当达到此限制时将创建进程。

在上一个例子中,我们有五个测试类,并将并行处理计数设置为三个。现在,我们将forkEvery选项设置为1,因此每个进程将只执行一个测试类:

test {
  ignoreFailures = true
  maxParallelForks = 3
  forkEvery = 1
}
TestUsingJunitParallel$ gradle clean --info test | grep 'Test Executor'
. . . .
Successfully started process 'Gradle Test Executor 1'
Successfully started process 'Gradle Test Executor 3'
Successfully started process 'Gradle Test Executor 2'
Gradle Test Executor 1 started executing tests.
Gradle Test Executor 2 started executing tests.
Gradle Test Executor 3 started executing tests.
Gradle Test Executor 1 finished executing tests.
Starting process 'Gradle Test Executor 4'. Working directory:
. . . .
Successfully started process 'Gradle Test Executor 4'
Gradle Test Executor 3 finished executing tests.
Gradle Test Executor 2 finished executing tests.
Starting process 'Gradle Test Executor 5'. Working directory:
. . . .
Successfully started process 'Gradle Test Executor 5'
Gradle Test Executor 4 started executing tests.
Gradle Test Executor 5 started executing tests.
Gradle Test Executor 4 finished executing tests.
Gradle Test Executor 5 finished executing tests.

从输出中,我们可以观察到 Gradle 首先创建了三个进程,执行了三个测试类。然后,创建了另外两个进程,例如'Gradle Test Executor 4''Gradle Test Executor 5',用于执行另外两个测试文件。

忽略失败

无论哪个测试用例失败,构建都会标记为FAILED

$ gradle test
. . .
:test

ch6.login.LoginTest > testLogin1 FAILED
 java.lang.AssertionError at LoginTest.java:26

4 tests completed, 1 failed
:test FAILED

FAILURE: Build failed with an exception.
. . .
BUILD FAILED

如果您希望构建无论测试用例结果如何都成功,您可以在构建脚本test闭包中添加ignoreFailures=true,如前例所示。其默认值是false。再次执行测试任务时,构建将成功如下:

$ gradle test
. . .
ch6.login.LoginTest > testLogin1 FAILED
 java.lang.AssertionError at LoginTest.java:26

4 tests completed, 1 failed
. . .
BUILD SUCCESSFUL

过滤

Gradle 允许您通过基于不同的模式过滤测试用例来选择性地执行测试用例。假设我们有两个测试包,包含四个测试用例。

/src/test/java/ch6/login/LoginTest.java包含以下两个测试包:

  • testUserLogin1()

  • testUserLogin2()

/src/test/java/ch6/profile/ProfileTest.java包含以下两个测试包:

  • testUserProfile1()

  • testUserProfile2()

以下代码片段显示了如何根据不同的模式应用过滤器:

test {
  filter {
    // 1: execute only login test cases
    includeTestsMatching "ch6.login.*"

    //2: include all test cases matching *Test
    includeTestsMatching "*Test"

    //3: include all integration tests having 1 in their name
    includeTestsMatching "*1"

    //4: Other way to include/exclude packages
    include "ch6/profile/**"
  }
}

第一个过滤器将只从ch6.login包中识别出两个测试用例。第二个过滤器选择所有四个测试用例,因为测试类名与*Test模式匹配。第三个语句最终只过滤出两个测试用例:testUserLogin1()testUserProfile1()

只注释前两个模式,并使用过滤器模式*1执行测试。尽管我们总共有四个测试用例,但你将发现 Gradle 会从每个包中执行一个测试用例。你也可以使用前面示例中提到的包结构,通过使用includeexclude来包含或排除包。如果你只想执行单个测试类,你也可以通过将测试类追加到命令行选项--tests来执行它:命令gradle tests --tests ch6.login.LoginTest将只执行LoginTest类中提到的测试用例:

filter

图 6.8

TestNG

Gradle 还提供了与 TestNG 框架的集成。要在 TestNG 中编写测试用例,你需要在build.gradle文件中添加依赖项:

dependencies {
  testCompile 'org.testng:testng:6.8.21'
}

在我们的示例中,我们创建了一个包含三个测试用例的 TestNG 测试类。现在,通过执行测试任务,我们得到在build/reports/tests下创建的报告文件:

$ gradle clean test

现在,打开index.html文件,你会看到以下输出:

TestNG

图 6.9

报告的外观和感觉与之前看到的 JUnit 相似。实际上,JUnit 和 TestNG 本身生成完全不同的报告格式,但 Gradle 将它们协调成标准的外观和感觉。

JUnit部分所述,你还可以在test闭包中定义其他属性,例如ignoreFailuresmaxParallelForks等。

test{
  useTestNG()
  ignoreFailures = true
  maxParallelForks = 2
  forkEvery = 1
}

基于组的执行

在前面的test闭包中,我们使用了useTestNG选项来启用TestNG支持。你还可以在此闭包中设置其他选项,如组和监听器。例如,以下设置仅执行具有组名Smoke的测试用例,并在reports/tests文件夹中创建一个额外的可发送电子邮件的TestNG报告:

useTestNG(){
  includeGroups 'Smoke'
  listeners << 'org.testng.reporters.EmailableReporter'
}

useTestNG中,你可以根据@Test注解的 group 属性对测试用例进行分组:

@Test(groups = "<group name>")

在我们的示例中,我们将测试用例分组为SmokeIntegration。在执行test任务时,只有verifyArraySizeverifyArrayNotNull测试用例将被执行:

@Test(groups = "Smoke")
public void verifyArraySize()

@Test(groups = "Smoke")
public void verifyArrayNotNull()

@Test(groups = "Integration")
public void verifyArrayPosition()

基于 TestNG 套件文件的执行

TestNG 套件文件提供了更好的控制来执行测试。在测试套件文件中,你可以定义所有将被包含以执行测试用例的测试类和方法,基于组名的任何过滤器,监听器信息等。

我们在src/test/resource文件夹中创建了一个testng.xml文件。该文件包含三个关键信息;创建可发送电子邮件的报告格式的listener配置,包含测试组为Smoke,并将ArrayTest文件作为测试类添加。使用测试套件文件,你还可以配置其他属性,例如线程池大小、测试类或测试方法是否并行运行,等等:

<!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="ch6.testng.example.ArrayTest">
      </class>
    </classes>
  </test>
</suite>

此套件文件可以按如下方式包含在test闭包中。然后,在执行测试任务时,报告将在reports/tests文件夹中创建:

test {
ignoreFailures = true
  useTestNG(){
    suites("src/test/resources/testng.xml")
  }
}

摘要

在本章中,我们探讨了 Gradle 的不同主题,例如使用 Gradle 进行 I/O 操作、日志记录、多项目构建和测试。我们还学习了如何使用 Gradle 轻松地为 Web 应用程序和 Scala 项目生成资源。在 使用 Gradle 进行测试 部分,我们学习了执行 JUnit 和 TestNG 测试的一些基础知识。

在下一章中,我们将学习 Java 项目的代码质量方面。我们将分析几个 Gradle 插件,例如 Checkstyle 和 Sonar。除了学习这些插件之外,我们还将讨论另一个名为持续集成的主题。这两个主题将通过探索两个不同的持续集成服务器,即 Jenkins 和 TeamCity,来结合并展示。

第七章. 持续集成

持续集成是当今软件世界中使用的术语之一。无论您在软件世界的哪个角落,每个人都在谈论持续集成。那么,什么是持续集成呢?

持续集成是将共享仓库中的所有软件代码进行集成的实践;为每次提交准备自动构建,并运行自动测试而无需任何人工操作。它帮助开发者能够在快速失败模式下尽早发现问题。在这里,“尽早”意味着开发者提交代码后;根据项目规模,几秒钟或几分钟内,持续集成过程将通知构建的成功或失败。由于错误在早期阶段就被捕获,因此在执行应用程序的集成和功能测试时可以节省大量精力。

在本章中,我们将探讨流行的持续集成工具 Jenkins 和 TeamCity。由于这是一本 Gradle 书籍,我们将限制我们的讨论范围仅限于这些工具的基本设置和配置。我们还将介绍一个新主题,即使用 Gradle 进行代码质量管理。我们将学习如何将CheckstylePMDSonar Runner插件集成到 Gradle 中,以及如何将其与持续集成工具集成。

Jenkins 快速入门

Jenkins 是最受欢迎的开源持续集成工具之一,它有助于自动化软件构建和部署过程。它可以与 Maven、Gradle 和 Ant 等构建工具一起工作。它支持各种源代码管理系统,如 CVS、Git、Subversion 和 Perforce。甚至支持简单的 shell 或批处理脚本执行。Jenkins 的主要优势在于其插件支持。有超过 1000 多个插件用于不同的功能,如果需要,它可以扩展以支持新的需求。

Jenkins 的一些主要特性包括:

  • 易于安装和配置。简单的基于 Web 的 UI 用于管理服务器

  • 支持各种插件,用于不同的构建和部署相关任务

  • 一个非常大的社区论坛

  • 支持 SVN、Git、CVS、Perforce 等不同存储库

  • 支持构建后钩子

Jenkins 安装

Jenkins 安装只需两个步骤。您需要从jenkins-ci.org/下载jenkins.war文件。您将始终从这个 URL 获取最新版本。对于任何之前的版本,点击过往版本选项,并决定您想要哪个版本。

下载 war 文件后,它可以在 Tomcat 等容器中部署,或者可以使用以下命令执行:

$ java -jar jenkins.war
Running from: /jenkins/jenkins.war
webroot: $user.home/.jenkins
….
Apr 02, 2015 3:30:32 PM org.eclipse.jetty.util.log.JavaUtilLog info
INFO: Started SelectChannelConnector@0.0.0.0:8080
…..
Apr 02, 2015 3:30:37 PM org.jenkinsci.main.modules.sshd.SSHD start
INFO: Started SSHD at port 50566
Apr 02, 2015 3:30:37 PM jenkins.InitReactorRunner$1 onAttained
INFO: Completed initialization
Apr 02, 2015 3:30:37 PM hudson.WebAppMain$3 run
INFO: Jenkins is fully up and running

在这里,Jenkins 从内置的 Jetty 容器开始,端口为 8080。默认的 Jenkins 主目录将被设置为<USER_HOME>/.jenkins。通过设置JENKINS_HOME环境变量,您可以将其设置为任何其他位置。此目录存储所有与 Jenkins 相关的信息,例如作业信息、用户账户详情、插件信息和 Jenkins 的一般设置。

现在打开浏览器并输入以下内容:http://localhost:8080,Jenkins 欢迎页面将显示。这就完成了。Jenkins 已为您准备好:

Jenkins 安装

图 7.1

由于这不是 Jenkins 用户指南,我们不会详细涵盖 Jenkins 的功能。您可以在 Jenkins 官方网站上查看可用的教程。我们将主要涵盖有助于使用 Gradle 自动化构建过程的主题。

Jenkins 配置

仅安装不足以让 Jenkins 开始使用 Gradle 构建过程。在我们开始使用 Jenkins 的第一个作业之前,我们需要配置一些插件。在 Gradle 中像task一样,Jenkins 的执行单元是作业。构建作业可以执行编译、运行自动化测试、打包甚至与部署相关的任务。但在我们开始处理作业之前,我们将为 Jenkins 配置以下插件。

  • Gradle 插件

  • Git 插件(如果您使用 Git 作为存储库,则必需)

在左侧垂直菜单中点击管理 Jenkins。您将看到一个不同类别的列表。点击管理插件。您将找到以下四个标签页:

Jenkins 配置

图 7.2

前往可用标签页,并筛选(右上角)为Gradle 插件。您将找到具有以下详细信息的 Gradle 插件:

Gradle 插件

此插件使得能够将 Gradle 构建脚本作为主要构建步骤调用。

选择插件并点击现在下载并在重启后安装。在安装后重启 Jenkins 是一个好习惯,以避免任何问题。

这将为 Jenkins 服务器添加 Gradle 构建执行能力。一旦安装成功,您将能够看到成功消息。如果系统受到防火墙保护,可能会出现错误,这可能会在连接到互联网时限制系统。在这种情况下,请手动下载插件(*.hpi文件)并将其复制到<Jenkins_home>/plugins目录。Jenkins 插件可以从updates.jenkins-ci.org/download/plugins/下载。

对于本章的示例,我们使用 GitHub 作为仓库。要使用 GitHub,我们将向 Jenkins 服务器添加 GitHub 插件。我们可以以添加 Gradle 插件相同的方式添加它。如果插件依赖于其他插件,那么 Jenkins 将自动下载所需的插件。当安装 GitHub 插件时,你可以观察到这一点。Jenkins 自动安装其他所需的插件,如 Git 客户端插件和 Git API 插件。如前所述,某些插件可能需要重启 Jenkins 服务器。在这种情况下,停止当前进程并重启 Jenkins 服务器以使插件生效。

下一个重要步骤是使用 Jenkins 配置 JDK、Gradle 和 Git。要配置这些设置,打开 Jenkins URL 并点击管理 Jenkins,然后配置系统

输入 JDK 的正确路径并保存设置。Jenkins 还有一个选项可以从互联网自动安装软件。请看以下截图:

Jenkins 配置

图 7.3

图 7.4 显示了如何在 Jenkins 中配置 Gradle:

Jenkins 配置

图 7.4

创建任务

在 Gradle 插件成功安装后,我们将创建第一个 Gradle 构建任务。转到 Jenkins 主页并点击创建新任务。在 Jenkins 中可以创建不同类别的任务。在本例中,我们将创建一个自由风格项目。为了简单起见,我们将构建我们在第四章中创建的插件项目,插件管理。只需给它一个名称,例如PluginProject,如图 7.5 所示。此外,尽量避免在任务名称中使用空格,因为这被认为是不良的做法。

如果你想单词之间有分隔,可以使用下划线(_):

创建任务

图 7.5

点击确定后,在下一页你将需要配置任务。你需要配置以下一些细节:

  • 下载项目的源代码管理位置

  • 项目构建步骤

  • 安排构建任务(每日、每小时、每次提交后等)

  • 添加任何构建后的操作以执行

首先,我们将在源代码管理下配置仓库。由于我们使用的是 GitHub 仓库,因此需要选择Git选项。

创建任务

图 7.6

使用凭据选项提供仓库 URL,并添加认证(用户名/密码),如图 7.6 所示进行配置。

Git 可执行文件必须在 Jenkins 系统配置中设置,否则您将无法执行 Git 命令。如果连接到 URL 存在问题,Jenkins 将显示适当的错误消息。这有助于调试和解决问题。如果验证 URL 成功,下一步是选择以下图中显示的选项之一中的构建选项:

创建工作

图 7.7

对于我们的项目,我们选择了当将更改推送到 GitHub 时构建的选项,这有助于通过运行构建脚本来验证每个提交。您可以按照构建需求设置任何其他选项。

下一步是选择项目的构建工具。从可用的选项,如 shell、Ant 和批处理中,我们将选择 Gradle 作为此项目的构建工具。此选项在下图中突出显示:

创建工作

图 7.8

选择调用 Gradle 脚本选项并配置一些基本参数:

创建工作

图 7.9

我们已将系统上安装的 Gradle gradle-2.4配置为工作 Gradle 版本。要构建PluginProject,可以在任务文本框中将任务设置为clean build。如果build.gradle文件位于项目的 home/root 文件夹中,则可以保留根构建脚本文本框为空。然而,如果它在另一个目录中,您必须提及相对于工作区位置的路径。我们的build.gradle文件位于Chapter7/PluginProject文件夹中。因此,我们可以输入根构建脚本为${workspace}/Chapter7/PluginProject。由于我们使用的是构建文件名build.gradle,这是 Gradle 中的默认命名约定,因此我们不需要在构建文件文本框中指定文件名。如果您使用任何其他构建文件名,它必须在构建文件文本框中提及。

您还可以添加构建后操作,例如发布 Java 文档、发送电子邮件通知、根据项目要求构建其他项目。

现在,保存配置,您将能够在仪表板上看到项目:

创建工作

图 7.10

执行工作

尽管我们已配置构建在将更改推送到源代码管理系统时执行,但如果您不想等待存储库中的更改发生,构建始终可以手动执行。点击 Jenkins 主页上您之前创建的PluginProject工作。您将被导航到http://localhost:8080/job/PluginProject/的工作控制台。

执行工作

图 7.11

在作业控制台,你将在左侧找到构建现在选项。点击此选项以手动执行作业。在控制台页面,你可以通过选择配置选项在任何时候配置作业。一旦作业成功执行,你将找到在构建历史部分的构建编号链接中显示的类似输出,如图 7.12 所示:

执行作业

图 7.12

构建历史在 UI 中以最新的作业执行状态显示在顶部。图 7.13 显示第 1 次和第 2 次执行失败并出现某些错误,但第 3 次执行成功。在构建历史部分,如果作业失败,将以红色标记。对于成功,它是蓝色,而取消的作业可以以灰色识别:

执行作业

图 7.13

此作业的默认位置是<USER_HOME>/.jenkins/jobs/<JOB_NAME>/workspace。如果你浏览到<USER_HOME>/.jenkins/jobs位置,你将找到一个以作业名称创建的目录,即PluginProject,它进一步包含用于作业配置的config.xml。作业目录有两个更进一步的子目录,builds用于已执行的作业,workspace是构建实际运行的地方。如果你进入builds目录,你将找到每次运行的构建执行详情。

执行作业

图 7.14

工作空间目录包含我们为作业配置的项目。在早期构建配置中,我们指定了Build Root${workspace}/Chapter7/PluginProject。现在如果我们去这个位置,我们将找到为该项目创建的构建文件夹:

/workspace/Chapter7/PluginProject$ ls -l
total 12
drwxrwxr-x 6 mainak mainak 4096 Apr  6 00:31 build
-rw-rw-r-- 1 mainak mainak  328 Apr  5 23:15 build.gradle
drwxrwxr-x 3 mainak mainak 4096 Apr  5 23:15 src

这只是 Jenkins 配置的简要概述。更多详细信息可以在jenkins-ci.org/找到。在接下来的两个部分中,我们将探索 Checkstyle、PMD 和 Sonar Runner 插件。

Checkstyle 和 PMD 插件

我们已经看到在 Jenkins 中创建 Gradle 构建作业是多么简单。现在我们将为质量检查目的向我们的项目添加CheckstylePMD插件。我们可以遵循不同的方法来使用这些插件。我们可以直接将这些插件添加到 Jenkins 并为其项目运行,或者我们可以使用 Gradle Checkstyle 和 PMD 插件并评估项目。

我们将使用 Gradle 方法添加 Checkstyle 和 PMD 插件以进行代码质量检查,并使用 Jenkins 执行此操作。让我们创建两个 Gradle 文件,一个用于 Checkstyle,另一个用于 PMD:

build_checkstyle.gradle

apply plugin: 'groovy'
apply plugin: 'eclipse'
apply plugin: 'checkstyle'

version = '1.0'

repositories {
  mavenCentral()
}
checkstyle {
  toolVersion = 6.5
  ignoreFailures = true
}

dependencies {
  compile gradleApi()
  compile localGroovy()
  compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
  testCompile group: 'junit', name: 'junit', version: '4.+'
}

在构建文件中,我们在checkstyle { … }闭包中添加了额外的配置。如果源代码未通过 CheckStyle 规则,将导致构建失败。为了忽略由于 CheckStyle 规则违反导致的任何构建失败,我们需要在checkstyle闭包中添加ignoreFailures=true属性。

Checkstyle 插件提供以下任务:

  • checkstyleMain:这将对 Java 源文件执行 Checkstyle

  • checkstyleTest:这个任务会对 Java 测试源文件执行 Checkstyle。

  • checkstyleSourceSet:这个任务会对给定源集的 Java 源文件执行 Checkstyle。

对于 Checkstyle 插件,我们需要在 <Project>/config/checkstyle/ 目录中有一个 checkstyle.xml 文件。这是默认位置。你可以在以下位置找到示例 checkstyle.xmlgithub.com/google/google-api-java-client/blob/dev/checkstyle.xml

它为项目提供了标准的质量检查。你也可以根据需求编写定制的 checkstyle.xml

要使用 PMD 插件,你可以复制上面的文件,并将 checkstyle 闭包替换为 pmd 闭包,并删除 toolVersion 属性。如果你没有指定版本,Gradle 默认下载 PMD 版本 5.1.1。你还需要添加 apply plugin: pmd`。

build_pmd.gradle

apply plugin: 'groovy'
apply plugin: 'pmd'

version = '1.0'

repositories {
  mavenCentral()
}

pmd{
  ignoreFailures = true
}

dependencies {
  compile gradleApi()
  compile localGroovy()
  compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
  testCompile group: 'junit', name: 'junit', version: '4.+'
}

PMD 插件提供了以下任务:

  • pmdMain:这个任务会对 Java 源文件执行 PMD。

  • pmdTest:这个任务会对 Java 测试源文件执行 PMD。

  • pmdSourceSet:这个任务会对给定源集的 Java 源文件执行 PMD。

Checkstyle 和 PMD 插件都可以使用 check 任务执行。

  • 如果你添加了 Checkstyle 插件并执行 check 任务,它将调用所有 Checkstyle 任务

  • 如果你添加了 PMD 插件并执行 check 任务,它将执行 pmd 任务

我们将创建一个新的项目 QualityCheck 并将以下文件添加到项目中:

  • build_checkstyle.gradle

  • build_pmd.gradle

  • config/checkstyle/checkstyle.xml

Checkstyle 和 PMD 插件在 Java 代码中执行,因此我们将在 src/main/java/ 目录下添加一些示例 Java 文件。为了在 Jenkins 中创建构建步骤,我们将创建一个执行 Checkstyle 任务(check 任务)的构建步骤,如图 7.15 所示。你也可以为 PMD 插件重复相同的步骤。

对于新的配置,Root Build script 设置为 ${workspace}/Chapter7/QualityCheck。同时,我们在文本框中添加了 Build file 名称,为 build_checkstyle.gradle

Checkstyle 和 PMD 插件

图 7.15

保存此配置并再次执行作业。按照配置,build_checkstyle.gradle 文件在 Java 源代码上执行,并为其生成了 CheckStyle 报告。你可以在 ${workspace}\Chapter7\QualityCheck\build\reports\checkstyle\main.xml 下找到这些报告。

Sonar Runner 插件

Sonar 是最受欢迎的质量管理工具之一,它从代码行数、文档、测试覆盖率、问题和复杂性等方面对项目进行全面的分析。Gradle 提供了与 Sonar 的无缝集成。唯一的前提是 Sonar 服务器应该已安装并运行。有关 Sonar 的详细信息,请参阅 www.sonarqube.org/

要运行 Sonar Runner 插件,我们只需应用插件 sonar-runner 并将其配置为连接到 Sonar 服务器。

为您的项目创建名为build_sonar.gradle的构建文件,内容如下:

apply plugin: 'groovy'
apply plugin: 'eclipse'
apply plugin: "sonar-runner"

repositories {
  mavenCentral()
}

version = '1.0'

sonarRunner {

  sonarProperties {
    property "sonar.host.url", "http://<IP_ADDRESS>:9000"
    property "sonar.jdbc.url", "jdbc:h2:tcp://<IP_ADDRESS>:9092/sonar"
    property "sonar.jdbc.driverClassName", "org.h2.Driver"
    property "sonar.jdbc.username", "sonar"
    property "sonar.jdbc.password", "sonar"
  }
}

上述配置是自我解释的。您需要添加诸如 Sonar URL、DB URL、JDBC 驱动程序详细信息之类的配置。我们的构建文件已准备就绪。下一步是在 Jenkins 服务器上配置一个作业。要在 Jenkins 中配置sonarRunner任务,我们可以添加几个基本步骤,如图 7.16 所示:

The Sonar Runner plugin

图 7.16

在这里,任务名称是sonarRunner,构建文件名称是build_sonar.gradle。现在,在 Jenkins 中执行此作业,您将在控制台找到输出。输出包含一个指向 Sonar 服务器的链接。您可以点击链接,它将重定向到如图 7.17 所示的 Sonar 报告:

The Sonar Runner plugin

图 7.17

如前所述,Sonar 对项目在不同区域进行分析,您可以在 Sonar UI 中找到详细信息。

TeamCity walk-through

在上一节中,我们学习了如何在 Jenkins 中配置 Gradle 项目以及如何集成质量插件。在本节中,我们将探索另一个流行的持续集成工具,TeamCity。我们假设 TeamCity 已经安装并运行在您的机器上。因此,我们将跳过 TeamCity 的安装和配置细节。实际上,安装过程非常简单,可以在几分钟内完成。您可以从以下 URL 下载 TeamCity:www.jetbrains.com/teamcity/download/,安装说明可在confluence.jetbrains.com/display/TCD9/Installation找到。

默认情况下,TeamCity 在http://localhost:8111/上运行,并且有一个在服务器上运行的构建代理。我们将使用 TeamCity 构建相同的插件项目。

登录 TeamCtiy 并点击创建项目。提供项目名称和描述:

TeamCity walk-through

图 7.18

保存后,然后点击创建构建配置按钮。您需要为项目提供常规设置。在常规设置之后,继续到版本控制设置

TeamCity walk-through

图 7.19

下一步是配置创建和附加新的 VCS 根。从下拉菜单中选择 Git,因为我们使用 Git 作为存储库,如图 7.20 所示:

TeamCity walk-through

图 7.20

提供插件项目的常规设置获取 URL,也提供认证,如用户名/密码和 Git 可执行文件的位置在Git 路径中。

在屏幕末尾,点击测试连接。如果连接成功,点击保存。下一步是添加构建步骤

在构建步骤中,您需要配置PluginProject构建文件细节和构建任务细节。例如,我们需要提供一些基本信息,如任务的clean build,工作目录为Chapter7/PluginProject,以及 Gradle 和 JDK 的主目录:

TeamCity 演示

图 7.21

保存此配置,然后项目将准备就绪。构建步骤的详细信息可以在构建配置屏幕中查看,如下面的截图所示:

TeamCity 演示

图 7.22

TeamCity 通过 TeamCity 代理执行项目。TeamCity 服务器与服务器一起安装了一个代理。您可以使用此代理来执行作业。否则,您可以通过代理选项卡配置更多代理。

TeamCity 演示

图 7.23

一旦配置并连接了代理,您就可以将项目与构建代理映射,然后您就可以运行构建作业了。

TeamCity 演示

图 7.24

点击运行按钮后,TeamCity 服务器将在映射的代理上执行构建作业,您可以看到构建作业的成功或失败输出。

构建日志控制台中,您还可以分析完整的日志,如下面的截图所示:

TeamCity 演示

图 7.25

摘要

在本章中,我们简要讨论了软件开发世界中持续集成的需求,并探讨了两个最受欢迎的持续集成工具:Jenkins 和 TeamCity。在本章中,我们学习了如何轻松配置这些工具以及如何将 Gradle 与这些 CI 工具集成。我们还学习了 Gradle 的三个不同质量插件:Checkstyle、PMD 和 Sonar Runner。我们借助 Jenkins 执行了这些质量任务。在持续集成、Jenkins 或 TeamCity 中还有许多主题需要学习。不幸的是,我们无法在本书中涵盖所有这些主题。我们强烈建议读者在每个未覆盖的领域进行进一步阅读。

在下一章中,我们将讨论从 Ant 和 Maven 到 Gradle 的不同迁移策略。这将有助于将现有的 Ant 或 Maven 脚本迁移到 Gradle。

第八章。迁移

如果你来自 Ant 或 Maven 背景,首先想到的问题可能是:为什么是 Gradle?我们已经在初始章节中讨论了这个问题。然后,另一个重要的问题出现了。我们已经在 Ant 或 Maven 中编写了大量的构建代码;现在,如果需要在新脚本中写入 Gradle,管理两个构建工具不会很困难吗?在本章中,我们将解释将现有的 Ant 或 Maven 脚本迁移到 Gradle 构建脚本的不同技术。在本章的第一节中,我们将讨论可以应用于从 Ant 迁移到 Gradle 的不同策略,而后续章节将涵盖从 Maven 迁移到 Gradle 的策略。

从 Ant 迁移

Ant 是最初成为开发者中非常受欢迎的构建工具之一,因为它们可以控制构建过程的每个步骤。但是,编写每个步骤意味着在构建文件中有很多样板代码。Ant 初始版本中缺少的另一个特性是依赖关系管理的复杂性,后来通过引入 Ivy 依赖关系管理器得到了简化。对于 Ant 用户来说,切换到使用 Gradle 作为他们的构建工具非常简单且容易。Gradle 通过 Groovy 的 AntBuilder 提供了对 Ant 的直接集成。在 Gradle 世界中,Ant 任务被视为一等公民。在接下来的几节中,我们将讨论三种不同的策略:将 Ant 文件导入 Gradle、AntBuilder 类的脚本使用以及重写为 Gradle。这些策略可以遵循从 Ant 迁移到 Gradle。

导入 Ant 文件

这是将 Ant 脚本与 Gradle 脚本集成的一种最简单的方法。作为迁移的第一步,当您有很多用 Ant 编写的构建脚本,并且希望在不改变当前构建结构的情况下开始使用 Gradle 时,它非常有用。我们将从一个示例 Ant 文件开始。假设我们有一个 Java 项目的 build.xml Ant 文件,并执行以下任务:

  1. 构建项目(编译代码并生成 JAR 文件)。

  2. 生成 JAR 文件的校验和。

  3. 创建一个包含 JAR 文件和校验和文件的 ZIP 文件。

以下是要执行所有前面提到的三个操作的 build.xml 文件:

<project name="sampleProject" default="makeJar" basedir=".">

<property name="src" location="src/main/java"/>
<property name="build" location="build"/>
<property name="classes" location="build/classes"/>
<property name="libs" location="build/libs"/>
<property name="distributions" location="build/distributions"/>
<property name="version" value="1.0"/>

<target name="setup" depends="clean">
   <mkdir dir="${classes}"/>
   <mkdir dir="${distributions}"/>
</target>

<target name="compile" depends="setup" description="compile the source">
   <javac srcdir="${src}" destdir="${build}/classes"  includeantruntime="false"/>
</target>
<target name="makeJar" depends="compile" description="generate the distributions">
   <jar jarfile="${libs}/sampleproject-${version}.jar" basedir="${classes}"/>
</target>
<target name="clean" description="clean up">
   <delete dir="${build}"/>
</target>

<target name="zip" description="zip the jar and checksum" depends="makeJar,checksum">
   <zip destfile="${distributions}/sampleproject.zip" filesonly="true" basedir="${libs}" includes="*.checksum,*.jar"  />
</target>

<target name="checksum" description="generate checksum and store in file" depends="makeJar">
   <checksum file="${libs}/sampleproject-${version}.jar" property="sampleMD5"/>
   <echo file="${libs}/sampleproject.checksum" message="checksum=${sampleMD5}"/>
</target>

<target name="GradleProperties">
<echo message="Gradle comments are:: ${comments}"/>
</target>

</project>

要构建项目,您需要运行以下目标(在 Ant 中,我们执行一个可以与 Gradle 任务相比的目标):

SampleProject$ ant makeJar
Buildfile: <path>/Chapter8/SampleProject/build.xml

clean:
 [delete] Deleting directory <path>/Chapter8/SampleProject/build

setup:
 [mkdir] Created dir: <path>/Chapter8/SampleProject/build/classes
 [mkdir] Created dir: <path>/Chapter8/SampleProject/build/distributions

compile:
 [javac] Compiling 2 source files to <path>/Chapter8/SampleProject/build/classes

makeJar:
 [jar] Building jar: <path>/Chapter8/SampleProject/build/libs/sampleproject-1.0.jar

BUILD SUCCESSFUL
Total time: 0 seconds

目标将执行其他所需的目标,如编译、清理、设置等。这将生成 sampleproject-1.0.jar 文件在 build/libs 目录中。现在,为了生成 JAR 的校验和并将其与 JAR 文件捆绑在一起,我们可以运行以下目标:

$ ant zip

此目标将运行 makeJar 目标以及所有其他依赖目标来创建 JAR 文件,然后它将执行校验和目标以生成 JAR 文件的 md5 校验和。最后,ZIP 任务将打包校验和文件和 JAR 文件,并在 build/distributions 目录内创建一个 ZIP 文件。

这是一个 Java 项目的示例构建文件;你可以根据定制需求添加额外的目标。我们可以简单地在这个 Gradle 构建文件中导入这个 Ant 构建文件,并能够执行 Ant 目标。构建文件的内容如下所示:

ant.importBuild 'build.xml'

这一行足以导入 Ant 构建文件并继续使用 Gradle。现在尝试执行以下操作:

$ gradle -b build_import.gradle zip
::clean
:setup
:compile
:makeJar
:checksum
:zip

BUILD SUCCESSFUL

在这里,我们将构建文件命名为 build_import.gradle。前面的命令依次执行了所有 Ant 任务。你可以在 build/distributions 目录中找到创建的 ZIP 文件。

这是从 Ant 迁移到 Gradle 的第一步之一。如果你不想玩现有的构建脚本而想使用 Gradle,这将非常有帮助。只需将 Ant 文件导入 Gradle 构建文件,就可以开始使用。

访问属性

Gradle 还允许你访问现有的 Ant 属性并添加新属性。要访问现有的 Ant 属性,你可以使用 ant.properties,如下所示:

ant.importBuild 'build.xml'

def antVersion = ant.properties['version']
def src = ant.properties['src']

task showAntProperties << {
      println "Ant Version is "+ antVersion
      println "Source location is "+ src

}
$ gradle -b build_import.gradle sAP

:showAntProperties
Ant Version is 1.0
Source location is D:\Chapter8\SampleProject\src\main\java

BUILD SUCCESSFUL

在这里,Gradle 脚本已从 Ant 文件中获取属性值,并且我们在 Gradle 任务中打印这些值。以类似的方式,我们可以在 Gradle 中设置 Ant 属性并在 Ant 构建文件中访问这些属性。

使用以下语句更新构建文件:

ant.properties['comments'] = "This comment added in Gradle"

此属性将由 Ant 文件中的 GradleProperties 目标读取,如下所示:

<target name="GradleProperties">
   <echo message="Gradle comments are ${comments}"/>
</target>

现在,在执行 GradleProperties 目标时,我们可以在控制台输出中找到显示的注释属性,如下代码片段所示:

$ gradle  -b build_import.gradle GradleProperties

Starting Build
...
Executing task ':GradleProperties' (up-to-date check took 0.015 secs) due to:
 Task has not declared any outputs.
[ant:echo] Gradle comments are:: This comments added in Gradle
:GradleProperties (Thread[main,5,main]) completed. Took 0.047 secs.

BUILD SUCCESSFUL

更新 Ant 任务

Gradle 还允许你增强现有的 Ant 任务。同样,我们可以使用 doFirstdoLast 闭包来增强任何现有的 Gradle 任务;Ant 任务也可以以类似的方式扩展。在构建文件(文件:build_import.gradle)中添加以下语句以将 doFirstdoLast 闭包添加到 GradleProperties 任务中:

GradleProperties.doFirst {
   println "Adding additional behavior before Ant task operations"
}
GradleProperties.doLast {
   println "Adding additional behavior after Ant Task operations"
}

现在,GradleProperties 目标执行了 doFirstdoLast 闭包,控制台输出如下所示:

$ gradle -b build_import.gradle GP

Starting Build
……
:GradleProperties (Thread[main,5,main]) started.
:GradleProperties
Executing task ':GradleProperties' (up-to-date check took 0.003 secs) due to:
 Task has not declared any outputs.
Adding additional behavior before Ant task operations
[ant:echo] Gradle comments are:: This comments added in Gradle
Adding additional behavior after Ant Task operations
:GradleProperties (Thread[main,5,main]) completed. Took 0.158 secs.

BUILD SUCCESSFUL

使用 AntBuilder API

我们已经看到,将 Ant 的 build.xml 导入 Gradle 并将 Ant 目标作为 Gradle 任务使用是多么简单。另一种方法是使用 AntBuilder 类。使用 AntBuilder,你可以在 Gradle 脚本中调用 Ant 任务。在 Gradle 构建文件中有一个名为 'ant' 的 AntBuilder 类实例可用。使用此实例,当我们调用方法时,它实际上执行了一个 Ant 任务。

在以下示例中,我们将使用相同的 build.xml 文件,并解释如何使用 AntBuilder 重写任务以使用 Gradle:

  1. 设置属性:

    Ant 方式 使用 AntBuilder

    |

    <project 
    name="qualitycheck" default="makeJar" 
    basedir=".">
    
    <property name="src" location="src/main/java"/>
    <property name="build" location="build"/>
    <property name="lib" location="lib"/>
    <property name="dist" location="dist"/>
    <property name="version" value="1.0"/>
    

    |

    defaultTasks "makeJar"
    
    def src = "src/main/java"
    def build = "build"
    def libs = "build/libs"
    def classes = "build/classes"
    def distributions = "build/distributions"
    def version = 1.0
    

    |

  2. 清理构建目录:

    Ant 方式 使用 AntBuilder

    |

    <delete dir="${build}"/>
    

    |

    ant.delete(dir:"${build}")
    

    |

  3. 创建新目录:

    Ant 方式 使用 AntBuilder

    |

    <mkdir dir="${classes}"/>
    <mkdir dir="${distributions}"/>
    

    |

    ant.mkdir(dir:"${libs}")
    ant.mkdir(dir:"${classes}")
    

    |

  4. 编译 Java 代码:

    Ant 方式 使用 AntBuilder

    |

    <javac srcdir="${src}"
    destdir="${build}/classes" 
    includeantruntime="false"/>
    

    |

    ant.javac(srcdir:"${src}",
    destdir:"${classes}",
    includeantruntime:"false")
    

    |

  5. 从编译后的源代码创建 JAR 文件:

    Ant 方式 使用 AntBuilder

    |

    <jar jarfile 
    ="${libs}/sampleproject-${version}.jar" 
    basedir="${classes}"
    />
    

    |

    ant.jar(
    destfile: "${libs}/sampleproject-${version}.jar",
    basedir:"${classes}") 
    

    |

  6. 为 JAR 生成校验和:

    Ant 方式 使用 AntBuilder

    |

    <checksum file="${libs}/sampleproject-${version}.jar" 
    property="sampleMD5"/>
    
    <echo file ="${libs}/sampleproject.checksum" message="checksum=${sampleMD5}"
    />
    

    |

    ant.checksum(
    file:"${libs}/sampleproject-${version}.jar",
    property:"sampleMD5"
    )
    
    ant.echo(file:"${libs}/sampleproject.checksum",
    message:"checksum=${ant.sampleMD5}"
    )
    

    |

  7. 将校验和文件和 JAR 文件打包成 ZIP 文件:

    Ant 方式 使用 AntBuilder

    |

    <zip 
    destfile ="${distributions}/sampleproject.zip" 
    filesonly="true" basedir="${libs}" includes="*.checksum,*.jar" 
     />
    

    |

    ant.zip(destfile: "${dist}/sampleproject.zip",
    basedir:"dist") 
    

    |

因此,完整的构建文件将如下所示:

defaultTasks "makeJar"

def src="img/java"
def build="build"
def libs="build/libs"
def classes = "build/classes"
def distributions="build/distributions"
def version=1.0

task setup(dependsOn:'clean') << {
   ant.mkdir(dir:"${libs}")
   ant.mkdir(dir:"${classes}")
}

task clean << {
   ant.delete(dir:"${build}")
}

task compileProject(dependsOn:'setup') << {
   ant.javac(srcdir:"${src}",destdir:"${classes}",
includeantruntime:"false")
}

task makeJar << {
   ant.jar(destfile: "${libs}/sampleproject-${version}.jar",
basedir:"${classes}") 
}

task zip(dependsOn:'checksum') << {
   ant.zip(destfile: "${distributions}/sampleproject.zip",
basedir:"${libs}")
}

task checksum(dependsOn:'makeJar') << {
   ant.checksum(file:"${libs}/sampleproject-${version}.jar",
property:"sampleMD5")
   ant.echo(file:"${libs}/sampleproject.checksum",
message:"checksum=${ant.sampleMD5}")
}

makeJar.dependsOn compileProject

现在,执行 ZIP 任务并检查分发目录。您将找到以下创建的 sampleproject.zip 文件:

$ gradle -b build_ant.gradle zip
:clean
:setup
:compileProject
:makeJar
:checksum
:zip

BUILD SUCCESSFUL

注意这里,AntBuilder 对于尚未迁移到 Gradle 的自定义 Ant taskdef 任务非常有用。

重写为 Gradle

到目前为止,我们已经看到将 Ant 文件导入 Gradle 脚本是多么容易。我们还探讨了另一种方法,即使用 AntBuilder 实例在从 Ant 迁移到 Gradle 的过程中复制相同的行为。现在,在第三种方法中,我们将用 Groovy 重新编写 Ant 脚本。

我们将继续使用相同的 Ant build.xml 文件,并将其转换为 Gradle 构建脚本。在这个例子中,我们正在构建一个 Java 项目。正如我们所知,要构建 Java 项目,Gradle 已经为我们提供了一个 Java 插件;您只需在构建文件中应用 Java 插件即可。Java 插件将负责所有标准约定和配置。

以下是我们已经在 第四章 中讨论过的 Java 插件的某些约定,插件管理

使用的约定 描述
build 默认构建目录名称
build/libs 默认 JAR 文件位置
src/main/java; src/test/java Java 源文件位置
项目名称 归档文件名

如果项目也遵循这些约定,我们就不需要为项目编写任何额外的配置。唯一需要的配置是定义版本属性;否则,将创建不带版本信息的 JAR 文件。

因此,我们新的构建脚本将如下所示:

apply plugin :'java'
version = 1.0

现在,我们已经完成了。不需要编写任何脚本来创建和删除目录、编译文件、创建 JAR 任务等。在执行构建命令后,您可以在 build/libs 目录中找到 <projectname>-<version>.jar

$ gradle build

:clean
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

将大约 30 行 Ant 代码缩减为两行 Gradle 代码真是太容易了。这使我们摆脱了所有样板代码,并专注于主要逻辑。然而,并非所有项目都可以通过应用插件或遵循某些约定简单地转换。如果项目不遵循 Gradle 或 Maven 约定,您可能需要配置 sourceSets 和其他配置。

回到示例,我们只创建了 JAR 文件;还有两个任务待完成。我们必须生成一个文件来存储校验和,我们需要将校验和文件和 JAR 文件捆绑到一个 ZIP 文件中。我们可以定义两个额外的任务来完成此操作,如下所示:

apply plugin:'java'
version = 1.0

task zip(type: Zip) {
    from "${libsDir}"
    destinationDir project.distsDir
}

task checksum << {
	ant.checksum(file:"${libsDir}/${project.name}-${version}.jar",property:"sampleMD5")
	ant.echo(file:"${libsDir}/${project.name}.checksum",message:"checksum=${ant.sampleMD5}")
}

zip.dependsOn checksum
checksum.dependsOn build

在前面的构建脚本中,校验和任务将为 jar 文件创建校验和。这里我们再次使用 Ant。校验和任务创建校验和,因为这是 Gradle 中最简单的方式。我们已经配置了 ZIP 任务(类型为 ZIP)以创建 ZIP 文件。Gradle 已经为构建/分发目录提供了约定,即 project.distsDir

$ gradle clean zip

:clean
:compileJava
:processResources UP-TO-DATE
:classes
....
:check UP-TO-DATE
:build
:checksum
:zip

BUILD SUCCESSFUL

配置

如果你不想遵循约定,Gradle 提供了一种简单的方法来根据需求配置项目。我们将展示如何配置之前创建的 Ant 任务在 Gradle 中:

  1. 清理构建目录:

    Ant 方式 Gradle 方式

    |

    <target 
    name="clean" description="clean up">
    
       <delete dir =   "${build}"/>
    
        <delete dir = "${dist}"/>
    </target>
    

    |

    task cleanDir(type: Delete) {
      delete "${build}"
    }
    

    |

  2. 创建新目录:

    Ant 方式 Gradle 方式

    |

    <target 
    name="setup" depends="clean">
    
       <mkdir dir =  "${build}"/>
    </target>
    

    |

    task setup(dependsOn:'cleanDir') << {
            def classesDir = file("${classes}")
            def distDir = file("${distributions}")
            classesDir.mkdirs()
            distDir.mkdirs()
    }
    

    |

  3. 编译 Java 代码:

    Ant 方式 Gradle 方式

    |

    <target name="compile" depends="setup" description="compile the source">
        <javac srcdir="${src}" destdir="${build}" />
    </target>
    

    |

    compileJava {
            File classesDir = file("${classes}")
            FileTree srcDir = fileTree(dir: "${src}")
            source srcDir
            destinationDir classesDir
    }
    

    |

  4. 将编译的类 JAR 化:

    Ant 方式 Gradle 方式

    |

    <target name="dist" depends="compile" description="generate the distribution">
       <mkdir dir="${dist}"/>
       <jar jarfile="${dist}/sampleproject-${version}.jar" basedir="${build}"/>
    </target>
    

    |

    task myJar(type: Jar) {
            manifest {
            attributes 'Implementation-Title': 'Sample Project',
                    'Implementation-Version': version,
                    'Main-Class': 'com.test.SampleTask'
        }
        baseName = project.name +"-" +version
        from "build/classes"
        into project.libsDir
    }
    

    |

因此,带有配置的最终构建文件(build_conf.gradle)将如下所示:

apply plugin:'java'

def src="img/java"
def build="$buildDir"
def libs="$buildDir/libs"
def classes = "$buildDir/classes"
def distributions="$buildDir/distributions"
def version=1.0

task setup(dependsOn:'cleanDir') << {
   def classesDir = file("${classes}")
   def distDir = file("${distributions}")
   classesDir.mkdirs()
   distDir.mkdirs()
}

task cleanDir(type: Delete) {
  delete "${build}"
}

compileJava {
   File classesDir = file("${classes}")
   FileTree srcDir = fileTree(dir: "${src}")
   source srcDir
   destinationDir classesDir
}
task myJar(type: Jar) {
   manifest {
        attributes 'Implementation-Title': 'Sample Project',  
        	'Implementation-Version': version,
        	'Main-Class': 'com.test.SampleTask'
    }
    baseName = project.name +"-" +version
    from "build/classes"
    into project.libsDir
}
task zip(type: Zip) {
    from "${libsDir}"
    destinationDir project.distsDir
}
task checksum << {
   ant.checksum(file:"${libsDir}/${project.name}-${version}.jar",
   property:"sampleMD5")
   ant.echo(file:"${libsDir}/${project.name}.checksum",
   message:"checksum=${ant.sampleMD5}")
}

myJar.dependsOn setup
compileJava.dependsOn myJar
checksum.dependsOn compileJava
zip.dependsOn checksum

现在,尝试执行 ZIP 命令。你可以在 build/libs 目录中找到创建的 JAR 文件和校验和文件,以及构建/分发目录中的 ZIP 文件:

$ gradle -b build_conf.gradle zip
:cleanDir
:setup
:myJar
:compileJava
:checksum
:zip

BUILD SUCCESSFUL

从 Maven 迁移

随着企业软件中 Ant 文件的大小和复杂性的增加,开发者开始寻找更好的解决方案。Maven 很容易成为解决方案,因为它引入了约定优于配置的概念。如果你遵循某些约定,通过跳过样板代码可以节省大量时间。Maven 还提供了一种依赖管理解决方案,这是 Ant 工具的主要缺点之一。Ant 没有提供任何依赖管理解决方案,而 Maven 带有内置的依赖管理器。

当我们讨论从 Ant 迁移到 Gradle 的策略时,你了解到最简单的解决方案是导入 Ant 的 build.xml 文件并直接使用它。对于 Maven 迁移,我们没有这样的功能。Maven 用户可能会发现从 Maven 迁移到 Gradle 很容易,因为两者遵循这些共同原则:

  • 约定优于配置

  • 依赖管理解决方案

  • 仓库配置

要从 Maven 迁移到 Gradle,我们需要编写一个新的 Gradle 脚本,以模拟其功能。如果你已经使用过 Maven,你可能已经注意到 Gradle 使用了大多数 Maven 概念;因此,从 Maven 迁移到 Gradle 不会非常困难。主要区别之一是 Gradle 使用 Groovy 作为构建脚本语言,而 Maven 使用 XML。在本节中,我们将讨论一些将 Maven 脚本转换为 Gradle 脚本的最常见任务。

构建文件名和项目属性

GroupIdartifactIdversion 是用户在 Maven pom 文件中需要提供的最小必需属性,而在 Gradle 中这些不是必需的。如果用户没有配置,则假定默认值。始终建议指定这些值以避免任何冲突:

Maven 方法 Gradle 方法

|

  • <groupId>ch8.example`

  • <artifactId>SampleMaven</artifactId>

  • <version>1.0</version>

  • <packaging>jar</packaging>

|

  • groupid:不必要。

  • artifactId:默认值为项目目录名。

  • version:如果未提及,则将创建不带版本的组件。

  • 打包取决于你在构建脚本中应用的插件。

|

提示

如果 Maven 中没有提及打包,则默认为 JAR。其他核心打包方式包括:pomjarmaven-pluginejbwarearrarpar

属性

以下是在 Maven 和 Gradle 中定义属性的方法:

Maven 方法 Gradle 方法

|

<properties>
     <src>src/main/java
     </src>
    <build>build</build>
     <classes>
       build/classes
    </classes>
    <libs>
        build/libs
    </libs>
<distributions>
     build/distributions
     </distributions>
    <version>
        1.0
     </version>
</properties>

|

def src="img/java"
def build="build"
def lib="lib"
def dist="dist"
def version=1.0

|

依赖管理

Maven 和 Gradle 都提供了依赖管理功能。它们会自行管理依赖。你不需要担心项目中任何二级或三级依赖。类似于 Maven 的范围(如编译和运行时),Gradle 也提供了不同的配置,例如编译、运行时、testCompile 等。以下表格列出了 Maven 和 Gradle 支持的范围:

Maven 范围 Gradle 范围

|

  • 编译

  • provided

  • 运行时

  • 测试

  • system

|

  • 编译

  • providedCompile

  • runtime

  • providedRuntime

  • testCompile

  • testRutime

|

与 Maven 中的提供范围相比,Gradle 的 war 插件增加了两个额外的范围,providedCompileprovidedRuntime。对于测试用例,Gradle 也提供了两个范围,testCompiletestRuntime。以下是如何在定义依赖时使用范围的一个示例:

Maven 方法 Gradle 方法

|

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
<scope>compile</scope>
</dependency>

|

dependencies {
    compile group:  'org.apache.commons', name: 'commons-lang3', version:'3.1'
}

|

排除传递依赖

在 第五章 中,依赖管理,你学习了如何使用依赖管理。Maven 在依赖管理功能方面没有太大差异。以下是一个示例,展示了如何在构建工具中排除传递依赖:

Maven 方法 Gradle 方法

|

<dependencies>
    <dependency>
      <groupId>
          commons-httpclient
      </groupId>
      <artifactId>
          commons-httpclient
       </artifactId>
      <version>3.1</version>
      <exclusions>
        <exclusion>
          <groupId>
            commons-codec
           </groupId>
          <artifactId>
             commons-codec
          </artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    ...
  </dependencies>

|

dependencies{

compile('commons-httpclient:commons-httpclient:3.1') {
 exclude group:'commons-codec',
module:'commons-codec'
}

}

|

插件声明

Maven 插件是一组可以应用于项目的目标集合。插件中的目标通过mvn [plugin-name]:[goal-name]命令执行。在 Maven 中,通常有两种类型的插件:构建插件和报告插件。构建插件将在构建过程中执行,应在pom.xml<build/>元素中进行配置。报告插件将在生成站点时执行,并使用<reporting/>元素进行配置。报告插件的例子包括 Checkstyle、PMD 等。在 Gradle 中,你只需在 Gradle 脚本中添加一个插件声明,或者需要在buildscript闭包中定义它。

以下是一个示例代码,它描述了如何在 Maven 中包含插件以及我们如何在 Gradle 中定义相同的内容:

Maven 方式 Gradle 方式

|

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>

|

  • apply plugin:'<pluginid>'

  • 对于自定义插件:

buildscript {
ext {
springBootVersion = '1.2.2.RELEASE'
}
repositories {
jcenter()
maven { url "http://repo.spring.io/snapshot" }
maven { url "http://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.2.RELEASE")
}
}

|

仓库配置

仓库用于下载资源和工件,并且也用于发布工件。在 Maven 中,你可以在pom.xmlsettings.xml中定义仓库。在 Gradle 中,你可以在init脚本(init.gradle)或build.gradle文件中添加仓库:

Maven 方式 Gradle 方式

|

<repositories>
    <repository>
      <id>rep1</id>
      <name>org repo1</name>
      <url>
       http://company.repository1
       </url>
    </repository>
</repositories>

|

repositories {
maven {
url "http://company.repository1"
}
}

|

多模块声明

Maven 和 Gradle 都提供了创建多模块项目的方法,如下所示:

Maven 方式 Gradle 方式

|

<project>
  <groupId>
    com.test.multiproject
   </groupId>
   <artifactId>
     rootproject
  </artifactId>
  <version>
     1.0
  </version>
  <packaging>
     Pom
  </packaging>
  <modules>
    <module>subproject1</module>
    <module>subproject2</module>
  </modules>
</project>

| 在根项目下添加settings.gradle并按如下方式包含子项目:

include 'subproject1', 'subproject2',

|

默认值

Gradle 和 Maven 都为某些属性提供了默认值。如果你想遵循约定,可以直接使用它们,如果需要则可以更新它们:

Maven 方式 Gradle 方式
项目目录 ${project.basedir} | ${project.rootDir}
构建目录 ${project.basedir}/target | ${project.rootDir}/build
类目录 ${project.build.directory}/classes | ${project.rootDir}/build/classes
JAR 名称 ${project.artifactId}-${project.version} ${project.name}
测试输出目录 ${project.build.directory}/test-classes | ${project.testResultsDir}
源目录 ${project.basedir}/src/main/java | ${project.rootDir}/src/main/java
测试源目录 ${project.basedir}/src/test/java | ${project.rootDir}/src/test/java

Gradle init 插件

Build init插件可以从pom文件生成build.gradle文件。命令gradle init --type pomgradle init会在有有效pom.xml文件的项目或目录中创建 Gradle 构建文件和其他工件。

我们创建了一个项目,SampleMaven,该项目包含位于根项目目录下的src\main\java\ch8目录中的 Java 文件和pom.xml文件。以下为pom.xml文件的内容:

<project 

   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
   http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>ch8.example</groupId>
   <artifactId>SampleMaven</artifactId>
   <version>1.0</version>
   <packaging>jar</packaging>
   <dependencies>
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
       <version>3.1</version>
       <scope>compile</scope>
     </dependency>
   </dependencies>
 </project>

现在,执行以下命令:

$ gradle init --type pom
:wrapper
:init
Maven to Gradle conversion is an incubating feature.

BUILD SUCCESSFUL

注意,从 Maven 到 Gradle 是一个孵化特性。当前的 DSL 和其他配置可能在将来发生变化。执行前面的命令后,你将在项目目录中找到build.gradlesettings.gradle和 Gradle 包装文件。自动生成的build.gradle文件包含以下内容。它自动添加插件详情,以及组和版本信息。

系统生成的构建文件内容如下所示(以下代码片段):

apply plugin: 'java'
apply plugin: 'maven'

group = 'ch8.example'
version = '1.0'
description = """"""

sourceCompatibility = 1.5
targetCompatibility = 1.5

repositories {
     maven { url "http://repo.maven.apache.org/maven2" }
}

dependencies {
    compile group: 'org.apache.commons', name: 'commons-lang3', version:'3.1'
}

此插件还支持多项目构建、仓库配置、依赖管理以及许多其他功能。

摘要

在本章中,我们讨论了从 Ant 和 Maven 迁移到 Gradle 的过程。这在任何组织中都是一个非常常见的场景,其中现有的构建脚本是用 Ant 或 Maven 编写的,并且正在尝试升级到 Gradle。本章提供了一些分析和不同的方法,这些方法可能有助于更好地、更有组织地规划 Gradle 迁移。

在下一章中,我们将讨论使用 Docker 构建自动化的部署方面。

第九章:部署

如果我们不谈论软件组件的部署方面,那么一本 Gradle 书籍将是不完整的。在我看来,在软件工程中,在构建自动化之后的下一个最合逻辑的步骤就是部署。部署本身是一个不同的领域,这与 Gradle 关系不大。但仍然我认为讨论构建和部署工具是有意义的,这样读者就可以对构建部署测试工作流程有一个概述。在本章中,我们将讨论部署的一些基础知识,以了解构建和部署过程。我们将学习如何使用 Gradle、Jenkins 和 Docker 等工具一起创建构建、部署和测试工作流程。在我们开始之前,我们必须理解什么是部署。部署与构建过程在软件生命周期中同样重要。你可以编写和构建出色的软件,但如果应用程序没有部署,它就不会产生太多价值。软件的部署不仅仅是安装软件并启动它。它因应用程序而异,因操作系统而异。某些应用程序只需将 JAR 文件复制到特定位置即可部署;某些应用程序需要在 Web 容器或外部容器中部署等。我们可以将软件的部署过程概括如下:

  1. 准备你想要部署应用程序的先决硬件和软件环境。

  2. 在准备好的环境中复制项目资产。

  3. 根据环境配置资产。

  4. 准备应用程序的生命周期,如启动、停止、重启等。

  5. 对应用程序进行合理性检查,以验证其功能。

所以,部署不仅仅是复制资产并通知每个人应用程序已准备好使用。它还涉及许多其他的前置和后置步骤。部署过程也随着开发过程的发展而发展,并且仍在随着新技术的发展而演变。曾经有一段时间,运维团队会在指定的节点上手动部署应用程序,配置负载均衡机制和从盒子到盒子的路由,以有效地处理客户端请求。现在,借助新的云基础设施,例如基础设施即服务IaaS)或各种自动化工具,只需一键或一些命令,开发者就可以在一个盒子、集群环境、基于云的环境或容器化环境中部署应用程序。在本章中,我们将重点关注使用 Docker(一种应用程序容器化技术)的部署过程。我们将详细探讨 Docker 的不同方面,如安装、配置;Docker 相对于虚拟服务器节点部署的优势;在 Docker 内部部署应用程序;以及如何使其对外界可用。

Gradle 在部署中的作用

Gradle 在构建和部署过程中扮演着重要的角色。开发者可以根据需求组合使用不同的工具来自动化整个构建和部署过程。例如,Jenkins、Puppet、Chef 和 Docker 等工具有助于创建构建和部署基础设施。但对于非常简单的部署,Gradle 的一些功能可能很有用。Gradle 提供了一系列任务,可以自动化一些之前提到的部署任务。以下是一些有用的任务:

  1. 下载任务用于下载工件(ZIP、WAR、EAR 等)及其依赖项。

    您可以通过仅将列表添加到依赖项闭包中来下载工件。以类似的方式,您可以下载运行软件所需的所有其他依赖项。不需要将软件及其所有依赖项捆绑在一起,使其变得笨重。在安装软件时下载依赖项以使其轻量级是很好的。

  2. 解压或解 tar 任务以解压工件。

    一旦下载了工件及其依赖项,下一步(如果需要)就是解压或解 tar 工件。

  3. 配置应用程序。

    可以通过添加自定义任务在 Gradle 中完成应用程序的配置或本地化。

  4. 启动/停止应用程序。

    可以使用现有的 Gradle 任务(如 JavaExec 或任何其他自定义任务)来启动/停止应用程序。

在我看来,尽管这些任务可以在 Gradle 中自动化,但更好的替代方案可能是 Shell 脚本或 Perl 等脚本语言。在本章的后面部分,当我们创建构建和部署管道的示例时,Gradle 将仅作为纯构建和测试工具。我们不会探索任何特定于部署的任务或插件。现在,我们将继续讨论下一个主题,Docker,它在近年来随着微服务架构的出现而变得非常流行。

Docker 概述

Docker 是一种开源的基于容器的虚拟化技术,有助于在容器内自动化应用程序的部署。Docker 使用 Linux 内核的资源隔离功能,如 cgroups 和内核命名空间,并允许在主机机器上独立且相互隔离地运行多个容器。与虚拟机相比,Docker 的优势在于它是一个轻量级的过程,与虚拟机相比,它提供了资源共享同一内核时的资源隔离,包括主机机的驱动程序。Docker 是开源技术,支持不同的平台。由于 Docker 建立在 Linux 内核之上,它通过Boot2Docker应用程序支持 Windows 和 Mac。

Docker 的一些主要特性包括:

  • Docker 引擎:轻量级容器,用于创建、管理和容器化应用程序。

  • 可移植性:其中一个重要特性是容器重用。您可以准备一个 Tomcat 镜像,并将此镜像用作所有其他 Web 应用程序的基础镜像。此镜像可以部署在任何系统上,如桌面、物理服务器、虚拟机,甚至云平台。

  • Docker Hub:Docker 还有一个基于 SaaS 的全球共享公共注册表。您可以找到不同种类的镜像,如 MySQL、Tomcat、Java、Redis 和其他技术。用户可以创建和上传镜像到这个存储库。

  • 更快的交付:与虚拟机相比,Docker 容器非常快。这个特性有助于减少开发、测试和部署的时间。

  • API:Docker 支持一个用户友好的 API 来管理 Docker 容器。

您可能已经在组织的基础设施中使用虚拟机。Docker 与虚拟机非常不同。虚拟机有自己的操作系统和设备驱动程序、内存、CPU 份额等。另一方面,容器与宿主操作系统共享,并且与其他宿主上的容器共享大部分这些资源。

让我们看看 Docker 和虚拟机之间的一些区别:

  • Docker 使用 Linux 容器,这些容器共享相同的操作系统,而每个虚拟机都有自己的操作系统,这增加了开销

  • Docker 使用另一个联合文件系统(AUFS),这是一个分层文件系统。它有一个所有容器共享的只读部分,以及每个容器独有的写入部分,用于写入自己的数据

  • Docker 是一种轻量级技术,它自己需要的资源最少,因为它共享了最多的资源,而完整的虚拟机系统共享最少的资源,并获取大部分自己的资源

  • 与虚拟机相比,Docker 的启动时间非常短。

  • Docker 主要适用于小型应用程序(微服务),这些应用程序可以共享公共资源,并通过一些进程进行隔离,而虚拟机则适用于需要完全隔离资源的重型应用程序

在接下来的两个部分中,我们将处理 Docker 安装,然后我们将学习一些最常用的 Docker 命令。

安装 Docker

要在 Ubuntu Trusty 14.04 LTS 上安装 Docker,可以使用以下命令:

$ sudo apt-get update
$ sudo apt-get -y install docker.io

或者,要获取 Docker 的最新版本,可以使用以下方法:

$ sudo wget -qO- https://get.docker.com/ | sh

要了解安装的版本,您需要运行docker version命令,如下所示:

$ docker version
Client version: 1.6.0
Client API version: 1.18
Go version (client): go1.4.2
Git commit (client): 4749651
...

Docker 也支持在 Mac OS X、Windows 或云平台上运行。这些平台的 Docker 安装指南可在docs.docker.com/找到。

要验证安装,您可以执行docker run hello-world命令。此命令下载一个测试镜像并在容器中运行命令:

$ docker run hello-world
Unable to find image 'hello-world' locally
Pulling repository hello-world
91c95931e552: Download complete 
a8219747be10: Download complete 
Hello from Docker.

如果在控制台上显示先前的消息,则表示安装成功。在下一节中,我们将学习一些有用的 Docker 命令。

Docker 命令

一旦在主机上安装了 Docker,它将以守护进程的形式运行。提供给用户的界面是 Docker 客户端。Docker 守护进程与用户之间的通信通过 Docker 客户端进行。Docker 为不同的需求提供了各种命令,这有助于非常容易地自动化部署过程。现在我们将学习不同的 Docker 命令。由于这不是一本 Docker 书,所以讨论将限于一些基本命令。您可以参考以下 Docker 网站,以获取完整的参考指南:docs.docker.com/reference/

帮助命令

一旦安装了 Docker,要查看所有支持的命令列表,您可以输入 docker help

此命令列出所有可用的 Docker 命令。Docker 命令的基本语法是 docker <options> command <argument>

下载镜像

如我们之前所述,Docker 提供了自己的公共仓库,您可以从那里下载镜像以开始使用 Docker。除非需要,否则您不需要通过创建镜像来重新发明轮子。在仓库中,您可以找到许多镜像,从简单的操作系统镜像到嵌入 Java、Tomcat、MySQL 等的镜像。要从仓库中下载镜像,您可以使用 docker pull <image name> 命令,如下所示:

$ docker pull ubuntu

latest: Pulling from ubuntu

e9e06b06e14c: Pull complete 
a82efea989f9: Pull complete 
37bea4ee0c81: Pull complete 
...

默认情况下,此命令从公共 Docker 仓库拉取镜像,但您也可以配置私有仓库。

镜像列表

一旦下载了镜像,您可以使用 docker images 命令查找镜像列表,如下所示:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED VIRTUAL SIZE
<none>              <none>              07f8e8c5e660        14 hours ago        188.3 MB
python              2.7                 912046e33f03        8 days ago          747.9 MB
ubuntu              latest              d0955f21bf24        6 weeks ago         188.3 MB

它将列出文件系统中所有可用的下载镜像。您可以使用镜像创建一个或多个容器。

创建容器

一旦下载了镜像,您可以使用 Docker 的 run 命令创建容器,如下所示:

$ docker run -dit --name "testUbuntu1" ubuntu /bin/bash
b25a9d5806a71f411631c4bb5c4c2dd4d059d874a24fee2210110ac9e8c2909a

此命令从 Ubuntu 镜像创建一个名为 testUbuntu1 的容器,我们提到的命令是 /bin/bash,仅用于执行 shell 或命令行界面。此命令的输出是容器 ID。您可以通过容器名称 tesUbuntu1 或容器 ID 访问该容器。

这里 -d 选项将以守护进程的形式启动,-i 选项是用于交互式,-t 选项是分配伪 TTY。让我们按照以下方式创建另一个容器:

$ docker run -dit --name "testUbuntu2" ubuntu /bin/bash
f9cdd046cbf47f957ef972690592245f27784f5f79ded6ca836afab54b4f9a8f

它将创建另一个名为 testUbuntu2 的容器。您可以通过提供不同的名称来创建具有相同镜像的多个容器。如果您没有指定任何名称,Docker 将分配一些默认名称。运行命令的语法是 $ docker run <options> <imagename> <command>

容器列表

要查找正在运行的容器列表,请使用 Docker 的 ps 命令,如下所示:

$ docker ps 
CONTAINER ID      IMAGE             COMMAND         CREATED         STATUS            PORTS             NAMES
b25a9d5806a7      ubuntu:latest     /bin/bash         2 minutes ago   Up 2 minutes                          testUbuntu1 

在这里,我们创建了两个容器,但输出只显示了正在运行的容器 testUbuntu1。现在使用 –a 选项运行相同的命令,如下所示:

$ docker ps -a
CONTAINER ID      IMAGE             COMMAND           CREATED         STATUS                   PORTS             NAMES
f8148e333eb3      ubuntu:latest     echo hello world  7 seconds ago   Exited (1) 7 seconds ago                     testUbuntu2 
b25a9d5806a7      ubuntu:latest     /bin/bash         3 minutes ago   Up 3 minutes                                 testUbuntu1

输出列出了所有容器及其各自的状态。注意testUbuntu2容器已退出,即已停止,而testUbuntu1仍在运行。

启动/停止容器

一旦从图像创建容器,可以使用以下命令启动/停止容器:

$ docker start|stop containername|containerid

以下是一个前述命令的示例:

$ docker stop testUbuntu1
$ docker start f8148e333eb3

连接到容器

如果您已启动容器,然后想要连接到正在运行的容器控制台,可以使用 Docker 的attach命令,如下所示:

$ docker attach testUbuntu1
[Enter]
root@b25a9d5806a7:/#

使用Ctrl + P + Q退出容器。exit^C命令将带您退出容器,并且还会通过杀死所有运行进程来停止正在运行的容器。如果您只想退出容器而不停止,请使用Ctrl + P + Q。这些命令可能因操作系统而异。有关更多详细信息,请参阅 Docker 文档。

删除容器

Docker 的rm命令从机器中删除或移除容器,如下所示:

$ docker rm testUbuntu2
testUbuntu2

您可以通过运行docker ps -a命令来检查是否已正确删除:

删除图像

要从系统中删除图像,请使用docker rmi命令。此命令将从机器中删除图像。在删除图像之前,您需要停止任何正在运行的容器。操作如下:

$ docker rmi ubuntu

将文件复制到容器

使用 UNIX 的cp命令,可以从主机复制文件到容器。例如,以下命令将主机系统中的dir1文件夹复制到容器的/home/mycontents目录。在这里,我们必须提供主机机器上安装的容器的绝对路径:

$sudo cp -r dir1 /var/lib/docker/aufs/mnt/b25a9d5806a71f411631c4bb5c4c2dd4d059d874a24fee2210110ac9e8c2909a/home/mycontents/

但这不是一个好的做法。替代方案是在创建容器时使用–v选项挂载目录:

$ docker run -ditP --name testUbuntu -v /home/user1/dir1:/home/dir1 ubuntu

前述命令将创建一个名为testUbuntu的容器。该命令还将主机机器的/home/user1/dir1目录映射到容器的/home/dir1目录。

要将容器中的内容复制到主机机器,可以使用docker cp命令,如下所示:

$ docker cp testUbuntu1:/home/dir1/readme.txt .

容器详情

Docker 的inspect命令有助于找到容器运行的完整详情,如下所示:

$ docker inspect testUbuntu1
[{
  "Args": [],
  "Config": {
    "AttachStderr": false,
    "AttachStdin": false,
    "AttachStdout": false,
    "Cmd": [
    "/bin/bash"
    ],
    "CpuShares": 0,
    "Cpuset": "",
    "Domainname": "",
    "Entrypoint": null,
    "Env": [
    "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
...
...
    "PublishAllPorts": false,
    "VolumesFrom": null
  },
  "HostnamePath": "/var/lib/docker/containers/b25a9d5806a71f411631c4bb5c4c2dd4d059d874a24fee2210110ac9e8c2909a/hostname",
  "HostsPath": "/var/lib/docker/containers/b25a9d5806a71f411631c4bb5c4c2dd4d059d874a24fee2210110ac9e8c2909a/hosts",
"Id": "b25a9d5806a71f411631c4bb5c4c2dd4d059d874a24fee2210110ac9e8c2909a",
"Image": "d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07",
  "MountLabel": "",
  "Name": "/testUbuntu1",
  "NetworkSettings": {
      "Bridge": "docker0",
      "Gateway": "172.17.42.1",
      "IPAddress": "172.17.0.22",
      "IPPrefixLen": 16,
      "PortMapping": null,
      "Ports": {}
  },
  "Path": "/bin/bash",
...
...
}

它将提供容器的完整详情,例如名称、路径、网络设置、IP 地址等。

更新 DNS 设置

更新 DNS 设置,您可以编辑/etc/default/docker文件。您可以在该文件中更改代理设置和 DNS 设置。文件内容如下所示:

# Docker Upstart and SysVinit configuration file

# Customize location of Docker binary (especially for development testing).
#DOCKER="/usr/local/bin/docker"

# Use DOCKER_OPTS to modify the daemon startup options.
#DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"

# If you need Docker to use an HTTP proxy, it can also be specified here.
#export http_proxy="http://127.0.0.1:3128/"

# This is also a handy place to tweak where Docker's temporary files go.
#export TMPDIR="/mnt/bigdrive/docker-tmp"

网络是一个重要的概念,您应该花更多时间阅读有关它的内容。更多详细信息可以在docs.docker.com/articles/networking/找到。

从容器创建图像

您可能对从基础容器创建带有额外软件的新镜像感兴趣。考虑一个例子,您已经从基础 Ubuntu 镜像创建了testUbuntu1容器。然后您安装了 Tomcat 服务器,部署了 Web 应用程序,也许您还安装了一些其他必需的软件,如 Ant、Git 等。您可能希望保存所有更改以备将来使用。以下docker commit命令在这种情况下很有用:

$ docker commit  -m "Creating new image" testUbuntu1 user1/ubuntu_1

此命令将创建一个新镜像user1/ubuntu_1,它将包含基本 Ubuntu 镜像和您在该容器上安装的所有应用程序。此命令将新镜像提交到本地仓库。下次,您可以从新镜像启动容器。

$ docker run -dit --name testUbuntu_1  user1/ubuntu_1

此命令将使用之前提交的新镜像创建名为testUbuntu_1的容器。如果您在 Docker 仓库中创建了账户(registry.hub.docker.com),您甚至可以将新镜像推送到公共仓库。

在 Docker 中运行应用程序

到目前为止,我们已经学习了什么是 Docker 以及如何使用不同的命令来操作 Docker。在本节中,我们将开发一个 Web 应用程序,并将该 Web 应用程序部署到 Docker 容器中。为了简化,我们将从 Docker 仓库下载一个 Tomcat 镜像。然后,通过适当的端口映射启动 Docker 容器,以便可以从主机机器访问它。最后,将在运行的容器中部署一个 Web 应用程序。

要创建 Tomcat 容器,我们将从中央仓库registry.hub.docker.com/_/tomcat/拉取一个镜像。该仓库支持 Tomcat 的不同版本,如 6、7 和 8。对于此应用程序,我们将使用 Tomcat 7.0.57 版本。此版本可以通过运行docker pull tomcat:7.0.57-jre7命令从注册表中下载。

下载镜像后,我们必须使用下载的镜像创建容器并启动它。使用带有选项-p <host_port>:<container_port>docker run命令创建并启动容器。此选项可以通过将主机端口路由到容器端口来访问正在运行的 Tomcat 容器。以下命令以userdetailsservice为名启动容器。此外,使用–rm选项在容器退出时删除文件系统。这对于清理过程是必需的:

$ docker run -it --rm -p 8181:8080 --name "userdetailsservice" tomcat:7.0.57-jre7
Using CATALINA_BASE:   /usr/local/tomcat
Using CATALINA_HOME:   /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME:        /usr
Using CLASSPATH:     /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
May 03, 2015 5:03:07 PM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server version:        Apache Tomcat/7.0.57
...

运行命令后,Tomcat 服务器可以通过主机机器上的http://localhost:8181访问:

在 Docker 中运行应用程序

图 9.1

Tomcat 服务器正在运行;下一个任务是部署 Web 应用程序到运行的容器中。部署 Web 应用程序可以通过多种方式完成。在这里,我们将讨论三种不同的部署 Web 应用程序的方法。

  • 将 Web 应用程序作为数据卷添加:我们已经学习了如何使用-v选项将数据卷挂载到容器中。这种方法甚至可以应用于部署 Web 应用程序。如果我们有主机机器上的 Web 应用程序的文件结构,它可以挂载到 Tomcat 的 webapps 目录中。

    以下命令显示了在 Tomcat 容器的/usr/local/tomcat/webapps/目录中部署名为userdetailsservice的应用程序的示例:

    $ docker run -it --rm -p 8181:8080 -v ~/userdetailsservice:/usr/local/tomcat/webapps/userdetailsservice --name "userdetailsservice" tomcat:7.0.57-jre7
    
    
  • 从主机复制 WAR 文件到容器:另一种方法是直接从主机机器复制应用程序 WAR 文件到容器。为了实现这一点,首先我们必须使用之前解释的 run 命令启动容器:

    $ docker run -it --rm -p 8181:8080 --name "userdetailsservice" tomcat:7.0.57-jre7
    
    

    当容器运行时,我们必须找到长的容器 ID。这可以通过使用带有--no-trunc选项的docker ps命令来完成:

    $ docker ps --no-trunc
    CONTAINER ID                                                       IMAGE                COMMAND             CREATED              STATUS               PORTS               NAMES
    1ad08559109a0f5eec535d05d55e76c5ad3646ae7bb6f4fffa92ad4721955349   tomcat:7.0.57-jre7   "catalina.sh run"   About a minute ago   Up About a minute   0.0.0.0:8181->8080/
    
    

    然后,我们可以使用简单的 UNIX cp命令将.war文件复制到 Docker 文件系统,如下所示:

    $ sudo cp ~/UserDetailsService/build/lib/userdetailsservice.war /var/lib/docker/aufs/mnt/1ad08559109a0f5eec535d05d55e76c5ad3646ae7bb6f4fffa92ad4721955349/usr/local/tomcat/webapps
    
    

    然而,这种方法并不推荐,因为从主机复制文件到容器不是一个好的选择。相反,我们应该使用数据挂载选项。

  • Tomcat 管理员:Tomcat 管理员工具可以从基于 Web 的用户界面部署 Web 应用程序。要从 Tomcat 管理员部署 Web 应用程序,您需要具有对 Tomcat 管理 GUI 的正确访问权限。我们为这个示例下载的 Tomcat 镜像不允许我们访问 Tomcat 管理员页面。因此,首先,我们必须通过修改tomcat-users.xml文件来为用户启用访问权限。我们可以简单地使用-v选项将现有的tomcat-users.xml文件绑定到容器,如下所示:

    $ docker run -it --rm -p 8181:8080 -v ~/Downloads/tomcat-users.xml:/usr/local/tomcat/conf/tomcat-users.xml --name "userdetailsservice" tomcat:7.0.57-jre7
    
    

    这种方法效果很好。但如果您想永久修改容器的tomcat-users.xml文件,可以采取不同的方法。首先,我们必须使用以下命令启动 Tomcat 容器:

    $ docker run -it --rm -p 8181:8080 --name "userdetailsservice" tomcat:7.0.57-jre7 command.
    
    

    然后,在另一个终端中,使用 Docker 的exec命令进入容器的 bash,如下所示:

    $ docker exec -it userdetailsservice /bin/bash
    
    

下一步是从文本编辑器修改/usr/local/tomcat/conf/tomcat-users.xml文件。为此,我们可能需要使用apt-get install vim命令安装 vim。您可以使用任何您选择的文本编辑器:

root@0ff13ab7f076:/usr/local/tomcat# apt-get update
root@0ff13ab7f076:/usr/local/tomcat# apt-get install vim

在成功安装 vim 之后,我们必须在tomcat-users.xml文件的末尾添加以下行(在</tomcat-users>之前),以启用管理员用户对 Tomcat-admin GUI 的访问:

<role rolename="manager-gui"/>
<user username="admin" password="admin" roles="manager-gui"/>

现在,更改已经应用到容器中,我们必须通过使用以下docker commit命令创建一个新的镜像来保存新的更改:

$ docker commit 0ff13ab7f076 usedetailsimage:v1
1d4cbdbe2b6ba97048431dbe2055f1df4d780cf5564200c5946e0944baf84b8f

新镜像已保存为带有v1标签的usedetailsimage。这可以通过列出所有docker images来验证:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED   VIRTUAL SIZE
usedetailsimage     v1                  1d4cbdbe2b6b        8 seconds ago       384.4 MB
hello-world         latest              91c95931e552        3 weeks ago         910 B
tomcat              7.0.57-jre7         b215f59f9987        3 months ago        345.9 MB

这个新创建的镜像可以用来启动 Tomcat 服务器,如下所示:

$ docker run -it --rm -p 8181:8080 --name "userdetailsservice" usedetailsimage:v1

在 Tomcat 成功启动后,我们将能够使用 admin/admin 凭据登录到 Tomcat 管理员页面 http://localhost:8181/manager/。可以通过选择部署 WAR 文件选项来部署 Web 应用程序。应用程序 userdetailsservice 启动需要几秒钟,它将在 Tomcat 管理员页面上显示,如下面的截图所示:

在 Docker 中运行应用程序

图 9.2

构建、部署和测试管道

在上一节中,我们学习了如何使用 Docker 创建一个类似于 Apache Tomcat 的容器,以及如何在运行中的容器中部署应用程序。一旦应用程序启动并运行,我们就可以运行一些自动化测试来验证其功能。这应该很简单!还能做些什么呢?嗯,在这本书的整个过程中,我们学习了如何使用 Gradle 自动化构建过程;在第七章“持续集成”中,我们讨论了持续集成工具,例如 Jenkins。现在,我们应该能够应用所有这些知识来创建一个简单的构建、部署和测试工作流程,以自动化从构建到部署的整个过程。不要与持续交付管道混淆。这只是一个简单的示例,用于使用 Gradle、Docker 和 Jenkins 等工具一起自动化构建、部署和测试。我们可以通过以下三个简单步骤设置管道:

  • 使用 Gradle 自动化创建或构建工件的过程。

  • 在运行中的容器中部署新创建的库。该容器是通过 Docker 创建并启动的。

  • 运行自动化测试以验证已部署应用程序的功能。

这些步骤可以通过 Jenkins 的帮助按顺序配置和执行。我们所需做的只是创建一个新的 Freestyle 项目,例如 build_deployment_pipeline。然后,添加源代码管理配置,如 Git(Git URL 为 github.com/mitramkm/mastering-gradle.git),如下面的截图所示。有关更多详细信息,请参阅第七章“持续集成”。在完成基本的 Jenkins 作业配置后,我们必须配置三个构建步骤来自动化构建、部署和测试执行:

构建、部署和测试管道

图 9.3

在源代码管理配置之后,我们必须在 Jenkins 中添加一个构建步骤来构建 Web 应用程序。在这个步骤中,我们将对名为UserDetailsService的 Gradle 项目执行clean war任务。这是一个简单的 Web 应用程序,用于公开 RESTful 服务。Gradle 任务将在项目的build/libs目录中创建一个 WAR 文件。在构建步骤配置中,我们指定了Root Build script${workspace}/Chapter9/UserDetailsService。因此,WAR 文件将在%JENKINS_HOME%/jobs/build_deployment_pipeline/workspace/Chapter9/UserDetailsService/build/libs/目录中创建:

构建、部署和测试流水线

图 9.4

我们已经完成了第一步。下一步是创建一个 Tomcat 容器并部署 WAR 文件。这可以通过运行一个自动化以下任务的 shell 脚本来完成:

  1. 从仓库中拉取 Tomcat 容器。

  2. 检查是否有任何现有的容器正在运行。如果有任何容器正在运行,请停止并删除该容器。

  3. 使用所需的配置(如端口、名称、内存和 CPU)启动容器。

  4. 最后,部署应用程序。

以下 shell 脚本自动化了之前提到的所有操作:

#!/bin/sh

if [ -z "$1" ]; then
 BUILD_HOME=$(pwd)/UserDetailsService
else
 BUILD_HOME=$1
fi

docker pull tomcat:7.0.57-jre7

runningContainer=`docker ps -l | grep userdetailsservice | awk '{print $1}'`

if [ ! -z "$runningContainer" ]
then
 docker stop $runningContainer
 docker rm $runningContainer
fi

docker run -d -v $BUILD_HOME/build/libs/userdetailsservice.war:/usr/local/tomcat/webapps/userdetailsservice.war -p 8181:8080 --name "userdetailsservice" tomcat:7.0.57-jre7

脚本已准备就绪。我们将配置并执行脚本,作为部署管道作业的第二步构建步骤。尽管我们使用 shell 脚本控制 docker 命令,但即使这样也可以使用 Gradle 任务(如 Exec)或 Gradle Docker 插件来完成。一些 Docker 插件可在plugins.gradle.org/找到。如果您想以 Gradle 的方式完成所有操作,也可以探索这些插件:

构建、部署和测试流水线

图 9.5

执行第二步构建步骤后,Web 应用程序在 Tomcat 容器中运行并启动。最后,我们必须通过运行自动化测试套件来验证应用程序的功能。示例 Web 应用程序是一个 RESTful 服务,它通过 HTTP GET 和 POST 方法公开getUsers()createUser()类型的功能。以下代码片段是TestNG测试用例的示例,可以作为健全性检查执行。它对http://localhost:8080/userdetailsservice/userdetails执行 HTTP GET 和 HTTP POST 调用:

@Test 
public void createUser() {
  User request = new User("User1", "User user", "user@abc.com");
  User response = resttemplate.postForObject(URL, request, User.class);
  Assert.assertEquals(response.getEmail(), "user@abc.com");
}

@Test(dependsOnMethods="createUser")
public void getUsers() {
  User[] response = resttemplate.getForObject(URL, User[].class);
  Assert.assertEquals(response.length, 1);
}

要执行测试用例,我们将在 Jenkins 管道中创建一个第三步构建步骤,任务为 gradle test。在这个例子中,为了简单起见,我们在src/test文件夹中创建了集成测试代码。理想情况下,在src/test目录中,我们应该只保留单元测试代码。如果您正在编写任何集成或回归测试,它应该在单独的 Java 项目中完成。另一个需要记住的是,测试任务主要用于执行单元测试代码。如果您正在编写一些集成测试代码,请考虑创建一个新的 Gradle 任务(如integrationTest),该任务运行 JUnit、TestNG 或任何其他测试套件:

构建、部署和测试流水线

图 9.6

现在,我们已经准备好在 Jenkins 中运行作业。作业依次执行三个任务——构建一个网络应用程序,在新创建的容器中部署应用程序,并最终执行一些集成测试。完整作业的控制台输出显示在下述屏幕截图中:

构建、部署和测试流水线

图 9.7

摘要

在本章中,我们讨论了应用程序部署以及如何借助 Docker 容器化应用程序。我们学习了如何使用 Gradle、Docker 和 Jenkins 自动化构建、部署和测试工作流程。

在下一章中,我们将介绍 Android 应用程序开发及其使用 Gradle 的构建过程。

第十章:使用 Gradle 构建 Android 应用

近年来,随着智能手机用户的不断增加,除了大数据和云计算之外,移动应用开发已经成为一个主要的关注领域。大多数公司都在为他们的产品开发移动应用,如游戏、社交网络、电子商务等。这种趋势无疑将在未来几年内继续增长。因此,在最后一章中,我们将涵盖与移动技术相关的主题。

在本章中,我们将讨论如何使用 Android Studio 作为 IDE 创建基本的 Android 应用以及如何使用 Gradle 构建应用。我们已经知道,Gradle 哲学基于约定而非配置,与市场上其他构建工具相比,使用 Gradle 编写构建自动化基础设施要容易得多。这就是为什么 Gradle 是 Android 官方构建工具的原因之一。您只需在构建文件中编写几行代码,应用就可以为不同的平台和版本做好准备,例如免费或付费。它还提供了在发布前对应用进行签名的支持。使用 Gradle,您可以在模拟器或物理设备上运行应用以执行单元和功能测试。

在本章中,我们将主要关注两个领域:使用 Android Studio 对 Android 应用开发的快速概述以及 Gradle 作为 Android 构建工具的各个方面。由于这是一本关于 Gradle 的书,我们的讨论将集中在理解 Gradle 特性上。

使用 Android Studio 创建 Android 项目

我们将从创建一个示例 Android 应用开始,当您在移动设备上打开它时,它将显示Hello World。您可以使用带有Android 开发工具ADT)插件的 Eclipse 或由 Google 发布的 Android Studio。Android Studio 基于 IntelliJ IDEA,现在是构建 Android 应用最首选的 IDE。带有 ADT 的 Eclipse 和 Android Studio 的设置说明可以在developer.android.com/sdk/index.html找到。

在本章中,我们将使用 Android Studio 进行应用开发。一旦您在系统上下载并安装了 Android Studio,请启动 Android Studio。Android Studio 还会安装 Android SDK,这是编译和执行 Android 应用所必需的。要创建一个应用,导航到文件 | 新建项目。您将看到以下屏幕:

使用 Android Studio 创建 Android 项目

图 10.1

点击下一步按钮并按照步骤操作。在活动屏幕上,选择空白活动

使用 Android Studio 创建 Android 项目

图 10.2

对于本章,我们的主要目的是创建一个示例应用程序,并强调使用 Gradle 的 Android 应用程序构建过程。因此,不需要创建一个完整的 Android 应用程序。因此,示例应用程序将只做一项工作,即在启动应用程序时显示 Hello World

要完成项目设置,在 自定义活动 界面中,提供如 活动名称标题等详细信息:

使用 Android Studio 创建 Android 项目

图 10.3

一旦点击 完成,Android Studio 将创建项目,目录结构如下所示:

使用 Android Studio 创建 Android 项目

图 10.4

在项目主目录中,你会找到 build.gradlesettings.gradle 文件。这意味着 Android Studio 创建了一个多项目构建结构。在 第六章 中,我们已经介绍了多项目结构,其中父项目包含一个或多个子项目。父项目包含所有子项目共享的公共配置和其他相关细节。

Android Studio 为父项目创建一个 build.gradle 文件,并为子项目创建单独的 build.gradle 文件。它还会创建一个包含所有属于此父项目的子项目的 settings.gradle 文件。你还会找到 local.properties 文件。此文件包含有关 Android SDK 位置的信息。此文件的内容如下:

sdk.dir=<Location of Android sdk>

Android Studio 还添加了 Gradle Wrapper,这意味着 Android 项目可以在未安装 Gradle 的机器上构建。Gradle Wrapper 自动安装 Gradle 并执行构建。

实际的 Android 应用程序位于 app 目录中,该目录包含源代码、资源等。app 目录的内容如下所示:

使用 Android Studio 创建 Android 项目

图 10.5

它包含用于 Java 源代码和测试代码的 src 目录。

源代码目录和测试目录分别是 src/main/javasrc/androidTest/java,如下截图所示:

使用 Android Studio 创建 Android 项目

图 10.6

你已经了解 Java 插件及其默认约定。如果我们在一个项目中包含 Java 插件,源结构将是 src/main/javasrc/main/resources。对于 Android 插件,除了这两个目录外,你还可以添加特定于 Android 约定的额外文件和文件夹,如以下内容所述:

  • AndroidManifest.xml

  • res/

  • assets/

  • jni/

  • proguard-rules.pro

这可以在 android 封闭中配置为 sourceSets 属性,如下所示:

android {
sourceSets {
  main {
    java {
      manifest.srcFile 'Manifest.xml' res.srcDirs = ['src/res'] assets.srcDirs = ['src/assets']
    }
  }
}
}

我们将在这里讨论一些重要概念。更多详细信息请参阅developer.android.com/sdk/index.html

AndroidManifest.xml 文件是必须存在于应用程序目录中的重要文件之一。它包含与应用程序相关的一些重要信息,例如活动、内容提供者、权限等。清单文件仅包含预定义元素。一些值是从 Gradle 属性中填充的。您不能在清单文件中添加任何自定义元素。例如 <manifest><application> 这样的元素是必需的,并且只出现一次。其他元素是可选的,可以应用一次或多次。

res 目录用于放置资源。您可以在 res 目录下放置所有应用程序资源,例如布局文件、可绘制文件和字符串值。有关资源的更多详细信息,请参阅developer.android.com/guide/topics/resources/providing-resources.html

res 目录内部支持的目录包括:

  • animator

  • anim

  • color

  • drawable

  • mipmap

  • layout

  • menu

  • raw

  • values

  • xml

assets 目录可能包含所有基本文件。此目录下的文件将作为未经修改的 .apk 文件的一部分,并且保留原始文件名。

jni 包含使用 Java Native Interface 的本地代码。

proguard-rules.pro 包含与 ProGuard 相关的设置。我们将在本章后面讨论 ProGuard 设置。

使用 Gradle 构建 Android 项目

我们使用一个简单的活动创建了应用程序,现在我们将尝试使用 Gradle 构建应用程序。Android Studio 已经为项目自动生成了两个构建文件;一个位于项目根目录的 build.gradle 文件,另一个位于 app 目录中的构建文件。我们将使用子项目(app 文件夹)的 build.gradle 文件来构建 Android 应用程序。此 build.gradle 文件包含以下内容:

apply plugin: 'com.android.application'

android {
  compileSdkVersion 22
  buildToolsVersion "22.0.1"

  defaultConfig {
    applicationId "ch10.androidsampleapp"
    minSdkVersion 15
    targetSdkVersion 22
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'com.android.support:appcompat-v7:22.1.1'
}

在第一行,我们使用 apply plugin: 'com.android.application' 语句应用了一个插件。这与应用任何其他标准 Gradle 插件类似。但这个插件 JAR 文件将从哪里下载?如果您检查父项目的 build.gradle 文件,您将找到以下条目:

buildscript {
   repositories {
       jcenter()
   }
   dependencies {
       classpath 'com.android.tools.build:gradle:1.2.3'
   }
}

buildscript 闭包中,我们定义了依赖项为 com.android.tools.build:gradle:1.2.3。此 JAR 文件将从 jcenter 仓库下载,并将其添加到 build.gradle 的类路径中。

构建文件的下一部分是 Android 闭包定义,其中我们定义了与应用程序相关的所有基本配置,例如 SDK 版本、支持的最低 SDK 版本、目标 SDK 版本、应用程序 ID 和版本控制。

接下来,我们有标准的 dependencies 闭包来定义应用程序的编译和运行时依赖项。在这里,我们已经包括了 lib 目录和 appcompat-v7 jar 作为依赖项。

通过这些简单的配置,我们就可以使用 Gradle 构建应用程序。我们已经在构建文件中应用了 Android 插件。现在,我们将探索可用于构建项目的不同任务。在命令提示符中输入 gradle tasks 以获取任务列表,如下所示:

> gradle tasks

Android tasks
-------------
androidDependencies - Displays the Android dependencies of the project.
signingReport - Displays the signing info for each variant.

Build tasks
-----------
assemble - Assembles all variants of all applications and secondary packages.
assembleAndroidTest - Assembles all the Test applications.
assembleDebug - Assembles all Debug builds.
……………...
compileDebugSources
compileDebugUnitTestSources
compileReleaseSources
compileReleaseUnitTestSources
mockableAndroidJar - Creates a version of android.jar that's suitable for unit tests.
…………………….

Install tasks
-------------
installDebug - Installs the Debug build.
installDebugAndroidTest - Installs the android (on device) tests for the Debug build.
uninstallAll - Uninstall all applications.
uninstallDebug - Uninstalls the Debug build.
uninstallDebugAndroidTest - Uninstalls the android (on device) tests for the Debug build.
uninstallRelease - Uninstalls the Release build.

……………

提示

注意,要构建 Android 项目,你需要 Gradle 2.2.1 及以上版本。

以下是一些你可能需要构建 Android 应用程序的重要任务:

  • assemble: 此任务与 Java 插件的组装任务相同,用于组装应用程序的输出。

  • check: 这类似于 Java 插件的检查任务,它运行所有检查。

  • clean: 此任务删除构建过程中创建的所有工件。

  • build: 此任务执行组装和检查任务,并构建应用程序工件。

  • androidDependencies: 此任务将显示项目的所有 Android 依赖项。

  • connectedCheck: 它将在所有连接的设备上并行执行检查任务

  • install<buildVariant>: 你可以找到各种安装任务(例如 installDebuginstallRelease),它们用于在设备上安装特定的 buildVariant。我们将在本书的后续部分中更详细地讨论 buildVariant

buildTypes

buildTypes 配置用于定义构建类型或环境,例如调试、发布、QA 和预发布,以构建和打包应用程序。默认情况下,当你构建 Android 项目时,你可以在 build/outputs/apk 目录中找到创建的调试和发布版本。默认情况下,调试版本使用自动创建的已知用户名/密码的密钥/证书进行签名。发布构建类型在构建过程中未签名;因此,你可以找到为发布构建类型创建的 app-release-unsigned.apk 文件。发布构建类型在部署到任何设备之前需要签名。

你可以自定义构建和发布构建类型,也可以通过添加自己的构建类型来扩展构建类型,如下所示:

buildTypes {
  release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
  staging.initWith(buildTypes.release)
  staging {
    debuggable true
  }
}

在这里,我们添加了一个额外的构建类型 staging(用于预发布环境),并将其配置为发布构建类型的副本,并添加了 debuggable true。你可以修改以下属性以适用于任何构建类型:

属性名称 调试类型的默认值 发布和其他类型的默认值
debuggable true false
jniDebuggable false false
renderscriptDebuggable false false
renderscriptOptimLevel 3 3
applicationIdSuffix null null
versionNameSuffix null null
signingConfig (稍后讨论) android.signingConfigs.debug null
zipAlignEnabled false true
minifyEnabled(稍后讨论) false false

表 10.1

此外,对于每个构建类型,您可以定义其特定的构建类型 SourceSet,例如 src/<build type>。如前例所述,您可以定义一个新的目录 src/staging,并将与预发布相关的源代码和资源放在此目录中。

此外,对于每个构建类型,Android 插件将在以下格式中添加新任务:assemble<buildtype>install<buildtype>compile<buildtype>jar<buildtype>。这可以通过执行 gradle task 命令来观察,如下所示:

> gradle  tasks | grep -i staging
assembleStaging - Assembles all Staging builds.
compileStagingSources
compileStagingUnitTestSources
installStaging - Installs the Staging build.
uninstallStaging - Uninstalls the Staging build.
lintStaging - Runs lint on the Staging build.
testStaging - Run unit tests for the staging build.
jarStagingClasses

如前所述,这些任务仅与预发布构建类型相关联。

ProGuard 设置

对于 release 构建类型,Gradle 提供了用于优化和混淆代码的 Proguard 工具的访问权限。它缩小了源代码,并使 .apk 文件的大小减小。您可以通过在 buildTypes/release 封闭中设置 minifyEnabled 来启用/禁用此功能。如 表 10.1 中所述,默认值设置为 false;因此,如果要启用它,请将其设置为 true

默认设置可以通过 getDefaultProguardFile('proguard-android.txt') 方法获取。您可以在 <Android sdk dir>/tools/proguard 找到 ProGuard 工具的位置。如果您想为项目提供自定义规则,可以将它添加到 Android Studio 提供的 proguard-rules.pro 文件中。您甚至可以添加自己的文件,并使用不同的名称:

 buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
}

构建风味

构建风味或产品风味与构建类型不同。这是另一个分离级别,允许构建应用程序的多个风味,例如付费版本、免费版本、手机版本和标签版本。每个应用程序版本都可以有自己的独立功能和不同的硬件要求。productFlavorsbuildTypes 的组合形成了一个构建变体,并为每个构建变体生成不同的 APK。产品风味在 productFlavors 封闭下定义:

productFlavors {
  phone{
    applicationId "ch10.androidsampleapp"
    minSdkVersion 14
    targetSdkVersion 20

    versionName "1.0-phone"
  }
  tab {
    applicationId "ch10.androidsampleapp"
    minSdkVersion 15
    targetSdkVersion 22
    versionName "1.0-tab"
  }
}

现在,如果我们使用 gradle clean build 命令构建项目,我们将在 build/outputs/apk/ 目录中找到不同的 APK 文件。我们有两个风味(phonetab)和四个构建类型(debug signeddebug unalignedstagingrelease)。因此,在构建过程中将创建总共 24 = 8* 个 APK 文件。

构建风味

图 10.7

当我们在上一节中将 staging 添加为构建类型时,我们观察到 Gradle 自动创建了一些任务。同样,对于每个风味配置,Gradle 将添加不同的任务,如 assemblePhoneDebugassembleTabDebug

> gradle tasks | grep -i phone
assemblePhone - Assembles all Phone builds.
assemblePhoneDebug - Assembles the DebugPhone build.
assemblePhoneDebugAndroidTest - Assembles the android (on device) tests for the PhoneDebug build.
assemblePhoneRelease - Assembles the ReleasePhone build.
assemblePhoneStaging - Assembles the StagingPhone build.
compilePhoneDebugAndroidTestSources
compilePhoneDebugSources
compilePhoneDebugUnitTestSources
compilePhoneReleaseSources
compilePhoneReleaseUnitTestSources
compilePhoneStagingSources
compilePhoneStagingUnitTestSources
installPhoneDebug - Installs the DebugPhone build.
installPhoneDebugAndroidTest - Installs the android (on device) tests for the PhoneDebug build.
uninstallPhoneDebug - Uninstalls the DebugPhone build.
uninstallPhoneDebugAndroidTest - Uninstalls the android (on device) tests for the PhoneDebug build.
uninstallPhoneRelease - Uninstalls the ReleasePhone build.
uninstallPhoneStaging - Uninstalls the StagingPhone build.
connectedAndroidTestPhoneDebug - Installs and runs the tests for DebugPhone build on connected devices.
lintPhoneDebug - Runs lint on the PhoneDebug build.
lintPhoneRelease - Runs lint on the PhoneRelease build.
lintPhoneStaging - Runs lint on the PhoneStaging build.
testPhoneDebug - Run unit tests for the phoneDebug build.
testPhoneRelease - Run unit tests for the phoneRelease build.
testPhoneStaging - Run unit tests for the phoneStaging build.
jarPhoneDebugClasses
jarPhoneReleaseClasses
jarPhoneStagingClasses

产品风味扩展了 defaultConfig 封闭中的配置。您可以在每个产品风味中覆盖默认配置。对于每个风味,您还可以拥有独立的源代码和所需的文件,如 src/<flavor>/javasrc/<flavor>/resources 等。

在设备/模拟器上运行应用程序

一旦构建了应用程序,您可能想要在模拟器或物理移动设备上安装或运行应用程序。为了简单起见,我们将在模拟器上运行应用程序。在开发阶段,借助模拟器,您可以在不使用设备的情况下测试不同平台上的应用程序。使用模拟器的优点如下:

  • 您可以在多个模拟器设备上测试应用程序

  • 您可以使用不同的硬件功能进行测试,例如声音、网络摄像头或传感器

  • 您可以控制电池电量、手机位置、网络设置,如 2G 或 3G 等

模拟器非常灵活,但使用过多的模拟器可能会降低系统性能。根据您的系统配置,您应仔细配置模拟器。您可以使用 AVD 管理器添加新的模拟器设备,如图所示:

在设备/模拟器上运行应用程序

图 10.8

这将显示现有的模拟器设备。您可以根据应用程序需求创建新的设备。有关更多信息,请参阅此链接 developer.android.com/tools/help/emulator.html

在设备/模拟器上运行应用程序

图 10.9

您可以通过点击 动作 列表中的启动符号来启动模拟器。在我们的例子中,我们创建了一个 Nexus 5 API 22x86 模拟器来测试应用程序。或者,您也可以通过在命令提示符中执行以下命令来启动模拟器设备:

>%ANDROID_SDK%\tools\emulator.exe -netdelay none -netspeed full -avd Nexus_5_API_22_x86

初始化模拟器需要一些时间。一旦模拟器启动并运行,我们应该能够从 Android Studio 运行应用程序。转到 运行 菜单并选择 运行应用程序

在设备/模拟器上运行应用程序

图 10.10

这将显示所有连接到系统的设备以及正在运行的模拟器。您可以选择任何正在运行的设备并点击 确定。几秒钟后,应用程序应该会在模拟器中可见。

在设备/模拟器上运行应用程序

图 10.11

或者,您也可以使用 gradle install<buildVariant> 命令来安装应用程序。我们已经在上一节中创建了不同的构建变体和口味。让我们尝试在模拟器上安装 PhoneDebug 变体。操作如下:

> gradle installPhoneDebug
:app:preBuild UP-TO-DATE 
:app:prePhoneDebugBuild UP-TO-DATE 
:............
.............
:app:mergePhoneDebugAssets UP-TO-DATE 
:app:compilePhoneDebugJava UP-TO-DATE 
:app:compilePhoneDebugNdk UP-TO-DATE 
:app:compilePhoneDebugSources UP-TO-DATE 
:app:preDexPhoneDebug UP-TO-DATE 
:app:dexPhoneDebug UP-TO-DATE 
:app:validateDebugSigning 
:app:packagePhoneDebug UP-TO-DATE 
:app:zipalignPhoneDebug UP-TO-DATE 
:app:assemblePhoneDebug UP-TO-DATE 
:app:installPhoneDebug 
Installing APK 'app-phone-debug.apk' on 'Nexus_5_API_22_x86(AVD) - 5.1'
Installed on 1 device. 

BUILD SUCCESSFUL

Total time: 24.543 secs

您可以在手机的程序列表中找到该应用程序。AndroidSampleApp 是我们使用 Gradle 任务安装的应用程序。您可以启动应用程序并检查输出。它将显示 Hello World

要使用 Gradle 卸载应用程序,请使用以下 gradle uninstall 命令:

> gradle uninstallPhoneDebug

签署发布版本

你可能在运行gradle tasks时观察到与发布相关的安装任务尚未创建;例如,installPhoneReleaseinstallTabRelease等。如果你使用密钥库签名应用程序,将会有与发布构建类型相关的任务可用。现在,我们将尝试使用密钥库签名一个应用程序。如果你已经有一个有效的密钥库,你可以使用该文件来签名应用程序;否则,你需要使用以下命令生成一个新的密钥库:

> keytool -genkey -v -keystore myCustomkey.keystore -alias customKey -keyalg RSA -keysize 2048 -validity 10000

要创建密钥库,我们需要提供一些基本信息。在输入所有详细信息后,前面的命令将生成myCustomkey.keystore文件。现在,我们必须使用以下配置更新build.gradle以启用应用程序的签名:

android {

......
signingConfigs {
  release {
    storeFile file("myCustomkey.keystore")
    storePassword "welcome"
    keyAlias "customKey"
    keyPassword "welcome"
  }
}
  ……………
buildTypes {
  release {
    minifyEnabled false
    signingConfig signingConfigs.release
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
  ………………
}

现在,如果我们执行gradle tasks命令,我们会发现为发布构建添加了新的任务。同样,新的 APK 文件将在apk文件夹中创建:

> gradle tasks | grep -i install
Install tasks
installPhoneDebug - Installs the DebugPhone build.
installPhoneDebugAndroidTest - Installs the android (on device) tests for the PhoneDebug build.
installPhoneRelease - Installs the ReleasePhone build.
installPhoneStaging - Installs the StagingPhone build.
installTabDebug - Installs the DebugTab build.
installTabDebugAndroidTest - Installs the android (on device) tests for the TabDebug build.
installTabRelease - Installs the ReleaseTab build.
installTabStaging - Installs the StagingTab build.
uninstallAll - Uninstall all applications.
....

摘要

在本章中,我们简要讨论了使用 Gradle 作为构建工具的 Android 开发。我们还讨论了 Android 插件提供的不同闭包以及如何遵循默认约定构建 Android 项目。我们还解释了如何自定义构建文件以满足新项目的要求。当然,还有很多事情可以讨论,例如 Android 开发和使用 Gradle 的 Android,但我们无法在一个章节中涵盖所有内容。这需要一本书来详细说明 Android 插件的全部功能。但我们认为,我们已经涵盖了构建 Android 项目所需的大部分基本和重要步骤,这将帮助你开始使用 Gradle 作为 Android 构建系统。

posted @ 2025-10-26 08:55  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报