(译) Organizing Gradle Projects

翻译自《Organizing Gradle Projects》,介绍了Gradle的若干最佳实践。

每个软件工程的源代码和构建逻辑应该以有意义的方式来组织。本文介绍了若干最佳实践来实现可读性好、易维护的项目结构。同时也介绍了若干常见问题以及解决方法。

单独存放不同语言的源码

Gradle的编程语言插件建立了发现和编译源码的约定。比如,使用了Java插件的工程会自动编译src/main/java目录下的代码。其他语言的插件也遵循类似的模式。目录路径的最后一部分通常表明了对应的语言。

某些编译器支持编译同一个源码目录中的不同语言写的代码。Groovy编译器可以处理混放在src/main/groovy目录下的Java和Groovy源码。Gradle建议按不同的语言来存放代码,以达到更好的构建性能。此外,the user and build can make stronger assumptions。

The following source tree contains Java and Kotlin source files. Java source files live in src/main/java, whereas Kotlin source files live in src/main/kotlin.

1
2
3
4
5
6
7
8
.
├── build.gradle
└── src
└── main
├── java
│ └── HelloWorld.java
└── kotlin
└── Utils.kt

不同类型的测试代码分开存放

Have a look at the sample sample that demonstrates how a separate integration tests configuration can be added to a Java-based project.

一个项目中有不同类型的测试代码,这个很常见。比如单元测试,集成测试,功能测试或冒烟测试。每种类型的测试代码应当放在专门的源码目录中(可选的)。分开存放对可维护性有好处,也能让你更关注特定类型的测试。

这个sample展示了如何在一个Java项目中添加不同的测试配置。

尽可能使用标准约定

Gradle核心插件遵守软件工程范例convention over configuration。插件逻辑在特定上下文中为用户提供有意义的缺省值以及约定。以Java plugin插件为例:

  • src/main/java作为缺省的源码位置
  • 编译后的产物放在build目录

严格遵守缺省约定的话,加入项目的新开发者能马上知道如何开始工作。当然,约定也支持重新配置,只不过构建脚本的用户和作者更难维护构建逻辑和构建输出。应当尽可能尝试严格遵守缺省约定,除非你需要适配遗留项目结构。参考各相关插件的手册来学习其缺省约定。

定义settings文件

每次构建时,Gradle会尝试找到settings.gradle(Groovy DSL)或者settings.gradle.kts(Kotlin DSL)文件。基于这个目的,运行时会沿着目录树结构往上一直搜索到根目录。一旦找到settings文件后立即停止查找。

一定要在根目录中添加一个settings.gradle以避免性能问题。这个建议对单工程构建和多工程构建都有效。该文件可以为空,也可以定义工程名。

一个典型的带settings文件的Gradle工程结构如下:

1
2
3
4
5
6
.
├── settings.gradle
├── subproject-one
│ └── build.gradle
└── subproject-two
└── build.gradle

使用 buildSrc 抽象逻辑

复杂的构建逻辑通常适合封装成自定义任务或者二进制插件。自定义任务和二进制插件不应放到项目的构建脚本中。如果这些逻辑不需要在多个独立的项目中共享,那么可以使用buildSrc

buildSrc目录视为一个included build。一旦 Gradle 发现这个目录,它会自动编译和测试其中的代码并将其添加到构建脚本的classpath。对于多项目构建(multi-project builds),只能有一个buildSrc目录,这个目录位于项目根目录。应优先使用buildSrc而不是插件(script plugins),因为前者代码更容易维护、重构和测试。

buildSrc使用跟Java和Groovy项目相同的代码结构(source code conventions)。它可以直接访问Gradle API。buildSrc目录下的build.gradle脚本中可以添加其他依赖。

Example 1. Custom buildSrc build script

1
2
3
4
5
6
7
repositories {
mavenCentral()
}

dependencies {
testImplementation 'junit:junit:4.13'
}

一个包含buildSrc的工程,其项目结构如下。buildSrc下的代码使用跟应用代码类似的包。如果有额外的配置需要,buildSrc目录可以放一个可选的构建脚本(比如,使用插件或声明依赖)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── buildSrc
│ ├── build.gradle
│ └── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── enterprise
│ │ ├── Deploy.java
│ │ └── DeploymentPlugin.java
│ └── test
│ └── java
│ └── com
│ └── enterprise
│ └── DeploymentPluginTest.java
├── settings.gradle
├── subprojecto-one
│ └── build.gradle.kts
└── subproject-two
└── build.gradle.kts

注意buildSrc中的变更会引起整个项目变成out-of-date状态。因此,当进行小的增量变更时,–no-rebuild command-line option可加快编译速度。记住buildSrc修改完成后要定期进行全量构建。

在 gradle.properties 文件中定义属性

Gradle中可以在构建脚本中定义属性,也可以在gradle.properties定义属性,或者在命令行参数中定义属性。

命令行参数中定义属性在ad-hoc场景下很常见。比如,你想传特定的属性值来控制某次构建的运行时行为。构建脚本中的属性很容易带来维护性问题。gradle.properties用于将属性跟构建脚本分离。它适用于保存控制构建环境的属性。

典型的工程中将gradle.properties文件放在根目录。另外,如果你想将其应用于所有构建任务的话,也可将该文件放在GRADLE_USER_HOME目录。

1
2
3
4
5
6
7
.
├── gradle.properties
└── settings.gradle
├── subproject-a
│ └── build.gradle
└── subproject-b
└── build.gradle

避免覆盖任务输出

Task 应当定义输入和输出以利用 incremental build functionality 提升性能。当声明 task 的输出时,应当确认输出目录是独有的。

混合或覆盖不同task的输出,会导致 up-to-date 检查过程复杂化,从而拖慢构建过程。另一方面,文件系统的变化可能让Gradle的构建缓存(build cache)难以识别和缓存应当缓存的task。

发布自定义的Gradle

企业常常想通过定义通用约定或规则来为所有的项目做标准化构建。你可以借助初始化脚本来实现这个功能。Initialization scripts可以非常容易地为同一台机器上的各个项目应同一构建逻辑。比如,使用一个私有的repo以及其凭证。

这种方式有期缺点。首先,你必须跟公司的所有开发人员沟通标准化设置过程。另外,统一升级初始化脚本逻辑也是个挑战。

发布自定义Gradle是个可行的解决方案。自定义的Gradle包含标准的Gradle发布版本,以及一个或多个自定义 initialization script。初始化脚本跟发布版本打包在一起,并且可应用到每次的构建上。开发者只需要将他们的 wraper 文件指向自定义Gradle的url。

创建自定义Gradle发布版本的典型步骤如下:

  1. 实现下载和重新打包Gradle发布版本的逻辑
  2. 定义一个或多个初始化脚本
  3. 将初始化脚本跟Gradle发布包打包到一起
  4. 将Gradle发布包上传到HTTP服务器
  5. 将所有项目的wrapper文件指向自定义Gradle发布版本的url

参考