读书笔记《gradle-essentials》揭开构建脚本的神秘面纱
在前三章中,我们看到了 Gradle 可以通过在构建文件中添加几行来添加到我们的构建中的许多有趣的功能。然而,这只是冰山一角。我们探索的主要是 Gradle 附带的插件添加的任务。根据我们的经验,我们知道项目构建从未如此简单。无论我们如何努力避免它们,它们都会进行自定义。这就是为什么添加自定义逻辑的能力对于构建工具来说极其重要。
此外,Gradle 的美妙之处就在于此。每当我们决定扩展现有功能或完全偏离约定并想要做一些非传统的事情时,它都不会出现在我们面前。如果我们希望在构建中添加一些逻辑,我们不需要编写 XML 汤或一堆 Java 代码。我们可以创建自己的任务或扩展现有任务来做更多事情。
这种灵活性伴随着学习 Groovy DSL 形式的非常温和的学习曲线。在本章中,我们将了解 Gradle 构建脚本的语法和 Gradle 的一些关键概念。我们将涵盖以下主题:
帮助我们理解 Gradle 构建脚本语法的 Groovy 入门
我们构建中可用的两个重要对象,即
project
对象和task
对象构建阶段和生命周期回调
任务的一些细节(任务执行和任务依赖)
要精通 Gradle 并编写有效的构建脚本,我们需要了解 Groovy 的一些基础知识,Groovy 本身就是一种出色的动态语言。如果我们有任何使用动态语言(如 Ruby 或 Python)的经验,除了 Java,我们会对 Groovy 感到宾至如归。如果不是这样,仍然知道大多数 Java 语法也是有效的 Groovy 语法应该会让我们对 Groovy 感到高兴,因为我们可以从第一天开始编写 Groovy 代码并保持高效,而无需学习任何东西。
对于没有准备的眼睛,Gradle 脚本一开始可能看起来有点难以理解。 Gradle 构建脚本不仅使用 Groovy 语法,还使用丰富且富有表现力的 DSL,该 DSL 提供高级抽象来表示常见的构建相关逻辑。让我们快速了解一下是什么让 Groovy 成为编写构建文件的绝佳选择。
Note
使用 Groovy 编写构建逻辑并不新鲜。 Gant 和 GMaven 已经使用 Groovy 编写构建逻辑,以利用 Groovy 的语法简洁性和表现力。 GMavenPlus 是 GMaven 的继承者。它们所构建的工具,即 Ant 和 Maven,分别限制了 Gant 和 GMaven。
Gradle 不是捎带现有工具来添加句法增强功能,而是利用从过去工具中学习的知识来设计的。
Gradle 的 核心大部分是用 Java 编写的(请参阅下面的信息)。 Java 是一门很棒的语言,但它并不是最适合编写脚本的语言。想象一下用 Java 编写脚本,由于 Java 的冗长和仪式,我们可能会编写另一个项目来定义我们的主项目的构建。 XML 在上一代构建工具(Ant 和 Maven)中被大量使用,对于声明性部分来说还可以,但对于编写逻辑来说不是很好。
Groovy 是 Java 的动态化身。如前所述,大多数 Java 语法也是有效的 Groovy 语法。如果我们了解 Java,我们已经可以编写 Groovy 代码。如果今天有大量可以编写 Java 的人,这是一个很大的优势。
Groovy 的语法简洁、富有表现力且功能强大。 Groovy 是动态风格的完美结合,同时仍然能够使用类型。它是少数支持可选类型的语言之一,也就是说,如果我们愿意,可以灵活地提供类型信息,而当我们不想提供类型信息时,可以将类型信息放在一边。由于一流的 lambda 支持和元编程功能,Groovy 是一种用于构建内部 DSL 的优秀语言。所有上述因素使其成为编写构建脚本的最合适的候选者之一。
虽然 我们可以在 Groovy 中编写 Java 风格的代码,但如果我们花一些时间来学习语言的动态特性和 Groovy 提供的一些语法增强功能,我们将能够编写更好的 Gradle 构建脚本和插件。如果我们还不了解 Groovy,这将会很有趣。
让我们充分了解 Groovy,以便我们能够正确理解 Gradle 脚本。我们将快速浏览一下 Groovy 的一些语言特性。
强烈建议尝试执行以下小节中的代码。此外,我们自己编写和尝试更多代码来探索 Groovy 将有助于我们加强对语言基础的理解。本指南并非详尽无遗,仅用于设置 Groovy 滚动。
最简单且推荐的方法是在本地安装最新的 Groovy SDK。可以使用以下任何选项执行 Groovy 代码片段:
将片段保存到
.groovy
脚本并使用以下代码从命令行运行:我们可以使用 Groovy 安装附带的 Groovy 控制台 GUI 来编辑和运行脚本
我们还可以使用 Groovy shell,它是一个交互式 shell,用于执行或评估 Groovy 语句和表达式
如果我们不想在本地安装 Groovy,那么:
我们可以使用 Groovy 控制台在浏览器中在线运行 Groovy 代码,地址为 http://groovyconsole.appspot.com
我们还可以通过创建任务并将代码片段放入其中来在构建脚本中运行 Groovy 代码(我们也可以将它们放在任何任务之外,它仍然会在配置阶段运行它)
在 Groovy 脚本中,def
关键字可以定义一个变量(取决于上下文):
但是,a
的类型是在运行时根据它指向的对象类型决定的。粗略地说,声明为 def
的引用可以引用任何 Object
或其子类。
声明一个更具体的类型同样有效,只要我们想要类型安全,就应该使用它:
我们也可以使用 Java 原始数据类型,但请记住,它们实际上并不是 Groovy 中的原始数据类型。它们仍然是一等对象,实际上是对应数据类型的 Java 包装类。我们用一个例子来确认一下,如下:
它打印以下输出:
这表明 c
是一个对象,我们可以在其上调用方法,并且 c
的类型是 “整数”
。
我们建议尽可能使用特定类型,因为这会增加可读性并帮助 Groovy 编译器通过捕获无效分配来及早检测错误。它还可以帮助 IDE 完成代码。
与 Java 不同,单引号是 (''
) 字符串文字,而不是 字符
:
当然,也可以使用常规的 Java 字符串字面量(""
),但它们在 Groovy 中称为 GStrings。它们具有字符串插值或变量或表达式的内联扩展的附加功能:
这将打印以下输出:
${var}
和 $var
都是有效的,但是包装 (${}< /code>) 更适合复杂或更长的表达式。例如:
它将打印以下内容:
我们所有人都会记得在每一行的末尾添加+ "\\n"
,以便在 Java 中生成多行字符串。那些日子已经一去不复返了,因为 Groovy 支持多行字符串文字。多行文字以三个单引号或双引号开始(与 GString 功能相同的字符串)并以三个单引号或双引号结束:
它将打印以下内容:
第 1 行的正斜杠是可选的,用于排除第一个新行。如果我们不输入正斜杠,我们将在输出的开头增加一个新行。
此外,请查看 stripMargin
和 stripIndent
方法,用于对前导空格进行特殊处理。
如果我们的文字包含很多转义字符(例如,正则表达式),那么我们最好使用“斜线”字符串文字,它以单个正斜杠 (/
):
它将打印以下内容:
在上面的示例中,如果我们必须使用常规字符串,那么我们必须在字符类 d
之前转义反斜杠。它看起来如下:
闭包 在 Groovy 是一个代码块,可以像任何其他变量一样分配给引用或传递。这个概念在许多其他语言中被称为 lambda ,包括 Java 8或函数指针。
如果我们没有接触过上述任何内容,那么需要一些详细的阅读才能很好地理解这个概念,因为它为未来的许多其他高级主题奠定了基础。闭包本身就是一个巨大的话题,深入的讨论超出了本书的范围。
闭包几乎就像一个常规的方法或函数,但它也可以分配给一个变量。此外,由于它可以分配给变量,因此它也必须是一个对象;因此,它将有自己的方法:
在这里,代码块被分配给一个名为 cl1
的变量。现在代码块可以在以后使用 call 方法执行,或者可以传递 cl1
变量并在以后执行:
难怪它会打印以下内容:
由于闭包就像方法一样,它们也可以接受参数:
它打印以下内容:
就像 方法一样,它们也可以返回值。如果没有明确声明 return
语句,则自动返回闭包的最后一个表达式。
当我们有接受闭包的方法时,闭包开始发光。例如,times
方法可用于整数,它接受一个闭包并执行它的次数与整数本身的值一样多;每次调用时,它都会传递当前值,就像我们循环到 0
中的值一样:
它打印以下内容:
我们还可以内联块并将其直接传递给方法:
它打印以下内容:
有一个名为 it
的特殊变量,如果闭包没有定义它的参数,它在块范围内可用。在前面的示例中,我们使用 it
访问传递给块的数字并将其与自身相乘以获得其平方。
闭包在回调处理等情况下非常有用,而在 Java 7 及更低版本中,我们必须使用匿名接口实现来实现相同的结果。
Groovy 支持常用的数据结构的文字声明,这使得代码在不牺牲可读性的情况下更简洁。
Groovy 支持经过彻底测试的 Java Collection API,并在底层使用相同的类,但有一些额外的方法和语法糖:
它打印以下内容:
让我们创建另一个包含一些初始内容的列表:
由于运算符重载,我们可以直观地使用列表中的许多运算符。例如,使用 anotherList[1]
将得到 b
。
下面是一些方便的运算符的更多示例。这将添加两个列表并将结果分配给列表变量:
这会将 60
附加到列表中:
以下两个示例只是从另一个列表中减去一个列表:
遍历列表同样简单直观:
它将打印以下内容
传递给 each
的闭包对列表的每个元素执行,该元素作为参数 到关闭。因此,前面的代码遍历列表并打印每个元素的值。注意 it
的用法,它是列表当前元素的句柄。
following 更像是一个类似 Java 的方法,当然是有效的 Groovy 方法:
上述方法可以简洁地改写如下:
我们没有指定返回类型,而是声明了 def
,这实际上意味着该方法可以返回任何 Object
或子类引用。然后,我们省略了形式参数的类型,因为声明 def
对于方法的形式参数是可选的。在第 2 行,我们省略了 return
语句,因为最后一个表达式的求值是由方法自动返回的。我们还省略了分号,因为它是可选的。
这两个示例都是有效的 Groovy 方法声明。但是,建议读者明智地选择类型,因为它们提供类型安全并充当方法的活文档。如果我们不声明参数的类型,如前面的方法,sum(1,"2") 也将成为有效的方法调用,更糟糕的是,它返回一个意外的结果,没有任何异常。
Groovy 不支持命名参数,例如 Python,但 Map 提供了非常接近相同功能的近似值:
在前面的代码中,我们希望地图包含键 a
和 b
。
我们可以省略方括号([]
),因为地图在方法调用中有特殊的支持:
现在,它显然看起来像命名参数。参数的顺序并不重要,不需要传递所有参数。此外,括号包装是可选的,就像任何方法调用一样:
Groovy 中的类 的声明方式与 Java 类一样,但有很多 较少的仪式.默认情况下,类是公共的。它们可以使用 extends
从其他类继承或使用 implmenets
实现接口。
下面是一个非常简单的类的定义,Person
,有两个属性,name
和年龄
:
我们可以使用更具体的类型,而不是使用 def
作为属性。
除了 到默认构造函数之外,Groovy 中的类还有一个特殊的构造函数,它获取类属性的映射。以下是我们如何使用它:
在前面的代码中,我们使用特殊的构造函数创建了 person
对象。参数是键值对,其中键是类中属性的名称。为键提供的值将为相应的属性设置。
Groovy 对属性有语言级别的支持。在前面的类中,name
和 age
与 Java 不同,它们不仅是字段,而且是类的属性getter 和 setter 就位。默认情况下,字段是私有的,它们的公共访问器和修改器(getter 和 setter)是自动生成的。
我们可以调用 getAge()
/setAge()
和 getName()
/setName()
我们在上面创建的 person
对象上的方法。但是,还有一种更简洁的方法可以做到这一点。我们可以像访问公共字段一样访问 属性,但在幕后,Groovy 通过 getter 和 setter 对其进行路由。我们试试看:
它打印以下内容:
在前面的代码中,在第 1 行,person.age
实际上是对 person.getAge()
的调用,因此,它返回人的年龄。然后,我们使用 person.age
在右侧使用赋值运算符和值来更新年龄。我们没有更新该字段,但它在内部通过 setter setAge()
传递。这只是可能的,因为 groovy 为属性提供了语法支持。
我们可以为所需的字段提供我们自己的 getter 和/或 setter,它们将优先于生成的字段,但只有在我们有一些逻辑可以写入这些字段时才需要。例如,如果我们想设置一个正的年龄值,那么我们可以提供我们自己的 setAge()
实现,每当属性更新时都会使用它:
对属性的支持导致类定义中的样板代码显着减少并增强了可读性。
现在我们已经了解了基本的 Groovy,让我们在 Gradle 构建脚本的上下文中使用它。在前面的章节中,我们已经看到了应用插件的语法。它看起来如下:
如果我们仔细看,apply
是一个方法调用。我们可以将参数包装在括号中:
接受映射的方法可以像命名参数一样传递键值。但是,为了更清楚地表示 Map,我们可以将参数包装在 []
中:
最后,apply
方法被隐式应用到 project
对象上(我们将在本章接下来的部分中看到这一点) .所以,我们也可以在 project
对象的引用上调用它:
因此,从 前面的示例中,我们可以看到将插件应用于项目的语句仅仅是对 上的方法调用的语法糖code class="literal">project 对象。我们只是使用 Gradle API 编写 Groovy 代码。此外,一旦我们意识到这一点,我们对理解构建脚本语法的看法就会改变。
如果我们以面向对象的方式考虑构建系统,我们会立即想到以下类:
代表正在构建的系统的
project
task
封装了需要执行的构建逻辑片段
好吧,我们很幸运。正如我们所料,Gradle 创建 project
和 task
类型的对象。这些对象可以在我们的构建脚本中访问,供我们自定义。当然,底层实现并不简单,API 也非常复杂。
project
对象是 API 的核心部分,通过构建脚本公开和配置。 project
对象在脚本中可用,以便在 project
对象上智能调用没有对象引用的方法。我们刚刚在上一节中看到了一个这样的例子。只需阅读项目 API 即可理解大部分构建脚本语法。
task
对象是为在构建文件中直接声明的每个任务以及插件创建的。我们已经在 Chapter 1 运行你的第一个 Gradle 任务中创建了一个非常简单的任务 并使用了来自 Chapter 2, 构建 Java 项目,第 3 章,构建 Web 应用程序。
我们很快就会看到这些对象是如何以及何时创建的。
Gradle 构建在每次调用时遵循一个非常简单的生命周期。 构建经过三个阶段:初始化、配置和执行。当调用 gradle
命令时,并不是所有写在我们构建文件中的代码都是从上到下顺序执行的。仅执行与当前构建阶段相关的代码块。此外,构建阶段的顺序决定了代码块何时执行。一个例子是任务配置与任务执行。了解这些阶段对于正确配置我们的构建非常重要。
Gradle首先判断当前项目是否有子项目,或者它是否是构建中的唯一项目。对于多项目构建,Gradle 会确定哪些项目(或子模块,许多人更喜欢称之为)必须包含在构建中。我们将在下一章看到多项目构建。 Gradle 然后为根项目和项目的每个子项目创建一个 Project
实例。对于我们目前看到的单模块项目,在这个阶段没有太多需要配置的。
在这个 阶段,参与项目的构建脚本将根据在初始化阶段创建的相应项目对象进行评估。在多模块项目的情况下,评估以广度方式进行,也就是说,所有兄弟项目将在子项目之前进行评估和配置。但是,此行为是可配置的。
请注意,执行脚本并不意味着任务也被执行。为了快速验证这一点,我们可以在 build.gradle
文件中添加一个 println
语句,并创建一个打印一个信息:
如果我们执行以下代码:
我们将看到以下输出:
事实上,也可以选择任何内置任务,例如 help
:
在执行任何任务之前,我们仍然会看到我们的 构建脚本被评估
消息。这是为什么?
当一个脚本 被求值时,脚本中的所有语句都会按顺序执行。这就是执行根级别的 println
语句的原因。如果你注意到,一个任务动作实际上是一个闭包。因此,它仅在语句执行期间附加到任务。但是,闭包本身尚未执行。动作闭包中的语句仅在任务执行时执行,这仅在下一阶段发生。
仅在此阶段配置任务。无论要调用什么任务,都会配置所有任务。 Gradle 准备了一个 有向无环图 (DAG) 表示任务以确定任务依赖性和执行顺序。
如前所述,Gradle 为每个 构建创建一个
。该对象可在我们的构建脚本中使用 project
对象。在初始化阶段为我们准备 gradleproject
参考。作为 API 的核心部分,该对象有许多可用的方法和属性。
我们一直在使用 project API,甚至没有意识到我们正在调用 project
对象上的方法。根据一些管理规则,如果没有提供显式引用,则构建脚本中的所有顶级方法调用都会在项目对象上调用。
让我们重写 第 1 章 运行你的第一个 Gradle 任务< /em> 将项目引用用于方法调用:
正如我们在本章前面所看到的,apply
是 项目
。所谓
dependencies
块其实就是project
dependencies()
的方法code> 接受闭包。 repositories
部分也是如此。我们可以在闭包块周围添加括号,使其看起来像一个普通的旧方法调用:
这个对象还有很多有趣的方法,我们将在接下来的章节和章节中再次看到,无论是否明确引用 project
对象。
project
对象有几个可用的属性。有些属性是只读属性,如name
、path
、parent
等等,而其他的都是可读和可写的。
例如,我们可以设置 project.description
来提供我们项目的描述。我们可以使用 project.version
属性来设置项目的版本。此版本将由其他任务使用,例如 Jar
以在生成的工件中包含版本号。
Note
我们无法从 build.gradle
文件中更改 project.name
,但我们可以使用 settings.gradle
在同一个项目中设置项目名称。当我们了解多项目构建时,我们将更详细地看到这个文件。
除了通过名称直接访问属性外,我们还可以在 project
对象上使用以下方法访问属性。
要检查属性是否存在,请使用以下方法:
要获取给定属性名称的属性值,请使用以下方法:
要为给定的属性名称设置属性的值,请使用以下方法:
例如,让我们创建一个 build.gradle
文件,其内容如下:
执行以下任务:
如前所述,在Groovy 中,我们可以使用property = value
语法来调用setter。我们在 project
对象上设置 description
和 version
属性。然后,我们使用 project
引用和 description
使用 project
对象上的 "literal">property() 方法。
我们上面看到的属性必须存在于项目中,否则构建失败并显示 Could not find property ...
消息。
Gradle 使得在项目中存储用户定义的属性变得非常容易,同时仍然能够享受项目属性语法的精妙之处。我们所要做的就是使用 ext
命名空间来为自定义属性赋值。然后,可以像常规项目属性一样在项目上访问此属性。这是一个例子:
执行以下任务:
在前面的示例中,我们声明了一个名为 abc
的自定义属性,并为其分配了值 123
。我们没有使用 project
引用,因为它在脚本根级别隐式可用。在任务操作中,我们首先使用项目引用直接打印它,就像它是 Project
上的属性一样。然后,我们使用 property()
方法和 project.ext
引用进行访问。请注意,在任务的动作闭包中,我们应该使用 project
引用以避免任何歧义。
在子项目(模块)中可以访问额外的属性。也可以在其他对象上设置额外的属性。
尽管我们已经查看了一些方法和属性,但在这里涵盖所有这些是不切实际的;因此,值得花一些时间阅读 project
接口的 API 和 DSL 文档。
正如我们目前所见,task
是一个执行某些构建逻辑的命名操作。它是构建工作的一个单元。例如,clean
、compile
、dist
等等,都是如果我们必须为我们的项目编写任务,我们很容易想到典型的构建任务。任务或多或少类似于 Ant 的目标。
创建任务的最简单方法如下:
在我们进一步讨论任务之前,让我们花点时间思考一下任务创建。
我们使用了 taskName
任务形式的语句。
如果我们把它改写成task (taskName)
,它马上就会看起来像方法调用。
正如我们现在可能已经猜到的那样,前面的方法可用于项目对象。
因此,我们也可以编写以下内容之一:
project.task "myTask"
project.task("myTask")
请注意,在后面的示例中,我们必须将任务名称作为字符串传递。 task taskName
是一种特殊形式,我们可以将 taskName
用作文字而不是字符串。这是由 Groovy AST 转换魔法完成的。
该项目有几种创建任务对象的任务方法:
但是,在本质中,我们可能会在创建任务和配置闭包以配置任务时传递一些键值作为命名参数。
我们实际上是在创建一个 Task
类型的对象(确切的类名现在并不重要)。我们可以查询这个对象的属性和调用方法。 Gradle 很好地使这个 task
对象可供使用。在漂亮的 DSL 背后,我们实际上是在编写一个脚本,以一种面向对象的方式创建构建逻辑。
Task
对象,,例如上面创建的对象,并没有多大作用。事实上,它没有附加任何动作。我们需要将操作附加到 Task
对象,以便 Gradle 在任务运行时执行这些操作。
Task
对象有一个名为 doLast
的方法,它接受一个闭包。 Gradle 确保传递给此方法的所有闭包都按照它们传递的顺序执行:
我们现在可以做的是再次调用 doLast
:
此外,在另一种语法中:
有多种方法可以将 doLast
逻辑添加到任务中,但最惯用的,也许是一种简洁的方法如下:
就像 Project
对象,我们有 Task
可以访问其方法和属性的对象。但是,与 Project
对象不同,它在脚本的顶层并不隐式可用,而仅在任务的配置范围内可用。此外,直观地,我们可以说每个 build.gradle
会有多个 Task
对象。稍后我们将看到访问 Task
对象的各种方法。
项目中的任务 可能相互依赖。在本节中,我们将看到项目任务中可能存在的不同类型的关系。
有 任务的执行取决于其他任务的成功完成。例如,为了创建一个可分发的 JAR 文件,代码应该首先被编译并且“类”文件应该已经存在。在这种情况下,我们不希望用户从命令行显式指定所有任务及其顺序,如下所示:
这是容易出错的。我们可能会忘记包含一项任务,或者如果有多个任务依赖于先前任务的成功完成,则排序可能会变得复杂。希望能够指定是否:
我们也可以声明,如果一个任务被调用,它应该紧跟着另一个任务,即使另一个任务没有被显式调用。这与 dependsOn
不同,后者在调用任务之前执行另一个任务。在finalizedBy
的情况下,在被调用的任务执行完之后再执行另一个任务:
有 时间,如果这种关系与 dependsOn< /代码>。例如,如果我们执行以下命令:
然后,不相关的 任务将按照它们在命令行中指定的顺序执行,在这种情况下这是没有意义的。
在这种情况下,我们可以添加以下代码行:
这告诉 Gradle,如果两个任务都在任务图中,那么 build
必须在 clean
运行之后运行.在这里,build 不依赖于 clean。
shouldRunAfter
和 mustRunAfter
的区别在于前者对 Gradle 的提示性更强,但并不强制 Gradle 遵循顺序每时每刻。在以下两种情况下,Gradle 可能不支持 shouldRunAfter
:
在它引入循环排序的情况下。
在并行执行的情况下,只有
shouldRunAfter
任务还没有成功完成且其他依赖都满足时,shouldRunAfter
会被忽略。
Gradle 的一大优点是我们也可以动态创建任务。这意味着在编写构建时并不完全知道任务的名称和逻辑,但是根据一些可变参数,任务将自动添加到我们的 Gradle 项目中。
让我们试着用一个例子来理解:
在前面的人为示例中,我们正在创建并动态添加十个任务到我们的构建中。尽管它们都只是打印任务编号,但动态创建和添加任务到我们的项目的能力非常强大。
到目前为止,我们 一直在使用任务名称调用gradle
命令行界面。这在本质上是一种重复,尤其是在开发过程中,像 Gradle 这样的工具可以让我们覆盖:
设置默认任务是明智的,这样如果我们不指定任何任务名称,则默认执行设置的任务。
在前面的例子中,不带任何参数从命令行运行 gradle
会按照 defaultTasks
。
到目前为止,我们看到的 任务本质上是临时性的。我们必须为任务执行时需要执行的任务操作编写代码。但是,无论我们在构建哪个项目,如果我们有能力对现有逻辑进行一些配置更改,那么任务操作的逻辑不需要更改。例如,当您复制文件时,只有源、目标和包含/排除模式发生变化,但如何将文件从一个位置复制到另一个位置以遵守包含/排除模式的实际逻辑保持不变。所以,如果一个项目中需要两个类似复制的任务,比如说 copyDocumentation
和 deployWar
,我们真的想要写一个完整的逻辑来复制选定的文件两次?
这 适用于非常小的构建(例如我们章节中的示例),但该方法不能很好地扩展。如果我们继续编写任务操作来执行这些普通操作,那么我们的构建脚本将很快膨胀到无法管理的状态。
自定义任务类型是 Gradle 的解决方案,用于将可重用的构建逻辑抽象到自定义任务类中,从而在任务对象上公开输入/输出配置变量。这有助于我们调整类型化的任务以满足我们的特定需求。这有助于我们保持通用构建逻辑的可重用和可测试性。
临时任务操作的另一个问题是它本质上是必不可少的。为了工具的灵活性,Gradle 允许我们在构建脚本中强制编写自定义逻辑。但是,在我们的构建脚本中过度使用命令式代码会使构建脚本无法维护。 Gradle 应该尽可能以声明的方式使用。命令式逻辑应封装在自定义任务类中,同时公开任务配置供用户配置。在 Gradle 的术语中,自定义任务类被称为 增强的任务。
自定义任务类型充当模板,为通用构建逻辑提供一些合理的默认值。我们仍然需要在构建中声明一个任务,但我们只是告诉 Gradle 这个任务的类型并配置这个任务类型的设置,而不是重新编写整个任务动作块。 Gradle 已经附带了许多自定义任务类型;例如,复制
、执行
、删除
、Jar
, Sync
, Test
, JavaCompile
, Zip
等等。我们也可以轻松编写自己的增强任务。我们将非常简要地了解这两种情况。
在前面的示例中,第一个重要区别是我们传递了一个键 type
,其值作为自定义任务的类名,即 Copy
在这种情况下。另外,请注意没有 没有 doLast
或间接 (< <
) 运算符。我们传递给这个任务的闭包实际上是在构建的配置阶段执行的。闭包内的方法调用被委托给正在配置的隐式可用的 task
对象。我们这里没有写任何逻辑,只是为一个类型为Copy
的任务提供了配置。在我们继续编写临时任务操作之前,总是值得看看可用的自定义任务。
如果我们 现在回过头来看,我们为示例任务的任务操作编写的代码主要是 println< /code> 语句将在
System.out
上打印给定的消息。现在,想象一下我们发现 System.out
不符合我们的要求,我们应该使用文本文件来打印来自任务的消息。我们需要完成所有任务并更改实现以写入文件而不是 println
。
有更好的方法来处理这种不断变化的需求。我们可以通过提供我们自己的任务类型来利用这里任务类型的功能。让我们将以下代码放入我们的 build.gradle
中:
在前面的代码示例中:
我们首先创建了一个扩展
DefaultTask
的类(这将是我们的任务类型),该类已经在 Gradle 中定义。接下来,我们在名为
message
的属性上使用@Input
为我们的任务声明了一个可配置的输入。我们任务的消费者可以配置这个属性。然后,我们在
print
方法上使用了@TaskAction
注解。这个方法在我们的任务被调用时执行。它只是使用println
来打印message
。然后,我们声明了三个任务;都使用不同的方式来配置我们的任务。注意没有任何任务动作。
最后,我们应用任务流控制技术来声明任务依赖关系。
如果我们现在运行 thanks
任务,我们可以看到预期的输出,如下所示:
这里需要注意的几点如下:
如果我们想改变我们的打印逻辑的实现,只有一个地方我们需要做改变,我们自定义任务类的
print
方法。使用使用任务类型的任务,它们的工作方式与任何其他任务一样。他们还可以使用
doLast {}
,<< {}
,但通常不是必需的。
有大量可用于 Groovy 的在线参考资料。我们可以从:
如需进一步阅读,请参阅 Groovy 的在线文档,位于 http://www.groovy- lang.org/documentation.html
更多 Groovy 资源参考可在 https://github.com/kdabir/awesome-时髦的
Groovy in Action 一书可在 https://www.manning.com/books/groovy-in-action-second-edition。
Groovy Cookbook 可在 https://www.packtpub.com/application-development/groovy-2-cookbook。
Programming Groovy 2 一书可在 https://pragprog.com/book/vslg2/programming-groovy-2。
我们从对 Groovy 语言的快速特性概述开始本章,涵盖了一些有助于我们理解 Gradle 语法和编写更好的构建脚本的主题。然后,我们查看了 Gradle 向我们的构建脚本公开的 API,以及如何通过 DSL 使用 API。我们还介绍了 Gradle 构建阶段。然后,我们研究了如何创建、配置任务、在它们之间建立依赖关系以及默认运行任务的方式。
阅读本章后,我们应该能够理解 Gradle DSL,而不仅仅是试图记住语法。我们现在可以阅读和理解任何给定的 Gradle 构建文件,并且我们现在应该能够轻松编写自定义任务。
这一章可能感觉有点冗长和复杂。我们应该花一些时间练习并重新阅读不清楚的部分,并查找整章提供的在线参考资料。未来的章节将一帆风顺。