vlambda博客
学习文章列表

读书笔记《gradle-essentials》揭开构建脚本的神秘面纱

Chapter 4. Demystifying Build Scripts

在前三章中,我们看到了 Gradle 可以通过在构建文件中添加几行来添加到我们的构建中的许多有趣的功能。然而,这只是冰山一角。我们探索的主要是 Gradle 附带的插件添加的任务。根据我们的经验,我们知道项目构建从未如此简单。无论我们如何努力避免它们,它们都会进行自定义。这就是为什么添加自定义逻辑的能力对于构建工具来说极其重要。

此外,Gradle 的美妙之处就在于此。每当我们决定扩展现有功能或完全偏离约定并想要做一些非传统的事情时,它都不会出现在我们面前。如果我们希望在构建中添加一些逻辑,我们不需要编写 XML 汤或一堆 Java 代码。我们可以创建自己的任务或扩展现有任务来做更多事情。

这种灵活性伴随着学习 Groovy DSL 形式的非常温和的学习曲线。在本章中,我们将了解 Gradle 构建脚本的语法和 Gradle 的一些关键概念。我们将涵盖以下主题:

  • 帮助我们理解 Gradle 构建脚本语法的 Groovy 入门

  • 我们构建中可用的两个重要对象,即 project 对象和 task 对象

  • 构建阶段和生命周期回调

  • 任务的一些细节(任务执行和任务依赖)

Groovy for Gradle build scripts


精通 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 不是捎带现有工具来添加句法增强功能,而是利用从过去工具中学习的知识来设计的。

Why Groovy?

Gradle 的 核心大部分是用 Java 编写的(请参阅下面的信息)。 Java 是一门很棒的语言,但它并不是最适合编写脚本的语言。想象一下用 Java 编写脚本,由于 Java 的冗长和仪式,我们可能会编写另一个项目来定义我们的主项目的构建。 XML 在上一代构建工具(Ant 和 Maven)中被大量使用,对于声明性部分来说还可以,但对于编写逻辑来说不是很好。

Note

我们可以在 下载 Gradle 的源代码target="_blank">https://github.com/gradle/gradle。

Groovy 是 Java 的动态化身。如前所述,大多数 Java 语法也是有效的 Groovy 语法。如果我们了解 Java,我们已经可以编写 Groovy 代码。如果今天有大量可以编写 Java 的人,这是一个很大的优势。

Groovy 的语法简洁、富有表现力且功能强大。 Groovy 是动态风格的完美结合,同时仍然能够使用类型。它是少数支持可选类型的语言之一,也就是说,如果我们愿意,可以灵活地提供类型信息,而当我们不想提供类型信息时,可以将类型信息放在一边。由于一流的 lambda 支持和元编程功能,Groovy 是一种用于构建内部 DSL 的优秀语言。所有上述因素使其成为编写构建脚本的最合适的候选者之一。

Groovy primer

虽然 我们可以在 Groovy 中编写 Java 风格的代码,但如果我们花一些时间来学习语言的动态特性和 Groovy 提供的一些语法增强功能,我们将能够编写更好的 Gradle 构建脚本和插件。如果我们还不了解 Groovy,这将会很有趣。

让我们充分了解 Groovy,以便我们能够正确理解 Gradle 脚本。我们将快速浏览一下 Groovy 的一些语言特性。

强烈建议尝试执行以下小节中的代码。此外,我们自己编写和尝试更多代码来探索 Groovy 将有助于我们加强对语言基础的理解。本指南并非详尽无遗,仅用于设置 Groovy 滚动。

Running Groovy code

最简单且推荐的方法是在本地安装最新的 Groovy SDK。可以使用以下任何选项执行 Groovy 代码片段:

  • 将片段保存到 .groovy 脚本并使用以下代码从命令行运行:

    groovy scriptname.groovy
  • 我们可以使用 Groovy 安装附带的 Groovy 控制台 GUI 来编辑和运行脚本

  • 我们还可以使用 Groovy shell,它是一个交互式 shell,用于执行或评估 Groovy 语句和表达式

如果我们不想在本地安装 Groovy,那么:

  • 我们可以使用 Groovy 控制台在浏览器中在线运行 Groovy 代码,地址为 http://groovyconsole.appspot.com

  • 我们还可以通过创建任务并将代码片段放入其中来在构建脚本中运行 Groovy 代码(我们也可以将它们放在任何任务之外,它仍然会在配置阶段运行它)

Variables

Groovy 脚本中,def 关键字可以定义一个变量(取决于上下文):

def a = 10

但是,a 的类型是在运行时根据它指向的对象类型决定的。粗略地说,声明为 def 的引用可以引用任何 Object 或其子类。

声明一个更具体的类型同样有效,只要我们想要类型安全,就应该使用它:

Integer b = 10

我们也可以使用 Java 原始数据类型,但请记住,它们实际上并不是 Groovy 中的原始数据类型。它们仍然是一等对象,实际上是对应数据类型的 Java 包装类。我们用一个例子来确认一下,如下:

int c = 10
println c.getClass()

