Gradle 工作原理 第 3 部分 - 构建脚本

之前在 *Gradle 工作原理* 中

  1. Gradle 工作原理 第 1 部分 - 启动
  2. Gradle 工作原理 第 2 部分 - 守护进程内部

这是 *Gradle 工作原理* 系列的第三篇博客。在这篇博客中,我们将解释构建脚本执行期间发生的事情。

Kotlin & Groovy DSL

如果您是 Java 开发人员,当您打开任何 Gradle 构建脚本(例如 build.gradle.ktsbuild.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 { } 的处理方式如下

  1. plugins 块首先被提取并执行;
  2. 解析后的依赖项将添加到整个构建脚本的类路径中;
  3. 构建脚本被编译并执行。

类似的事情也发生在 buildscript { } 代码块中:你可以显式地指定构建脚本编译和执行的依赖项。这样,你可以利用 JVM 生态系统中众多可用的库来增强你的构建脚本。

下一步

构建脚本调用 Gradle API 来配置构建,在此过程中会发生奇妙的事情。可以将构建脚本打包成 Gradle 插件,以提高可重用性和性能。

在该系列的下一篇文章中,我们将解释 Gradle 插件内部的工作原理。

讨论