Gradle 最佳实践
使用 Kotlin DSL
在编写新构建或在现有构建中创建新子项目时,优先使用 Kotlin DSL(build.gradle.kts
),而不是 Groovy DSL(build.gradle
)。
解释
Kotlin DSL 比 Groovy DSL 有几个优点:
-
严格类型:IDE 为 Kotlin DSL 提供更好的自动补全和导航。
-
提高可读性:用 Kotlin 编写的代码通常更容易理解和跟踪。
-
单一语言栈:已经使用 Kotlin 进行生产和测试代码的项目,不需要仅仅为了构建而引入 Groovy。
自 Gradle 8.0 以来,Kotlin DSL 已成为新 Gradle 构建的默认设置,这些构建使用 gradle init
创建。Android Studio 也默认使用 Kotlin DSL。
使用最新的 Gradle 小版本
保持在当前使用的 Gradle 主要版本的最新小版本上,并定期将插件更新到最新的兼容版本。
解释
Gradle 遵循相当可预测的、基于时间的发布节奏。只有当前和上一个主要版本的最新小版本得到积极支持。
我们建议采用以下策略:
-
尝试直接升级到当前 Gradle 主要版本的最新小版本。
-
如果失败,一次升级一个次要版本,以隔离回归或兼容性问题。
每个新的次要版本包括:
-
性能和稳定性改进。
-
弃用警告,帮助您为下一个主要版本做准备。
-
已知错误和安全漏洞的修复。
使用 wrapper
任务更新您的项目
./gradlew wrapper --gradle-version <version>
使用 plugins
块应用插件
您应该始终使用 plugins
块在构建脚本中应用插件。
解释
plugins
块是 Gradle 中应用插件的首选方式。插件 API 允许 Gradle 更好地管理插件的加载,它比显式地向构建脚本的类路径添加依赖项以使用 apply
方法更简洁,也更不容易出错。
它允许 Gradle 优化插件类的加载和重用,并帮助工具了解插件将添加到构建脚本中的扩展的潜在属性和值。它被限制为幂等(每次都产生相同的结果)和无副作用(Gradle 可以在任何时候安全执行)。
示例
不要这样做
buildscript {
repositories {
gradlePluginPortal() (1)
}
dependencies {
classpath("com.google.protobuf:com.google.protobuf.gradle.plugin:0.9.4") (2)
}
}
apply(plugin = "java") (3)
apply(plugin = "com.google.protobuf") (4)
buildscript {
repositories {
gradlePluginPortal() (1)
}
dependencies {
classpath("com.google.protobuf:com.google.protobuf.gradle.plugin:0.9.4") (2)
}
}
apply plugin: "java" (3)
apply plugin: "com.google.protobuf" (4)
1 | 声明一个仓库:要使用旧的插件应用语法,您需要明确告诉 Gradle 在哪里找到插件。 |
2 | 声明一个插件依赖:要将旧的插件应用语法与第三方插件一起使用,您需要明确告诉 Gradle 插件的完整坐标。 |
3 | 应用一个核心插件:使用这两种方法非常相似。 |
4 | 应用一个第三方插件:语法与核心 Gradle 插件相同,但在构建脚本的应用点没有版本。 |
取而代之这样做
plugins {
id("java") (1)
id("com.google.protobuf").version("0.9.4") (2)
}
plugins {
id("java") (1)
id("com.google.protobuf").version("0.9.4") (2)
}
1 | 应用一个核心插件:使用这两种方法非常相似。 |
2 | 应用一个第三方插件:您可以使用 plugins 块中的方法链来指定版本。 |
不要使用内部 API
不要使用包中任何部分为 internal
的 API,或名称中带有 Internal
或 Impl
后缀的类型。
解释
使用内部 API 本质上是危险的,并且可能在升级过程中导致重大问题。Gradle 和许多插件(例如 Android Gradle Plugin 和 Kotlin Gradle Plugin)会将这些内部 API 视为在任何新 Gradle 版本发布时(即使是小版本)都可能未经通知地发生破坏性更改。有许多案例表明,即使是经验丰富的插件开发人员也因使用此类 API 而导致其用户遇到意想不到的故障。
如果您需要缺少特定功能,最好提交功能请求。作为临时解决方案,可以考虑将必要的代码复制到您自己的代码库中,并使用复制的代码扩展 Gradle 公共类型,以实现您自己的自定义实现。
示例
不要这样做
import org.gradle.api.internal.attributes.AttributeContainerInternal
configurations.create("bad") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.LIBRARY))
}
val badMap = (attributes as AttributeContainerInternal).asMap() (1)
logger.warn("Bad map")
badMap.forEach { (key, value) ->
logger.warn("$key -> $value")
}
}
import org.gradle.api.internal.attributes.AttributeContainerInternal
configurations.create("bad") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
}
def badMap = (attributes as AttributeContainerInternal).asMap() (1)
logger.warn("Bad map")
badMap.each {
logger.warn("${it.key} -> ${it.value}")
}
}
1 | 应该避免强制转换为 AttributeContainerInternal 并使用 toMap() ,因为它依赖于内部 API。 |
取而代之这样做
configurations.create("good") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.LIBRARY))
}
val goodMap = attributes.keySet().associate { (1)
Attribute.of(it.name, it.type) to attributes.getAttribute(it)
}
logger.warn("Good map")
goodMap.forEach { (key, value) ->
logger.warn("$key -> $value")
}
}
configurations.create("good") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
}
def goodMap = attributes.keySet().collectEntries {
[Attribute.of(it.name, it.type), attributes.getAttribute(it as Attribute<Object>)]
}
logger.warn("Good map")
goodMap.each {
logger.warn("$it.key -> $it.value")
}
}
1 | 实现自己的 toMap() 版本,只使用公共 API,会健壮得多。 |
模块化你的构建
通过将代码拆分为多个项目来模块化您的构建。
解释
将构建的源文件拆分为多个 Gradle 项目(模块)对于利用 Gradle 的自动工作规避和并行化功能至关重要。当源文件更改时,Gradle 只重新编译受影响的项目。如果所有源文件都驻留在单个项目中,Gradle 就无法避免重新编译,也无法并行运行任务。将源文件拆分为多个项目可以通过最小化每个子项目的编译类路径并确保注释和符号处理器等代码生成工具仅在相关文件上运行来提供额外的性能优势。
尽快这样做。不要等到源代码文件或类的数量达到任意数字才这样做,而是在一开始就根据代码库中存在的任何自然边界将构建组织成多个项目。
如何最好地拆分源文件因每个构建而异,因为它取决于该构建的具体情况。以下是我们发现的一些常见的模式,它们可以很好地工作并形成内聚的项目:
-
API 与实现
-
前端与后端
-
核心业务逻辑与 UI
-
垂直切片(例如,每个包含 UI + 业务逻辑的功能模块)
-
源代码生成的输入与其消费者
-
或者仅仅是密切相关的类。
最终,具体方案的重要性不如确保您的构建逻辑清晰且一致。
将构建扩展到数百个项目是很常见的,Gradle 旨在扩展到这个大小甚至更大。在极端情况下,只包含一个或两个类的微小项目可能适得其反。但是,您通常应该倾向于添加更多项目而不是更少。
示例
不要这样做
├── app // This project contains a mix of classes
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
│ └── GuavaUtil.java
│ └── Main.java
│ └── Util.java
├── settings.gradle.kts
├── app // This project contains a mix of classes
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
│ └── GuavaUtil.java
│ └── Main.java
│ └── Util.java
├── settings.gradle
include("app") (1)
include("app") (1)
plugins {
application (2)
}
dependencies {
implementation("com.google.guava:guava:31.1-jre") (3)
implementation("commons-lang:commons-lang:2.6")
}
application {
mainClass = "org.example.Main"
}
plugins {
id 'application' (2)
}
dependencies {
implementation 'com.google.guava:guava:31.1-jre' (3)
implementation 'commons-lang:commons-lang:2.6'
}
application {
mainClass = "org.example.Main"
}
1 | 此构建只包含一个项目(除了根项目),其中包含所有源代码。如果任何源文件发生任何更改,Gradle 都必须重新编译和重新构建所有内容。虽然增量编译会有所帮助(特别是在这个简化的示例中),但这仍然不如避免编译高效。Gradle 也无法并行运行任何任务,因为所有这些任务都在同一个项目中,因此这种设计无法很好地扩展。 |
2 | 由于此构建中只有一个项目,因此必须在此处应用 application 插件。这意味着 application 插件将影响构建中的所有源文件,即使是那些不需要它的文件。 |
3 | 同样,这里的依赖项只被 util 的每个特定实现所需要。使用 Guava 的实现不需要访问 Commons 库,但它确实有访问权限,因为它们都在同一个项目中。这也意味着每个子项目的类路径比其需要的要大得多,这可能导致更长的构建时间和其他混乱。 |
取而代之这样做
├── app
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Main.java
├── settings.gradle.kts
├── util
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Util.java
├── util-commons
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
└── util-guava
├── build.gradle.kts
└── src
└── main
└── java
└── org
└── example
└── GuavaUtil.java
├── app // App contains only the core application logic
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Main.java
├── settings.gradle
├── util // Util contains only the core utility logic
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Util.java
├── util-commons // One particular implementation of util, using Apache Commons
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
└── util-guava // Another implementation of util, using Guava
├── build.gradle
└── src
└── main
└── java
└── org
└── example
└── GuavaUtil.java
include("app") (1)
include("util")
include("util-commons")
include("util-guava")
include("app") (1)
include("util")
include("util-commons")
include("util-guava")
// This is the build.gradle file for the app module
plugins {
application (2)
}
dependencies { (3)
implementation(project(":util-guava"))
implementation(project(":util-commons"))
}
application {
mainClass = "org.example.Main"
}
// This is the build.gradle file for the app module
plugins {
id "application" (2)
}
dependencies { (3)
implementation project(":util-guava")
implementation project(":util-commons")
}
application {
mainClass = "org.example.Main"
}
// This is the build.gradle file for the util-commons module
plugins { (4)
`java-library`
}
dependencies { (5)
api(project(":util"))
implementation("commons-lang:commons-lang:2.6")
}
// This is the build.gradle file for the util-commons module
plugins { (4)
id "java-library"
}
dependencies { (5)
api project(":util")
implementation "commons-lang:commons-lang:2.6"
}
// This is the build.gradle file for the util-guava module
plugins {
`java-library`
}
dependencies {
api(project(":util"))
implementation("com.google.guava:guava:31.1-jre")
}
// This is the build.gradle file for the util-guava module
plugins {
id "java-library"
}
dependencies {
api project(":util")
implementation "com.google.guava:guava:31.1-jre"
}
1 | 此构建逻辑上将源文件拆分为多个项目。每个项目都可以独立构建,并且 Gradle 可以并行运行任务。这意味着如果您更改其中一个项目中的单个源文件,Gradle 只需重新编译并重新构建该项目,而无需重新构建整个构建。 |
2 | application 插件只应用于 app 项目,这是唯一需要它的项目。 |
3 | 每个项目只添加它需要的依赖项。这意味着每个子项目的类路径要小得多,这可以导致更快的构建时间并减少混乱。 |
4 | 每个项目只添加它需要的特定插件。 |
5 | 每个项目只添加它需要的依赖项。项目可以有效地使用API 与实现分离。 |
不要将源文件放在根项目中
不要将源文件放在您的根项目中;相反,将它们放在一个单独的项目中。
解释
根项目是 Gradle 中一个特殊的 Project,它作为构建的入口点。
它是配置一些全局应用于整个构建的设置和约定的地方,这些设置和约定不是通过 Settings 配置的。例如,您可以在此处声明(但不应用)插件,以确保所有项目都一致地使用相同的插件版本,并定义构建中所有项目共享的其他配置。
请注意不要在根项目中不必要地应用插件——许多插件只影响源代码,因此应该只应用于包含源代码的项目。 |
根项目不应用于放置源文件,它们应该位于一个单独的 Gradle 项目中。
从一开始就以这种方式设置您的构建,也将使您在将来随着构建的增长而更容易添加新项目。
示例
不要这样做
├── build.gradle.kts // Applies the `java-library` plugin to the root project
├── settings.gradle.kts
└── src // This directory shouldn't exist
└── main
└── java
└── org
└── example
└── MyClass1.java
├── build.gradle // Applies the `java-library` plugin to the root project
├── settings.gradle
└── src // This directory shouldn't exist
└── main
└── java
└── org
└── example
└── MyClass1.java
plugins { (1)
`java-library`
}
plugins {
id 'java-library' (1)
}
1 | java-library 插件应用于根项目,因为 Java 源文件位于根项目中。 |
取而代之这样做
├── core
│ ├── build.gradle.kts // Applies the `java-library` plugin to only the `core` project
│ └── src // Source lives in a "core" (sub)project
│ └── main
│ └── java
│ └── org
│ └── example
│ └── MyClass1.java
└── settings.gradle.kts
├── core
│ ├── build.gradle // Applies the `java-library` plugin to only the `core` project
│ └── src // Source lives in a "core" (sub)project
│ └── main
│ └── java
│ └── org
│ └── example
│ └── MyClass1.java
└── settings.gradle
include("core") (1)
include("core") (1)
// This is the build.gradle.kts file for the core module
plugins { (2)
`java-library`
}
// This is the build.gradle file for the core module
plugins { (2)
id 'java-library'
}
1 | 根项目仅用于配置构建,告知 Gradle 有一个名为 core 的(子)项目。 |
2 | java-library 插件仅应用于 core 项目,该项目包含 Java 源文件。 |
在 gradle.properties
中设置构建标志
在 gradle.properties
文件中设置 Gradle 构建属性标志。
解释
与其使用命令行选项或环境变量,不如在根项目的 gradle.properties
文件中设置构建标志。
Gradle 附带了很长的Gradle 属性列表,这些属性的名称以 org.gradle
开头,可用于配置构建工具的行为。这些属性对构建性能有重大影响,因此了解它们的工作原理非常重要。
您不应该依赖于每次 Gradle 调用都通过命令行提供这些属性。通过命令行提供这些属性用于短期测试和调试目的,但它容易被遗忘或在不同环境中应用不一致。设置和共享这些属性的永久、惯用位置是位于根项目目录中的 gradle.properties
文件。此文件应添加到源代码管理中,以便在不同机器和开发人员之间共享这些属性。
您应该了解构建使用的属性的默认值,并避免将属性显式设置为这些默认值。Gradle 中属性默认值的任何更改都将遵循标准的弃用周期,并且用户将得到适当的通知。
当使用复合构建时,以这种方式设置的属性不会跨构建边界继承。 |
示例
不要这样做
├── build.gradle.kts
└── settings.gradle.kts
├── build.gradle
└── settings.gradle
tasks.register("first") {
doLast {
throw GradleException("First task failing as expected")
}
}
tasks.register("second") {
doLast {
logger.lifecycle("Second task succeeding as expected")
}
}
tasks.register("run") {
dependsOn("first", "second")
}
tasks.register("first") {
doLast {
throw new GradleException("First task failing as expected")
}
}
tasks.register("second") {
doLast {
logger.lifecycle("Second task succeeding as expected")
}
}
tasks.register("run") {
dependsOn("first", "second")
}
此构建使用 gradle run -Dorg.gradle.continue=true
运行,因此 first
任务的失败不会阻止 second
任务执行。
这依赖于运行构建的人记住设置此属性,这容易出错,并且在不同机器和环境之间不可移植。
取而代之这样做
├── build.gradle.kts
└── gradle.properties
└── settings.gradle.kts
├── build.gradle
└── gradle.properties
└── settings.gradle
org.gradle.continue=true
此构建在 gradle.properties
文件中设置了 org.gradle.continue
属性。
现在它可以使用 gradle run
独立执行,并且 continue 属性将始终在所有环境中自动设置。
倾向于使用 build-logic
复合构建来处理构建逻辑
您应该设置一个复合构建(通常称为“包含的构建”)来存放您的构建逻辑——包括任何自定义插件、约定插件和其他构建特定的自定义项。
解释
构建逻辑的首选位置是包含的构建(通常命名为 build-logic
),而不是 buildSrc
。
自动可用的 buildSrc
非常适合快速原型开发,但它有一些微妙的缺点:
-
这两种方法在类加载器行为上存在差异,这可能会令人惊讶;包含的构建被视为外部依赖项,这是一种更简单的心智模型。依赖项解析在
buildSrc
中表现得微妙不同。 -
当包含的构建中的文件被修改时,构建中可能存在更少的任务失效,从而导致更快的构建。
buildSrc
中的任何更改都会导致整个构建过期,而包含的构建的子项目中的更改只会导致使用该特定子项目产品的构建中的项目过期。 -
包含的构建是完整的 Gradle 构建,可以作为独立项目独立打开、处理和构建。发布其产品(包括插件)以与其他项目共享非常简单。
-
buildSrc
项目会自动应用java
插件,这可能是不必要的。
此建议的一个重要注意事项是创建 Settings
插件时。在 build-logic
项目中定义这些插件需要将其包含在主构建的 settings.gradle(.kts)
文件的 pluginManagement
块中,以便这些插件足够早地提供给构建,以便应用于 Settings
实例。这是可能的,但会降低构建缓存能力,可能会影响性能。更好的解决方案是使用一个单独的、最小的、包含的构建(例如 build-logic-settings
)来仅包含 Settings
插件。
使用 buildSrc
的另一个潜在原因是,如果您的包含的 build-logic
中有大量的子项目。将不同的 build-logic
插件集应用于您的包含构建中的子项目将导致为每个项目使用不同的类路径。这可能会对性能产生影响,并使您的构建更难理解。使用不同的插件组合可能会导致诸如 构建服务之类的功能以难以诊断的方式中断。
理想情况下,使用 buildSrc
和包含的构建之间没有区别,因为 buildSrc
旨在表现得像一个隐式可用的包含构建。然而,由于历史原因,这些细微的差异仍然存在。随着这种情况的变化,此建议将来可能会进行修订。目前,这些差异可能会导致混淆。
由于设置复合构建只需要最少的额外配置,因此在大多数情况下,我们建议使用它而不是 buildSrc
。
示例
不要这样做
├── build.gradle.kts
├── buildSrc
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ ├── MyPlugin.java
│ └── MyTask.java
└── settings.gradle.kts
├── build.gradle
├── buildSrc
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ ├── MyPlugin.java
│ └── MyTask.java
└── settings.gradle
// This file is located in /buildSrc
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
// This file is located in /buildSrc
plugins {
id "java-gradle-plugin"
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
设置插件构建:使用任何一种方法都是相同的。
rootProject.name = "favor-composite-builds"
rootProject.name = "favor-composite-builds"
buildSrc
产品自动可用:此方法无需额外配置。
取而代之这样做
├── build-logic
│ ├── plugin
│ │ ├── build.gradle.kts
│ │ └── src
│ │ └── main
│ │ └── java
│ │ └── org
│ │ └── example
│ │ ├── MyPlugin.java
│ │ └── MyTask.java
│ └── settings.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
├── build-logic
│ ├── plugin
│ │ ├── build.gradle
│ │ └── src
│ │ └── main
│ │ └── java
│ │ └── org
│ │ └── example
│ │ ├── MyPlugin.java
│ │ └── MyTask.java
│ └── settings.gradle
├── build.gradle
└── settings.gradle
// This file is located in /build-logic/plugin
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
// This file is located in /build-logic/plugin
plugins {
id "java-gradle-plugin"
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
设置插件构建:使用任何一种方法都是相同的。
// This file is located in the root project
includeBuild("build-logic") (1)
rootProject.name = "favor-composite-builds"
// This file is located in the root project
includeBuild("build-logic") (1)
rootProject.name = "favor-composite-builds"
// This file is located in /build-logic
rootProject.name = "build-logic"
include("plugin") (2)
// This file is located in /build-logic
rootProject.name = "build-logic"
include("plugin") (2)
1 | 复合构建必须明确包含:使用 includeBuild 方法定位和包含一个构建,以使用其产品。 |
2 | 将您的包含构建组织成子项目:这允许主构建只依赖于包含构建的必要部分。 |