读书笔记《gradle-effective-implementations-guide-second-edition》使用Gradle构建脚本
在构建脚本中,我们必须处理文件和目录是很常见的。例如,当我们需要将文件从一个目录复制到另一个目录或创建一个目录来存储任务或程序的输出时。
要定位相对于当前项目的文件或目录,我们可以使用 file()
方法。这个方法实际上是连接到我们的构建脚本的Project
对象的方法。在上一章中,我们讨论了如何使用对 project
变量的显式引用或简单地调用 Project
的方法和属性代码> 对象隐式。
file()
方法将解析文件或目录相对于当前项目而不是当前工作目录的位置。这非常有用,因为我们可以从不同于实际构建脚本位置的目录运行构建脚本。 file()
方法返回的文件或目录引用随后会相对于项目目录进行解析。
我们可以将任何对象作为 file()
方法的参数传递。通常,我们将传递一个 String
或 java.io.File
对象。
在下一个示例中,我们将演示如何使用 file()
方法获取对 File
对象的引用:
我们可以通过多种方式使用 file()
方法。我们可以传递一个 url
或 uri
实例作为参数。 Gradle 现在仅支持基于文件的 URL。我们也可以使用闭包来定义文件或目录。最后,我们还可以传递一个 java.util.concurrent.Callable
接口的实例,其中 call()的返回值
方法是对文件或目录的有效引用:
使用 file()
方法,我们创建了一个新的 File
对象;此对象可以引用文件或目录。我们可以使用 File的
对象来查看我们是在处理文件还是目录。如果我们想检查文件或目录是否真的存在,我们使用 isFile()
或 isDirectory()
方法exists()
方法。由于我们的 Gradle 构建脚本是用 Groovy 编写的,因此我们还可以使用 Groovy 为 File
类添加的额外属性和方法。例如,我们可以使用 text
属性来读取文件的内容。但是,我们只能在使用 file()
方法创建 File
对象后对其进行测试。如果我们想停止构建以防目录不存在或者我们正在处理一个文件并且我们希望处理一个目录怎么办?在 Gradle 中,我们可以将一个额外的参数传递给 org.gradle.api.PathValidation
file() 方法> 类型。 Gradle 然后验证创建的 File
对象是否对 PathValidation
实例有效;如果不是,则构建停止,我们会收到一条很好的错误消息,告诉我们出了什么问题。
假设,我们想在我们的构建脚本中使用一个名为 config
的目录。该目录必须存在,否则构建将停止:
现在我们可以运行构建并从输出中看到该目录不存在:
我们还可以使用 PathValidation
参数来测试 File
对象是否真的是文件而不是目录。最后,我们可以检查 File
对象是否引用了现有文件或目录。如果文件或目录不存在,则会引发异常并停止构建:
我们还可以使用一组文件或目录,而不仅仅是单个文件或目录。在 Gradle 中,一组文件由 ConfigurableFileCollection
接口表示。好消息是 Gradle API 中的很多类都实现了这个接口。
我们可以使用 files()
方法在我们的构建脚本中定义一个文件集合。此方法在我们可以在构建脚本中访问的Project
对象中定义。 files()
方法接受许多不同类型的参数,这使得它使用起来非常灵活。例如,我们可以使用 String
和 File
对象来定义一个文件集合。
与 file()
方法一样,路径被解析,相对于项目目录:
然而,这些并不是我们可以使用的唯一论据。我们可以传递 URI
或 URL
对象,就像我们可以使用 文件()
方法:
我们还可以使用数组、Collection
或 Iterable
对象与文件名或其他 ConfigurableFileCollection
实例作为参数:
我们还可以使用 Callable
接口的闭包或实例来定义文件列表,如下所示:
最后,我们可以将 Task
对象作为参数传递给 files()
方法。任务的 outputs
属性用于确定文件集合或者我们可以直接使用 TaskOutputs
对象而不是让Gradle 通过Task
对象的 outputs
属性解决它。让我们看看我们在上一章创建的 convert
任务。这个任务有一个 outputs
属性和一个文件,但这也可以是多个文件或一个目录。要在我们的构建脚本中获取文件集合对象,我们只需将 Task
实例作为参数传递给 files()
方法:
同样重要的是要注意文件收集是惰性的。这意味着当我们定义集合时,集合中的路径不会被解析。只有在构建脚本中实际查询和使用文件时,才会解析集合中的路径。
ConfigurableFileCollection
接口具有操作集合的有用方法,例如,我们可以使用 +
和 -
运算符分别从集合中添加或删除元素:
要获取 ConfigurableFileCollection
中元素的绝对路径名,我们可以使用 asPath
属性。路径名由操作系统的路径分隔符分隔。在 Microsoft Windows 操作系统中,分号 (;
) 用作路径分隔符;在 Linux 或 Mac OS X 操作系统中,使用冒号 (:
)。这意味着我们可以在任何操作系统上简单地使用 asPath
属性,Gradle 将自动使用正确的路径分隔符:
当我们在 Mac OS X 上运行构建脚本时,我们得到以下输出:
要获取构成文件集合的 File
对象,我们可以使用 files
属性。我们还可以使用 as
关键字将集合转换为 File
对象列表;如果我们知道我们的集合仅由单个文件或目录组成,那么我们可以使用 singleFile
属性来获取 File< /code> 对象,如下:
最后,我们可以使用 filter()
方法对我们的文件集合应用过滤器。我们传递了一个闭包,它定义了要在过滤集合中的元素。过滤后的集合是实时集合。这意味着如果我们向原始集合添加新元素,过滤器闭包将再次应用于我们过滤的集合。在下面的示例中,我们有 filterFiles
任务,我们在其中定义两个文件的文件集合,其名称为 INSTALL.txt
和
README
。接下来,我们定义一个带有过滤器的新文件集合,该过滤器包含所有具有 .txt
文件扩展名的文件。这个集合是一个实时的、过滤的集合,当我们向原始集合添加一个新文件时,过滤的集合也会更新:
在 Gradle 中,我们还可以处理组织为树的文件集合,例如磁盘上的目录树或 ZIP 文件中的分层内容。分层文件集合由 ConfigurableFileTree
接口表示。这个接口扩展了我们之前看到的 ConfigurableFileCollection
接口。
要创建新的文件树,我们在项目中使用 fileTree()
方法。我们可以使用几种方法来定义文件树。
我们可以使用 include
方法和 includes
属性来定义匹配模式以在文件中包含一个文件(或多个文件)树。使用 exclude
方法和 excludes
属性,我们可以使用相同的语法从文件中排除一个或多个文件树。匹配模式风格被描述为 Ant 风格的匹配模式,因为 Ant 构建工具使用这种风格来定义语法来匹配文件树中的文件名。可以使用以下模式:
*
匹配任意数量的字符?
匹配任何单个字符**
匹配任意数量的目录或文件
以下示例演示了如何创建文件树:
要过滤文件树,我们可以像处理文件集合一样使用 filter()
方法,但我们也可以使用 matching( )
方法。我们将闭包传递给 matching()
方法或 org.gradle.api.tasks.util.PatternFilterable
接口。我们可以使用
include
, includes
, exclude
,和 excludes
方法从文件树中包含或排除文件,如下所示:
我们可以使用 visit()
方法来访问每个树节点。我们可以检查节点是目录还是文件。然后按广度顺序访问树,如以下代码所示:
要在 Gradle 中复制文件,我们可以使用 Copy
任务。我们必须分配一组要复制的源文件和这些文件的目的地。这是用复制规范定义的。复制规范由 org.gradle.api.file.CopySpec
接口定义。该接口有一个 from() 方法,我们可以使用它来设置我们要复制的文件或目录。使用我们在目标目录或文件中指定的 into()
方法。
以下示例显示了一个名为 simpleCopy
的简单 Copy
任务,具有单个源 src/ xml
目录和目标 definitions
目录:
from()
方法接受与 files()
方法相同的参数。当参数是一个目录时,该目录中的所有文件——€”但不是目录本身——都被复制到目标目录。如果参数是文件,则仅复制该文件。
into()
方法接受与 file()
方法相同的参数。要包含或排除文件,我们使用 CopySpec
接口。我们可以像使用 fileTree()
方法一样应用 Ant 样式的匹配模式。
以下示例定义了一个名为 copyTask
的任务,并使用了 include()
和 exclude()
方法来选择要复制的文件集:
另一种复制文件的方法是使用 Project.copy()
方法。 copy()
方法接受一个 CopySpec
接口实现,就像 Copy
任务。我们的simpleCopy
任务也可以写成如下:
要创建归档文件,我们可以使用 Zip
, Tar
, Jar
、 War
和 Ear
任务。为了定义存档的源文件和存档文件中的目标,我们使用 CopySpec
接口,就像复制文件一样。我们可以使用 rename()
, filter()
, expand( )
、 include()
和 exclude()
方法一样,这样你就不用不必学习任何新东西,你可以使用你已经学过的东西。
要设置存档的文件名,我们使用以下任何属性:baseName
, appendix
, 版本
, 分类器
,或 扩展
。 Gradle 将使用以下模式创建文件名: [baseName]-[appendix]-[version]-[classifier]。 [扩展]
。如果未设置属性,则它不会包含在生成的文件名中。要覆盖默认文件名模式,我们可以设置
archiveName
属性并分配我们自己的完整文件名,用于生成的存档文件。
在以下示例中,我们将使用 archiveZip
任务创建一个 ZIP 存档。我们将包含 dist
目录中的所有文件,并将它们放在存档的根目录中。文件的名称由遵循 Gradle 模式的各个属性设置:
当我们运行 archiveDist
任务时,一个新的dist-files-archive-1.0-sample.zip
文件会在我们项目的根。要更改存档文件的目标目录,我们必须设置 destinationDir
属性。在以下示例中,我们将目标目录设置为 build/zips
。我们还将使用 into()
方法将文件放在归档文件中的 files
目录中。文件名现在由 archiveName
属性设置:
要使用可选的 gzip
或 bzip2
压缩创建 TAR 存档,我们必须使用 tarFiles
任务。语法与 Zip
类型的任务相同,但我们有一个额外的 compression
属性可以使用设置我们想要使用的压缩类型 (gzip, bzip2)
。如果我们不指定 compression
属性,则不使用压缩来创建归档文件。
在下面的示例中,我们创建了一个 Tar
类型的 tarFiles
任务。我们将 compression
属性设置为 gzip
。运行这个任务后,我们得到一个新的 dist/tarballs/dist-files.tar.gz
文件:
Jar
、War
和 Ear
任务类型遵循与 Zip
和 Tar
任务类型的模式相同。每种类型都有一些额外的属性和方法来包含特定于该类型存档的文件。当我们了解如何在 Java 项目中使用 Gradle 时,我们将看到这些任务的示例。
在 Gradle 构建文件中,我们可以访问 Gradle 定义的多个属性,但我们也可以创建自己的属性。我们可以直接在构建脚本中设置自定义属性的值,也可以通过命令行传递值来实现。
我们可以在 Gradle 构建中访问的默认属性如下表所示:
名称 |
类型 |
默认值 |
|
|
项目实例。 |
|
|
项目目录的名称。该名称是只读的。 |
|
|
项目的绝对路径。 |
|
|
项目的描述。 |
|
|
包含构建脚本的目录。该值是只读的。 |
|
|
目录中具有 |
|
|
项目结构根目录下的项目目录。 |
|
|
未指定。 |
|
|
未指定。 |
|
|
|
以下构建文件具有显示属性值的任务:
当我们运行构建时,我们得到以下输出:
要添加我们自己的属性,我们必须在构建文件的 ext{}
脚本块中定义它们。在属性名称前加上 ext.
是另一种设置值的方法。要读取属性的值,我们不必使用 ext.
前缀,我们可以简单地引用属性的名称。该属性也会自动添加到内部 project
属性中。
在下面的脚本中,我们添加了一个 customProperty
属性和一个 String
值 custom。在 showProperties
任务中,我们显示属性的值:
运行脚本后,我们得到以下输出:
我们还可以在外部文件中设置项目的属性。该文件需要命名为 gradle.properties
,并且它应该是一个纯文本文件,属性名称及其值位于不同的行中。我们可以将文件放在项目目录或 Gradle 用户主目录中。默认的 Gradle 用户主目录是 $USER_HOME/.gradle
。 Gradle 用户主目录中的 properties
文件中定义的属性会覆盖项目目录中属性文件中定义的属性值。
我们现在将在我们的项目目录中创建一个 gradle.properties
文件,其内容如下。
我们使用我们的构建文件来显示属性值:
如果我们运行构建文件,我们不需要传递任何命令行选项,Gradle 将使用 gradle.properties
来获取属性值:
我们可以使用 -P
命令行选项为构建添加额外的属性,而不是直接在构建脚本或外部文件中定义属性。我们还可以使用 -P
命令行选项来设置现有属性的值。如果我们使用 -P
命令行选项定义一个属性,我们可以覆盖一个在外部 gradle中定义的同名属性.properties
文件。
以下构建脚本有一个 showProperties
任务,它显示现有属性和新属性的值:
让我们运行我们的脚本并传递现有 version
属性和不存在的 customProperty
的值:
我们还可以使用 Java 系统属性为我们的 Gradle 构建定义属性。我们使用 -D
命令行选项,就像在普通 Java 应用程序中一样。系统属性的名称必须以 org.gradle.project
开头,后跟我们要设置的属性的名称,然后是值。
我们可以使用之前创建的相同构建脚本:
但是,这次我们使用不同的命令行选项来获得结果:
使用命令行选项提供了很大的灵活性;但是,有时由于环境限制或我们不想在每次调用 Gradle 构建时重新键入完整的命令行选项,我们无法使用命令行选项。 Gradle 还可以使用在操作系统中设置的环境变量将属性传递给 Gradle 构建。
环境变量名称以 ORG_GRADLE_PROJECT_
开头,后跟属性名称。我们使用我们的构建文件来显示属性:
首先,我们设置 ORG_GRADLE_PROJECT_version
和 ORG_GRADLE_PROJECT_customProperty
环境变量,然后我们运行我们的 showProperties< /code> 任务,如下:
在 第 1 章 中, 从 Gradle 开始 ,我们讨论了几个命令行选项,我们可以在运行 Gradle 构建时使用它们来显示更多或更少的日志消息。这些消息来自 Gradle 内部任务和类。我们在 Gradle 构建脚本中使用了一个println
方法来查看一些输出,但我们也可以使用 Gradle 的日志机制来拥有一种更可定制的方式来定义日志消息。
Gradle 支持多个日志级别,我们可以将这些级别用于我们自己的消息。我们的消息级别很重要,因为我们可以使用命令行选项来过滤日志级别的消息。
下表显示了 Gradle 支持的日志级别:
级别 |
用于 |
|
调试消息 |
|
信息消息 |
|
进度信息消息 |
|
警告信息 |
|
导入信息消息 |
|
错误信息 |
每个 Gradle 构建文件和任务都有一个 logger
对象。 logger
对象是 Java 的简单日志门面的 Gradle 特定扩展的实例span> (SLF4J) 记录器界面。 SLF4J 是一个 Java 日志库。这个库提供了一个独立于底层日志框架的日志 API。可以在部署时或运行时使用特定的日志框架来输出实际的日志消息。
要在我们的 Gradle 构建文件中使用 logger
对象,我们只需要引用 logger
并调用日志级别的方法我们想要使用,或者我们可以使用常见的 log()
方法,并将日志级别作为参数传递给该方法。
让我们创建一个简单的任务并使用不同的日志级别:
当我们从命令行运行这个 logLevels
任务时,我们得到以下输出:
我们注意到只有 LIFECYCLE
, WARN
, QUIET
, 如果我们不添加任何额外的命令行选项,则会显示ERROR
日志级别。要查看 INFO
消息,我们必须使用 --info
命令行选项。然后我们得到以下输出:
请注意,我们还从 Gradle 本身获得了更多消息。早些时候,我们只看到了来自脚本的日志消息,但这次显示了很多关于构建过程本身的额外日志。
要获得更多输出和我们的 DEBUG
级别的日志消息,我们必须使用 --debug
命令行选项来调用 logLevels
任务,如下:
这一次,我们收到了很多信息,我们真的必须仔细寻找我们自己的信息。日志的输出格式也发生了变化,请注意,虽然之前只显示了日志消息,但现在还显示了日志消息的时间、日志级别和原始类。
因此,我们知道每个 Gradle 项目和任务都有一个我们可以使用的记录器。但是,我们也可以使用Logging
类显式创建一个logger
实例。例如,如果我们定义自己的类并希望在 Gradle 构建中使用它,我们可以使用 Logging
类来获取 Gradle logger
对象。我们可以在这个 上使用额外的
实例,就像在项目和任务中一样。lifecycle()
和 quiet()
方法logger
我们现在将在我们的构建文件中添加一个类定义,并使用这个类的一个实例来查看输出:
我们使用了项目和任务的logger
;在类 Simple
中,我们使用 Logging.getLogger()
来创建一个Gradle 记录器
实例。当我们运行我们的脚本时,我们得到以下输出:
要查看记录器的原始类,我们可以使用 --debug
(或 -d
)命令行选项.然后,我们不仅会看到记录消息的时间,还会看到记录器的名称:
请注意,我们的 logger
项目名为 org.gradle.api.Project
,即 logger
任务被命名为 org.gradle.api.Task
,而我们的 logger
在 Simple
类被命名为 Simple
。
在前面的示例中,我们使用 logger
实例和 println()
方法来记录消息。 Gradle 将发送的输出重定向到 System.out
—€”这是我们在使用时所做的 println()
—到具有 quiet
日志级别的记录器。这就是为什么我们在运行 Gradle 构建时会看到 println()
输出的原因。 Gradle 拦截输出并使用其日志记录支持。
当我们使用 --debug
选项运行以下简单的 Gradle 构建时,我们可以看到 Gradle 已将输出重定向到 QUIET
日志级别:
如果我们运行构建,让我们看看输出:
Gradle 使用 ERROR
日志级别将标准错误重定向到日志消息。这也适用于我们在 Gradle 构建中从外部库中使用的类。如果这些库中的代码使用标准输出和错误,Gradle 将捕获输出和错误消息并将它们重定向到 logger
实例。
如果我们想更改用于重定向输出和错误消息的日志级别,我们可以自己配置。每个项目和任务都有一个名为 logging 的 org.gradle.api.logging.LoggingManager
类的实例。 LoggingManager
类有方法 captureStandardOutput()
和 captureStandardError()
我们可以使用来设置输出和错误消息的日志级别。 请记住,Gradle 默认使用 QUIET
日志级别来输出消息和 ERROR
错误消息的日志级别。在以下脚本中,我们将输出消息的日志级别更改为 INFO
:
首先,我们在没有任何额外命令行选项的情况下运行构建:
请注意,我们在任务的 doFirst
方法中定义的 println
语句已显示,但其他 的输出;println
语句未显示。我们将这些 println
语句的输出重定向到具有 INFO
日志级别的 Gradle 日志记录。 INFO
日志级别现在默认显示。
让我们再次运行脚本,但现在我们添加 --info
命令行选项,以便我们可以看到我们的 println 的所有输出
语句:
通常,如果我们想运行 Gradle 构建,我们必须在我们的计算机上安装 Gradle。此外,如果我们将项目分发给其他人并且他们想要构建项目,他们必须在他们的计算机上安装 Gradle。 Gradle Wrapper 可用于允许其他人构建我们的项目,即使他们的计算机上没有安装 Gradle。
包装器是 Microsoft Windows 操作系统上的批处理脚本或其他操作系统上的 shell 脚本,它将下载 Gradle 并使用下载的 Gradle 运行构建。
通过使用包装器,我们可以确保使用项目的正确 Gradle 版本。我们可以定义 Gradle 版本,如果我们通过包装脚本文件运行构建,则使用我们定义的 Gradle 版本。
要创建 Gradle Wrapper 批处理和 shell 脚本,我们可以调用内置的 wrapper
任务。如果我们在计算机上安装了 Gradle,则此任务已经可用。让我们从命令行调用 wrapper
任务:
任务执行后,我们有两个脚本文件——€”gradlew.bat
和gradlew
——€”在我们项目目录的根目录。这些脚本包含运行 Gradle 所需的所有逻辑。如果 Gradle 尚未下载,Gradle 发行版将被下载并安装在本地。
在 gradle/wrapper
目录中,相对于我们的项目目录,我们找到了 gradle-wrapper.jar
和 gradle-wrapper.properties
文件。 gradle-wrapper.jar
文件包含下载和调用 Gradle 所需的几个类文件。 gradle-wrapper.properties
文件包含下载 Gradle 的设置,例如 URL。 gradle-wrapper.properties
文件还包含 Gradle 版本号。如果发布了新的 Gradle 版本,我们只需要在 gradle-wrapper.properties
文件中更改版本,Gradle Wrapper 就会下载新版本以便我们使用它来构建我们的项目。
所有生成的文件现在都是我们项目的一部分。如果我们使用版本控制系统,那么我们必须将这些文件添加到版本控制中。检查我们项目的其他人可以使用 gradlew
脚本来执行项目中的任务。下载指定的 Gradle 版本并用于运行构建文件。
如果我们想使用另一个 Gradle 版本,我们可以使用 --gradle-version
选项调用 wrapper
任务。我们必须指定为其生成 Wrapper 文件的 Gradle 版本。默认情况下,用于调用 wrapper
任务的 Gradle 版本是包装文件使用的 Gradle 版本。
要为 Gradle 安装文件指定不同的下载位置,我们必须使用 wrapper
的 --gradle-distribution-url
选项代码>任务。例如,我们可以在本地 Intranet 上安装自定义的 Gradle,使用此选项,我们可以生成将使用 Intranet 上的 Gradle 分发的 Wrapper 文件。
在以下示例中,我们为 Gradle 2.12 显式生成包装文件:
如果我们想自定义内置 wrapper
任务的属性,我们必须使用 org. 添加一个新任务到我们的 Gradle 构建文件中。 gradle.api.tasks.wrapper.Wrapper
类型。我们不会更改默认的 wrapper
任务,而是使用我们想要应用的新设置创建一个新任务。我们需要使用我们的新任务来生成 Gradle Wrapper shell 脚本和支持文件。
我们可以更改使用 Wrapper 任务的 scriptFile
属性生成的脚本文件的名称。要更改生成的 JAR 和属性文件的名称和位置,我们可以更改 jarFile
属性:
如果我们运行 createWrapper
任务,我们会得到一个 Windows 批处理文件和 shell 脚本,并且带有属性文件的 Wrapper 引导 JAR 文件存储在 gradle-bin
目录:
要更改必须下载 Gradle 版本的 URL,我们可以更改 distributionUrl
属性。例如,我们可以在我们公司的 Intranet 上发布一个固定的 Gradle 版本,并使用 distributionUrl
属性来引用我们 Intranet 上的下载 URL。这样我们可以确保公司的所有开发人员都使用相同的 Gradle 版本:
在本章中,我们讨论了 Gradle 在处理文件时提供的支持。我们看到了如何创建文件或目录以及文件和目录的集合。文件树表示一组分层的文件。
我们可以将日志消息添加到我们的项目和任务中,并在运行 Gradle 构建时查看输出。我们讨论了如何使用不同的日志级别来影响输出中显示的信息量。我们还使用 LoggingManager
来捕获标准输出和错误消息,并将它们重定向到自定义日志级别。
我们讨论了如何使用 Gradle Wrapper 来允许用户构建我们的项目,即使他们没有安装 Gradle。我们讨论了如何自定义 Wrapper 以下载特定版本的 Gradle 并使用它来运行我们的构建。
在下一章中,我们将创建一个 Java 项目并使用 Java 插件添加一组默认任务,我们可以使用这些任务来编译、测试和打包我们的 Java 代码。