它打印以下输出:

class java.lang.Integer

这表明 c 是一个对象,我们可以在其上调用方法,并且 c 的类型是 “整数”

我们建议尽可能使用特定类型,因为这会增加可读性并帮助 Groovy 编译器通过捕获无效分配来及早检测错误。它还可以帮助 IDE 完成代码。

Strings

Java 不同,单引号是 ('') 字符串文字,而不是 字符

String s = 'hello'

当然,也可以使用常规的 Java 字符串字面量(""),但它们在 Groovy 中称为 GStrings。它们具有字符串插值或变量或表达式的内联扩展的附加功能:

def name = "Gradle"
println "$name is an awesome build tool"

这将打印以下输出:

Gradle is an awesome build tool

${var}$var 都是有效的,但是包装 (${}< /code>) 更适合复杂或更长的表达式。例如:

def number = 4
println "number is even ? ${number % 2 == 0 }"

它将打印以下内容:

number is even ? true

我们所有人都会记得在每一行的末尾添加+ "\\n",以便在 Java 中生成多行字符串。那些日子已经一去不复返了,因为 Groovy 支持多行字符串文字。多行文字以三个单引号或双引号开始(与 GString 功能相同的字符串)并以三个单引号或双引号结束:

def multilineString = '''\
    Hello
    World
'''
println multilineString

它将打印以下内容:

    Hello
    World

第 1 行的正斜杠是可选的,用于排除第一个新行。如果我们不输入正斜杠,我们将在输出的开头增加一个新行。

此外,请查看 stripMarginstripIndent 方法,用于对前导空格进行特殊处理。

如果我们的文字包含很多转义字符(例如,正则表达式),那么我们最好使用“斜线”字符串文字,它以单个正斜杠 (/ ):

def r = /(\d)+/
println r.class

它将打印以下内容:

class java.lang.String

在上面的示例中,如果我们必须使用常规字符串,那么我们必须在字符类 d 之前转义反斜杠。它看起来如下:

"(\\d)+"
Regular expressions

Groovy 支持模式运算符(~),当应用于字符串时,会给出模式对象:

def pattern = ~/(\d)+/
println pattern.class

它打印以下内容:

class java.util.regex.Pattern

我们还可以使用 find 运算符直接将字符串与模式匹配:

if ("groovy" ==~ /gr(.*)/)
  println "regex support rocks"

它将打印以下内容:

regex support rocks
Closures

闭包 在 Groovy 是一个代码块,可以像任何其他变量一样分配给引用或传递。这个概念在许多其他语言中被称为 lambda ,包括 Java 8或函数指针。

Note

从 Java 8 开始就支持 Lambda,但语法与 Groovy 的闭包有点不同。您无需使用 Java 8 即可在 Groovy 中使用闭包。

如果我们没有接触过上述任何内容,那么需要一些详细的阅读才能很好地理解这个概念,因为它为未来的许多其他高级主题奠定了基础。闭包本身就是一个巨大的话题,深入的讨论超出了本书的范围。

闭包几乎就像一个常规的方法或函数,但它也可以分配给一个变量。此外,由于它可以分配给变量,因此它也必须是一个对象;因此,它将有自己的方法:

def cl1 = {
    println "hello world!"
}

在这里,代码块被分配给一个名为 cl1 的变量。现在代码块可以在以后使用 call 方法执行,或者可以传递 cl1 变量并在以后执行:

cl1.call()

难怪它会打印以下内容:

hello world!

由于闭包就像方法一样,它们也可以接受参数:

def cl2 = { n ->
    println "value of param : $n"
}
cl2.call(101)

它打印以下内容:

value of param : 101

就像 方法一样,它们也可以返回值。如果没有明确声明 return 语句,则自动返回闭包的最后一个表达式。

当我们有接受闭包的方法时,闭包开始发光。例如,times 方法可用于整数,它接受一个闭包并执行它的次数与整数本身的值一样多;每次调用时,它都会传递当前值,就像我们循环到 0 中的值一样:

3.times(cl2)

它打印以下内容:

value of param : 0
value of param : 1
value of param : 2

我们还可以内联块并将其直接传递给方法:

3.times { println it * it }

它打印以下内容:

0
1
4

有一个名为 it 的特殊变量,如果闭包没有定义它的参数,它在块范围内可用。在前面的示例中,我们使用 it 访问传递给块的数字并将其与自身相乘以获得其平方。

闭包在回调处理等情况下非常有用,而在 Java 7 及更低版本中,我们必须使用匿名接口实现来实现相同的结果。

Data structures

Groovy 支持常用的数据结构的文字声明,这使得代码在不牺牲可读性的情况下更简洁。

List

Groovy 支持经过彻底测试的 Java Collection API,并在底层使用相同的类,但有一些额外的方法和语法糖:

def aList = []
println aList.getClass()

它打印以下内容:

class java.util.ArrayList

Note

在 Groovy 中,[] 实际上是 Java 的 List 实例而不是数组。

让我们创建另一个包含一些初始内容的列表:

def anotherList = ['a','b','c']

