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 闭包中称为“委托”。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 插件的幕后工作原理。