Gradle 工作原理 第 3 部分 - 构建脚本
之前在 *Gradle 工作原理* 中
这是 *Gradle 工作原理* 系列的第三篇博客。在这篇博客中,我们将解释构建脚本执行期间发生的事情。
Kotlin & Groovy DSL
如果您是 Java 开发人员,当您打开任何 Gradle 构建脚本(例如 build.gradle.kts
或 build.gradle
)时,您可能会首先被花括号的特殊语法弄糊涂
// Kotlin DSL:
plugins {
id("some.plugin") version "0.0.1"
}
// or Groovy DSL:
plugins {
id "some.plugin" version "0.0.1"
}
这是什么?Gradle 执行这些类型的脚本时会发生什么?
简而言之:它们是基于 领域特定语言 (DSL),分别构建在 Kotlin 编程语言之上(用于 .gradle.kts
文件)和 Groovy 编程语言之上(用于 .gradle
文件)。这些 DSL 有一些隐式规则,使得它们看起来非常令人困惑。
隐式规则 1:Lambda/闭包
首先,{ ... }
在 Groovy 和 Kotlin 中都是一个特殊的对象。这个对象在 Kotlin 中被称为 lambda,在 Groovy 中被称为 闭包。它们类似于其他编程语言中的函数对象,例如 Java 的 lambda 或 JavaScript 的函数对象。
你可以将 plugins { ... }
看作是一个方法调用,其中传递了一个 Kotlin lambda 对象或 Groovy 闭包对象作为参数,因为 Groovy 和 Kotlin 都允许你省略括号。
plugins(function() {
...
})
此外,还有一个值得注意的 DSL:在 Kotlin/Groovy 中,如果函数的最后一个参数是 lambda/闭包,则允许将其放在括号之外。例如,以下代码片段
tasks.register("myTask") {
...
doLast {
...
}
}
等同于
tasks.register("myTask", function() {
...
doLast(function() {
...
})
})
函数内部的代码可能会立即执行,也可能稍后执行,具体取决于特定方法的实现。
隐式规则 2:链式方法调用
在上面的 plugins { }
示例中,Kotlin 版本:id("some.plugin") version "0.0.1"
和 Groovy 版本:id "some.plugin" version "0.0.1"
都等同于链式方法调用 id("some.plugin").version("0.0.1")
。
等等,为什么?
因为 id("some.plugin") version "0.0.1"
中的 version
实际上是 Kotlin 中的 中缀函数,它在 这里 定义,而 id "some.plugin" version "0.0.1"
是 Groovy 中的 “命令链”。
我们不会解释 Groovy 和 Kotlin DSL 中的所有隐式规则(因为这需要另一个完整的博客系列 :-P),你只需要理解它们以某种方式映射到 Gradle API 方法。请参阅 Kotlin DSL 入门 和 Groovy DSL 入门。
但是,方法调用 id("some.plugin")
中的 this
指的是什么呢?
函数内部的代码是在一个 this
对象上执行的,在 Kotlin lambda 中被称为 “接收者”,在 Groovy closure 中被称为 “委托”。Gradle 会确定正确的 this
对象,并在该 this
对象上调用方法。在这个例子中,this
对象的类型是 PluginDependenciesSpec
。
构建脚本执行
一旦我们揭开了 DSL 的所有机制,Gradle 构建脚本就只是建立在 DSL 之上的几个 Gradle API 调用。与几乎所有其他编程语言一样,Gradle 从上到下逐行执行构建脚本。
当然,构建脚本必须先被编译成字节码,然后才能在 JVM 中执行。Gradle 透明地执行此操作,给人一种构建脚本正在被解释和执行的印象。
构建脚本中的外部依赖项
考虑以下构建脚本
// build.gradle.kts
import com.android.build.gradle.tasks.LintGlobalTask
plugins {
id("com.android.application") version "7.4.0"
}
tasks.withType<LintGlobalTask>().configureEach {
...
}
如何在不指定 com.android.build.gradle.tasks.LintGlobalTask
的依赖项的情况下编译此构建脚本?你可能会说,“它不是来自下面的 plugins { }
块吗?”
但请记住,要执行 plugins { }
块,必须先编译构建脚本。现在这是一个鸡生蛋的问题:要获得 LintGlobalTask
的依赖项,我们必须编译并运行构建脚本,但要编译构建脚本,我们必须获得 LintGlobalTask
的依赖项。
Gradle 对 plugins { }
的处理方式如下
plugins
块首先被提取并执行;- 解析后的依赖项将添加到整个构建脚本的类路径中;
- 构建脚本被编译并执行。
类似的事情也发生在 buildscript { }
代码块中:你可以显式地指定构建脚本编译和执行的依赖项。这样,你可以利用 JVM 生态系统中众多可用的库来增强你的构建脚本。
下一步
构建脚本调用 Gradle API 来配置构建,在此过程中会发生奇妙的事情。可以将构建脚本打包成 Gradle 插件,以提高可重用性和性能。
在该系列的下一篇文章中,我们将解释 Gradle 插件内部的工作原理。