由于运算符重载,我们可以直观地使用列表中的许多运算符。例如,使用 anotherList[1] 将得到 b

下面是一些方便的运算符的更多示例。这将添加两个列表并将结果分配给列表变量:

def list = [10, 20, 30] + [40, 50]

这会将 60 附加到列表中:

list  <<  60 

以下两个示例只是从另一个列表中减去一个列表:

list = list – [20, 30, 40] 
list  -= [20,30,40]

遍历列表同样简单直观:

list.each {println it}

它将打印以下内容

10
50
60

传递给 each 的闭包对列表的每个元素执行,该元素作为参数 到关闭。因此,前面的代码遍历列表并打印每个元素的值。注意 it 的用法,它是列表当前元素的句柄。

Set

定义一个 set 和 list 类似,但除此之外,我们必须使用 as Set

def aSet = [1,2,3] as Set
println aSet.class

这将打印以下内容:

class java.util.LinkedHashSet

由于选择的实现类是LinkedHashSetaSet会保持插入顺序。

或者,声明变量的类型以获得正确的实现:

TreeSet anotherSet = [1,2,3]
println anotherSet.class

这将打印以下内容:

class java.util.TreeSet

向集合中添加元素就像使用间接运算符的列表一样。其他设置接口方法也可用:

aSet << 4
aSet << 3
println aSet

这将打印以下内容:

[1, 2, 3, 4]

我们没有看到条目 4 两次,因为集合是一个集合实现,根据定义,它消除了重复。

Map

Map 是任何动态语言中最重要的数据结构之一。因此,它在 Groovy 的语法中占有一席之地。可以使用地图文字 [:] 声明地图:

def a = [:]

默认选择的实现是java.util.LinkedHashMap,它保留了插入顺序:

def tool = [version:'2.8', name:'Gradle', platform:'all']

请注意,键不是字符串文字,但它们会自动转换为字符串:

println tool.name
println tool["version"]
println tool.get("platform")

除了普通的旧 get() 方法。

我们可以使用下标和点运算符在地图中放置和更新数据,当然还有旧的 put()

tool.version = "2.9"
tool["releaseDate"] = "2015-11-17"
tool.put("platform", "ALL")

Methods

following 更像是一个类似 Java 的方法,当然是有效的 Groovy 方法:

int sum(int a, int b) {
  return a + b;
}

上述方法可以简洁地改写如下:

def sum(a, b) {
  a + b
}

我们没有指定返回类型,而是声明了 def,这实际上意味着该方法可以返回任何 Object 或子类引用。然后,我们省略了形式参数的类型,因为声明 def 对于方法的形式参数是可选的。在第 2 行,我们省略了 return 语句,因为最后一个表达式的求值是由方法自动返回的。我们还省略了分号,因为它是可选的。

这两个示例都是有效的 Groovy 方法声明。但是,建议读者明智地选择类型,因为它们提供类型安全并充当方法的活文档。如果我们不声明参数的类型,如前面的方法,sum(1,"2") 也将成为有效的方法调用,更糟糕的是,它返回一个意外的结果,没有任何异常。

Calling methods

Groovy 中的方法调用 在很多情况下可以省略括号。以下两种情况都是有效的方法调用。

sum(1,2)  
sum 1, 2  
Default values of parameters

许多 一次,我们希望通过提供默认值来使参数可选,这样如果调用者不提供该值,将使用默认值.看看下面的例子:

def divide(number, by=2) {
    number/by
}

println divide (10, 5)
println divide (10)

它打印以下内容:

2
5

如果我们提供将使用的 by 参数的值,则将假定该参数的默认值 2

Methods with map parameters/named parameters

Groovy 不支持命名参数,例如 Python,但 Map 提供了非常接近相同功能的近似值:

def method(Map options) {
    def a = options.a ?: 10
    def b = options.b ?: 20
}

在前面的代码中,我们希望地图包含键 ab

Note

在第 2 行和第 3 行,注意 elvis 运算符 ?:,如果值存在并且是 ,则返回左侧值真实的;否则返回右侧(默认)值。它基本上是以下代码的简写:

options.a ? options.a : 10

现在,这个方法可以如下调用:

method([a:10,b:20])

我们可以省略方括号([]),因为地图在方法调用中有特殊的支持:

method(a:10, b:20)

现在,它显然看起来像命名参数。参数的顺序并不重要,不需要传递所有参数。此外,括号包装是可选的,就像任何方法调用一样:

method b:30, a:40
method b:30
Methods with varags

Java 中一样,可变参数由 ... 表示,但提供的类型是可选的:

def sumSquares(...numbers) {
    numbers.collect{ it * it }.sum()
}
sumSquares 1, 2, 3

在前面的示例中,数字是数组,它们具有 collect 方法,该方法接受闭包并转换集合的每个元素以生成新集合。在这种情况下,我们转换正方形集合中的数字。最后,我们使用内置的 sum 方法对所有平方求和。

Methods with closure params

闭包 很重要,因此,如果闭包是方法签名的最后一个参数,Groovy 有一个特殊的闭包语法:

