Gradle 工作原理第三部分 - 构建脚本
引言
《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 执行这些脚本时会发生什么?
简短的回答是:它们是基于 Kotlin 编程语言(用于 .gradle.kts
文件)或基于 Groovy 编程语言(用于 .gradle
文件)的 DSL(领域特定语言)。这些 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 闭包中被称为“委托”。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 插件的内部工作原理。