def myMethod (param, cls) {
    ...
}

然后,可以按如下方式调用此方法:

myMethod(1,{ ... })
myMethod 2, {... }
myMethod(3) {...}

其中,第三个是特殊的句法支持,括号只是包裹了其他参数,而闭包写在括号外,好像它是一个方法体。

Classes

Groovy 中的类 的声明方式与 Java 类一样,但有很多 较少的仪式.默认情况下,类是公共的。它们可以使用 extends 从其他类继承或使用 implmenets 实现接口。

下面是一个非常简单的类的定义,Person,有两个属性,name年龄

class Person {
  def name, age
}

我们可以使用更具体的类型,而不是使用 def 作为属性。

Constructors

除了 到默认构造函数之外,Groovy 中的类还有一个特殊的构造函数,它获取类属性的映射。以下是我们如何使用它:

def person = new Person(name:"John Doe", age:35)

在前面的代码中,我们使用特殊的构造函数创建了 person 对象。参数是键值对,其中键是类中属性的名称。为键提供的值将为相应的属性设置。

Properties

Groovy 对属性有语言级别的支持。在前面的类中,nameage 与 Java 不同,它们不仅是字段,而且是类的属性getter 和 setter 就位。默认情况下,字段是私有的,它们的公共访问器和修改器(getter 和 setter)是自动生成的。

我们可以调用 getAge()/setAge()getName()/setName() 我们在上面创建的 person 对象上的方法。但是,还有一种更简洁的方法可以做到这一点。我们可以像访问公共字段一样访问 属性,但在幕后,Groovy 通过 getter 和 setter 对其进行路由。我们试试看:

println person.age
person.age = 36
println person.age

它打印以下内容:

35
36

在前面的代码中,在第 1 行,person.age 实际上是对 person.getAge() 的调用,因此,它返回人的年龄。然后,我们使用 person.age 在右侧使用赋值运算符和值来更新年龄。我们没有更新该字段,但它在内部通过 setter setAge() 传递。这只是可能的,因为 groovy 为属性提供了语法支持。

我们可以为所需的字段提供我们自己的 getter 和/或 setter,它们将优先于生成的字段,但只有在我们有一些逻辑可以写入这些字段时才需要。例如,如果我们想设置一个正的年龄值,那么我们可以提供我们自己的 setAge() 实现,每当属性更新时都会使用它:

  void setAge(age){
    if (age < 0) 
      throw new IllegalArgumentException("age must be a positive number")
    else
      this.age = age
  }

对属性的支持导致类定义中的样板代码显着减少并增强了可读性。

Tip

属性是 Groovy 中的一等公民。展望未来,每当我们提到属性时,不要混淆属性和字段。

Instance methods

我们可以像在 Java 中一样向类添加实例和静态方法:

def speak(){
  println "${this.name} speaking"
}
static def now(){
  new Date().format("yyyy-MM-dd HH:mm:ss")
}

如上所述,方法 部分没有使用类,而是按原样应用于类中的方法。

Note

脚本就是类

事实上,我们上面讨论的方法是在一个类中,它们不是自由浮动的函数。当脚本被透明地转换为类时,感觉就像我们在使用函数一样。

到目前为止,我相信您已经喜欢 Groovy。 Groovy 中还有很多内容需要介绍,但我们必须将注意力转移到 Gradle 上。但是,我希望对 Groovy 产生足够的好奇心,以便您可以欣赏它作为一种语言并自行探索更多内容。参考资料部分包含一些很好的资源。

Another look at applying plugins

现在我们已经了解了基本的 Groovy,让我们在 Gradle 构建脚本的上下文中使用它。在前面的章节中,我们已经看到了应用插件的语法。它看起来如下:

apply plugin: 'java'

如果我们仔细看,apply 是一个方法调用。我们可以将参数包装在括号中:

apply(plugin: 'java')

接受映射的方法可以像命名参数一样传递键值。但是,为了更清楚地表示 Map,我们可以将参数包装在 [] 中:

apply([plugin: 'java'])

最后,apply 方法被隐式应用到 project 对象上(我们将在本章接下来的部分中看到这一点) .所以,我们也可以在 project 对象的引用上调用它:

project.apply([plugin: 'java'])

因此,从 前面的示例中,我们可以看到将插件应用于项目的语句仅仅是对 上的方法调用的语法糖code class="literal">project 对象。我们只是使用 Gradle API 编写 Groovy 代码。此外,一旦我们意识到这一点,我们对理解构建脚本语法的看法就会改变。

Gradle – an object-oriented build tool


如果我们以面向对象的方式考虑构建系统,我们会立即想到以下类:

  • 代表正在构建的系统的 project

  • task 封装了需要执行的构建逻辑片段

好吧,我们很幸运。正如我们所料,Gradle 创建 projecttask 类型的对象。这些对象可以在我们的构建脚本中访问,供我们自定义。当然,底层实现并不简单,API 也非常复杂。

project 对象是 API 的核心部分,通过构建脚本公开和配置。 project 对象在脚本中可用,以便在 project 对象上智能调用没有对象引用的方法。我们刚刚在上一节中看到了一个这样的例子。只需阅读项目 API 即可理解大部分构建脚本语法。

task 对象是为在构建文件中直接声明的每个任务以及插件创建的。我们已经在 Chapter 1 运行你的第一个 Gradle 任务中创建了一个非常简单的任务 并使用了来自 Chapter 2, 构建 Java 项目第 3 章构建 Web 应用程序

Note

正如我们所见,有些任务已经在我们的构建中可用,而无需我们在构建文件中添加一行(例如 help 任务和 tasks 任务,以此类推)。即使对于这些任务,我们也会有任务对象。

我们很快就会看到这些对象是如何以及何时创建的。

Build phases


Gradle 构建在每次调用时遵循一个非常简单的生命周期。 构建经过三个阶段:初始化、配置和执行。当调用 gradle 命令时,并不是所有写在我们构建文件中的代码都是从上到下顺序执行的。仅执行与当前构建阶段相关的代码块。此外,构建阶段的顺序决定了代码块何时执行。一个例子是任务配置与任务执行。了解这些阶段对于正确配置我们的构建非常重要。

Initialization

Gradle首先判断当前项目是否有子项目,或者它是否是构建中的唯一项目。对于多项目构建,Gradle 会确定哪些项目(或子模块,许多人更喜欢称之为)必须包含在构建中。我们将在下一章看到多项目构建。 Gradle 然后为根项目和项目的每个子项目创建一个 Project 实例。对于我们目前看到的单模块项目,在这个阶段没有太多需要配置的。

Configuration

在这个 阶段,参与项目的构建脚本将根据在初始化阶段创建的相应项目对象进行评估。在多模块项目的情况下,评估以广度方式进行,也就是说,所有兄弟项目将在子项目之前进行评估和配置。但是,此行为是可配置的。

请注意,执行脚本并不意味着任务也被执行。为了快速验证这一点,我们可以在 build.gradle 文件中添加一个 println 语句,并创建一个打印一个信息:

task myTask << {
  println "My task is executed"
}
// The following statement will execute before any task 
println "build script is evaluated"

如果我们执行以下代码:

$ gradle -q myTask

我们将看到以下输出:

build script is evaluated
My task is executed

事实上,也可以选择任何内置任务,例如 help

$ gradle -q help

在执行任何任务之前,我们仍然会看到我们的 构建脚本被评估 消息。这是为什么?

当一个脚本 被求值时,脚本中的所有语句都会按顺序执行。这就是执行根级别的 println 语句的原因。如果你注意到,一个任务动作实际上是一个闭包。因此,它仅在语句执行期间附加到任务。但是,闭包本身尚未执行。动作闭包中的语句仅在任务执行时执行,这仅在下一阶段发生。

仅在此阶段配置任务。无论要调用什么任务,都会配置所有任务。 Gradle 准备了一个 有向无环图 (DAG) 表示任务以确定任务依赖性和执行顺序。

Execution

在这个 阶段,Gradle 根据作为命令行参数传递的任务名称和当前目录等参数确定需要运行哪些任务。这是将执行任务操作的地方。因此,在这里,如果任务要运行,动作闭包将实际执行。

Note

在随后的调用中,Gradle 会智能地确定哪些任务需要实际运行,哪些可以跳过。例如,对于一个编译任务,如果在最后一次构建之后源文件没有变化,那么再次编译是没有意义的。在这种情况下,可以跳过执行。我们可以在标记为 UP-TO-DATE 的输出中看到此类任务:

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

在前面的输出中,由于与之前的构建没有任何变化,Gradle 实际上跳过了每个任务。但是,对于我们编写的自定义任务,这不会发生,除非我们告诉 Gradle 确定任务是否需要执行的逻辑。

Life cycle callbacks

Gradle 提供了各种钩子,用于在生命周期事件的各个点执行代码。我们可以在构建脚本中实现回调接口或为 DSL 提供回调闭包。例如,我们可以使用 beforeEvaluateafterEvaluate 方法监听项目评估之前和之后的事件="literal">项目。我们不打算单独查看它们,而是 ProjectGradle(接口名称不要与如果我们觉得需要实现生命周期回调,API 和 DSL 文档是检查可用回调的正确位置。

Gradle Project API


如前所述,Gradle 为每个 构建创建一个 project 对象。在初始化阶段为我们准备 gradle。该对象可在我们的构建脚本中使用 project 参考。作为 API 的核心部分,该对象有许多可用的方法和属性。

Project methods

我们一直在使用 project API,甚至没有意识到我们正在调用 project 对象上的方法。根据一些管理规则,如果没有提供显式引用,则构建脚本中的所有顶级方法调用都会在项目对象上调用。

让我们重写 第 1 章 运行你的第一个 Gradle 任务< /em> 将项目引用用于方法调用:

project.apply plugin: 'java'

project.repositories {
    mavenCentral()
}

project.dependencies {
    testCompile 'junit:junit:4.11'
}

正如我们在本章前面所看到的,apply 项目 。所谓dependencies块其实就是project dependencies()的方法code> 接受闭包。 repositories 部分也是如此。我们可以在闭包块周围添加括号,使其看起来像一个普通的旧方法调用:

project.repositories({...})
project.dependencies({...})

这个对象还有很多有趣的方法,我们将在接下来的章节和章节中再次看到,无论是否明确引用 project 对象。

Project properties

project 对象有几个可用的属性。有些属性是只读属性,如namepathparent 等等,而其他的都是可读和可写的。

例如,我们可以设置 project.description 来提供我们项目的描述。我们可以使用 project.version 属性来设置项目的版本。此版本将由其他任务使用,例如 Jar 以在生成的工件中包含版本号。

Note

我们无法从 build.gradle 文件中更改 project.name,但我们可以使用 settings.gradle 在同一个项目中设置项目名称。当我们了解多项目构建时,我们将更详细地看到这个文件。

除了通过名称直接访问属性外,我们还可以在 project 对象上使用以下方法访问属性。

要检查属性是否存在,请使用以下方法:

boolean hasProperty(String propertyName)

要获取给定属性名称的属性值,请使用以下方法:

Object property(String propertyName)

要为给定的属性名称设置属性的值,请使用以下方法:

void setProperty(String name, Object value)

例如,让我们创建一个 build.gradle 文件,其内容如下:

description = "a sample project"
version = "1.0"

task printProperties << {
    println project.version
    println project.property("description")
}

执行以下任务:

$ gradle -q printProperties
1.0
a sample project

如前所述,在Groovy 中,我们可以使用property = value 语法来调用setter。我们在 project 对象上设置 descriptionversion 属性。然后,我们使用 project 引用和 description 使用 project 对象上的 "literal">property() 方法。

我们上面看到的属性必须存在于项目中,否则构建失败并显示 Could not find property ... 消息。

Extra properties on a project

Gradle 使得在项目中存储用户定义的属性变得非常容易,同时仍然能够享受项目属性语法的精妙之处。我们所要做的就是使用 ext 命名空间来为自定义属性赋值。然后,可以像常规项目属性一样在项目上访问此属性。这是一个例子:

ext.abc = "123"
task printExtraProperties << {
    println project.abc
    println project.property("abc")
    println project.ext.abc
}

执行以下任务:

$ gradle -q printExtraProperties
123
123
123

在前面的示例中,我们声明了一个名为 abc 的自定义属性,并为其分配了值 123。我们没有使用 project 引用,因为它在脚本根级别隐式可用。在任务操作中,我们首先使用项目引用直接打印它,就像它是 Project 上的属性一样。然后,我们使用 property() 方法和 project.ext 引用进行访问。请注意,在任务的动作闭包中,我们应该使用 project 引用以避免任何歧义。

在子项目(模块)中可以访问额外的属性。也可以在其他对象上设置额外的属性。

Note

我们可以使用 def 声明局部变量。但是,这些变量在词法范围之外是不可访问的。此外,它们不可查询。

尽管我们已经查看了一些方法和属性,但在这里涵盖所有这些是不切实际的;因此,值得花一些时间阅读 project 接口的 API 和 DSL 文档。

Tasks


正如我们目前所见,task 是一个执行某些构建逻辑的命名操作。它是构建工作的一个单元。例如,cleancompiledist等等,都是如果我们必须为我们的项目编写任务,我们很容易想到典型的构建任务。任务或多或少类似于 Ant 的目标。

创建任务的最简单方法如下:

task someTask

在我们进一步讨论任务之前,让我们花点时间思考一下任务创建。

我们使用了 taskName 任务形式的语句。

如果我们把它改写成task (taskName),它马上就会看起来像方法调用。

正如我们现在可能已经猜到的那样,前面的方法可用于项目对象。

因此,我们也可以编写以下内容之一:

  • project.task "myTask"

  • project.task("myTask")

请注意,在后面的示例中,我们必须将任务名称作为字符串传递。 task taskName 是一种特殊形式,我们可以将 taskName 用作文字而不是字符串。这是由 Groovy AST 转换魔法完成的。

该项目有几种创建任务对象的任务方法:

Task task(String name)

Task task(String name, Closure configureClosure)

Task task(Map<String, ?> args, String name)

Task task(Map<String, ?> args, String name, Closure configureClosure)

但是,在本质中,我们可能会在创建任务和配置闭包以配置任务时传递一些键值作为命名参数。

我们实际上是在创建一个 Task 类型的对象(确切的类名现在并不重要)。我们可以查询这个对象的属性和调用方法。 Gradle 很好地使这个 task 对象可供使用。在漂亮的 DSL 背后,我们实际上是在编写一个脚本,以一种面向对象的方式创建构建逻辑。

Attaching actions to a task

Task 对象,,例如上面创建的对象,并没有多大作用。事实上,它没有附加任何动作。我们需要将操作附加到 Task 对象,以便 Gradle 在任务运行时执行这些操作。

Task 对象有一个名为 doLast 的方法,它接受一个闭包。 Gradle 确保传递给此方法的所有闭包都按照它们传递的顺序执行:

someTask.doLast({
    println "this should be printed when the task is run"
})

我们现在可以做的是再次调用 doLast

someTask.doLast({
    println "this should ALSO be printed when the task is run"
})

此外,在另一种语法中:

someTask {
    doLast {
        println "third line that should be printed"
    }
}

有多种方法可以将 doLast 逻辑添加到任务中,但最惯用的,也许是一种简洁的方法如下:

someTask << {
    println "the action of someTask"
}

就像 Project 对象,我们有 Task 可以访问其方法和属性的对象。但是,与 Project 对象不同,它在脚本的顶层并不隐式可用,而仅在任务的配置范围内可用。此外,直观地,我们可以说每个 build.gradle 会有多个 Task 对象。稍后我们将看到访问 Task 对象的各种方法。

Task flow control

项目中的任务 可能相互依赖。在本节中,我们将看到项目任务中可能存在的不同类型的关系。

dependsOn

任务的执行取决于其他任务的成功完成。例如,为了创建一个可分发的 JAR 文件,代码应该首先被编译并且“类”文件应该已经存在。在这种情况下,我们不希望用户从命令行显式指定所有任务及其顺序,如下所示:

$ gradle compile dist

这是容易出错的。我们可能会忘记包含一项任务,或者如果有多个任务依赖于先前任务的成功完成,则排序可能会变得复杂。希望能够指定是否:

task compile << {
    println 'compling the source'
}

task dist(dependsOn: compile) << {
    println "preparing a jar dist"
}

finalizedBy

我们也可以声明,如果一个任务被调用,它应该紧跟着另一个任务,即使另一个任务没有被显式调用。这与 dependsOn 不同,后者在调用任务之前执行另一个任务。在finalizedBy的情况下,在被调用的任务执行完之后再执行另一个任务:

task distUsingTemp << {
  println ("preapring dist using a temp dir")
}

task cleanup << {
  println("removing tmp dir")
}

distUsingTemp.finalizedBy cleanup

onlyIf

我们可以指定一个条件,如果满足,任务就会被执行:

cleanup.onlyIf { file("/tmp").exists()}

mustRunAfter and shouldRunAfter

时间,如果这种关系与 dependsOn< /代码>。例如,如果我们执行以下命令:

$ gradle build clean

然后,不相关的 任务将按照它们在命令行中指定的顺序执行,在这种情况下这是没有意义的。

在这种情况下,我们可以添加以下代码行:

build.mustRunAfter clean

这告诉 Gradle,如果两个任务都在任务图中,那么 build 必须在 clean 运行之后运行.在这里,build 不依赖于 clean。

shouldRunAftermustRunAfter 的区别在于前者对 Gradle 的提示性更强,但并不强制 Gradle 遵循顺序每时每刻。在以下两种情况下,Gradle 可能不支持 shouldRunAfter

  • 在它引入循环排序的情况下。

  • 在并行执行的情况下,只有 shouldRunAfter 任务还没有成功完成且其他依赖都满足时,shouldRunAfter 会被忽略。

Creating tasks dynamically

Gradle 的一大优点是我们也可以动态创建任务。这意味着在编写构建时并不完全知道任务的名称和逻辑,但是根据一些可变参数,任务将自动添加到我们的 Gradle 项目中。

让我们试着用一个例子来理解:

10.times { number ->
  task "dynamicTask$number" << {
    println "this is dynamic task number # $number "
  }
}

在前面的人为示例中,我们正在创建并动态添加十个任务到我们的构建中。尽管它们都只是打印任务编号,但动态创建和添加任务到我们的项目的能力非常强大。

Setting default tasks

到目前为止,我们 一直在使用任务名称调用gradle 命令行界面。这在本质上是一种重复,尤其是在开发过程中,像 Gradle 这样的工具可以让我们覆盖:

defaultTasks "myTaskName", "myOtherTask"

设置默认任务是明智的,这样如果我们不指定任何任务名称,则默认执行设置的任务。

在前面的例子中,不带任何参数从命令行运行 gradle 会按照 defaultTasks

Task types

到目前为止,我们看到的 任务本质上是临时性的。我们必须为任务执行时需要执行的任务操作编写代码。但是,无论我们在构建哪个项目,如果我们有能力对现有逻辑进行一些配置更改,那么任务操作的逻辑不需要更改。例如,当您复制文件时,只有源、目标和包含/排除模式发生变化,但如何将文件从一个位置复制到另一个位置以遵守包含/排除模式的实际逻辑保持不变。所以,如果一个项目中需要两个类似复制的任务,比如说 copyDocumentationdeployWar,我们真的想要写一个完整的逻辑来复制选定的文件两次?

适用于非常小的构建(例如我们章节中的示例),但该方法不能很好地扩展。如果我们继续编写任务操作来执行这些普通操作,那么我们的构建脚本将很快膨胀到无法管理的状态。

自定义任务类型是 Gradle 的解决方案,用于将可重用的构建逻辑抽象到自定义任务类中,从而在任务对象上公开输入/输出配置变量。这有助于我们调整类型化的任务以满足我们的特定需求。这有助于我们保持通用构建逻辑的可重用和可测试性。

临时任务操作的另一个问题是它本质上是必不可少的。为了工具的灵活性,Gradle 允许我们在构建脚本中强制编写自定义逻辑。但是,在我们的构建脚本中过度使用命令式代码会使构建脚本无法维护。 Gradle 应该尽可能以声明的方式使用。命令式逻辑应封装在自定义任务类中,同时公开任务配置供用户配置。在 Gradle 的术语中,自定义任务类被称为 增强的任务

自定义任务类型充当模板,为通用构建逻辑提供一些合理的默认值。我们仍然需要在构建中声明一个任务,但我们只是告诉 Gradle 这个任务的类型并配置这个任务类型的设置,而不是重新编写整个任务动作块。 Gradle 已经附带了许多自定义任务类型;例如,复制执行删除Jar, Sync, Test, JavaCompile , Zip 等等。我们也可以轻松编写自己的增强任务。我们将非常简要地了解这两种情况。

Using task types

我们可以使用以下语法配置Copy类型的任务:

task copyDocumentation(type:Copy) {
from file("src/docs/html")
into file("$buildDir/docs")
}

在前面的示例中,第一个重要区别是我们传递了一个键 type,其值作为自定义任务的类名,即 Copy 在这种情况下。另外,请注意没有 没有 doLast 或间接 (< <) 运算符。我们传递给这个任务的闭包实际上是在构建的配置阶段执行的。闭包内的方法调用被委托给正在配置的隐式可用的 task 对象。我们这里没有写任何逻辑,只是为一个类型为Copy的任务提供了配置。在我们继续编写临时任务操作之前,总是值得看看可用的自定义任务。

Creating task types

如果我们 现在回过头来看,我们为示例任务的任务操作编写的代码主要是 println< /code> 语句将在 System.out 上打印给定的消息。现在,想象一下我们发现 System.out 不符合我们的要求,我们应该使用文本文件来打印来自任务的消息。我们需要完成所有任务并更改实现以写入文件而不是 println

有更好的方法来处理这种不断变化的需求。我们可以通过提供我们自己的任务类型来利用这里任务类型的功能。让我们将以下代码放入我们的 build.gradle 中:

class Print extends DefaultTask {
  @Input
  String message = "Welcome to Gradle"

  @TaskAction
  def print() {
    println "$message"
  }
}

task welcome(type: Print)

task thanks(type: Print) {
  message = "Thanks for trying custom tasks"
}

task bye(type: Print)
bye.message = "See you again"

thanks.dependsOn welcome
thanks.finalizedBy bye

在前面的代码示例中:

  • 我们首先创建了一个扩展 DefaultTask 的类(这将是我们的任务类型),该类已经在 Gradle 中定义。

  • 接下来,我们在名为 message 的属性上使用 @Input 为我们的任务声明了一个可配置的输入。我们任务的消费者可以配置这个属性。

  • 然后,我们在 print 方法上使用了 @TaskAction 注解。这个方法在我们的任务被调用时执行。它只是使用 println 来打印 message

  • 然后,我们声明了三个任务;都使用不同的方式来配置我们的任务。注意没有任何任务动作。

  • 最后,我们应用任务流控制技术来声明任务依赖关系。

如果我们现在运行 thanks 任务,我们可以看到预期的输出,如下所示:

$ gradle -q thanks
Welcome to Gradle
Thanks for trying custom tasks
See you again

这里需要注意的几点如下:

  • 如果我们想改变我们的打印逻辑的实现,只有一个地方我们需要做改变,我们自定义任务类的print方法。

  • 使用使用任务类型的任务,它们的工作方式与任何其他任务一样。他们还可以使用 doLast {}, << {},但通常不是必需的。

References


接下来的部分提到了一些对 Groovy 有用的 参考。

Groovy

有大量可用于 Groovy 的在线参考资料。我们可以从:

以下是关于 Groovy 的书籍列表:

Gradle API and DSL used in this chapter

Gradle 的官方 API 和 DSL 文档是探索和了解本章讨论的各种类的好地方。这些 API 和 DSL 非常丰富,值得我们花时间阅读。

Summary


我们从对 Groovy 语言的快速特性概述开始本章,涵盖了一些有助于我们理解 Gradle 语法和编写更好的构建脚本的主题。然后,我们查看了 Gradle 向我们的构建脚本公开的 API,以及如何通过 DSL 使用 API。我们还介绍了 Gradle 构建阶段。然后,我们研究了如何创建、配置任务、在它们之间建立依赖关系以及默认运行任务的方式。

阅读本章后,我们应该能够理解 Gradle DSL,而不仅仅是试图记住语法。我们现在可以阅读和理解任何给定的 Gradle 构建文件,并且我们现在应该能够轻松编写自定义任务。

这一章可能感觉有点冗长和复杂。我们应该花一些时间练习并重新阅读不清楚的部分,并查找整章提供的在线参考资料。未来的章节将一帆风顺。