【Gradle多项目系列0】多项目结构标准路径构建测试
目录
Gradle支持多项目构建。
虽然一些小型项目和单体应用程序可能包含一个单独的构建文件和源代码树,但通常更常见的是将一个项目拆分为更小、相互依赖的模块。关键词“相互依赖”非常重要,因为您通常希望通过一个单一的构建将许多模块链接在一起。
Gradle通过多项目构建来支持这种情况。有时也称为多模块项目。
多项目构建由一个根项目和一个或多个子项目组成。
多项目结构
以下是一个包含两个子项目的多项目构建结构示例:
├── .gradle
│ └── ⋮
├── gradle
│ ├── libs.version.toml
│ └── wrapper
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── sub-project-1
│ └── build.gradle.kts
└── sub-project-2
└── build.gradle.kts
settings.gradle.kts
文件应包含所有子项目。- 每个子项目应该有自己的
build.gradle.kts
文件。
多项目标准
Gradle社区有两种多项目构建结构的标准:
-
使用
buildSrc
的多项目构建:在Gradle项目根目录下创建一个名为buildSrc
的子项目,其中包含所有构建逻辑。——请看下文组织Gradle项目部分的使用buildSrc来抽象命令式逻辑
-
复合构建:一个包含其他构建的构建,其构建逻辑位于Gradle项目根目录下的一个名为
build
的子目录中。——参考下文组合构建部分
请参阅如何使用 include
声明子项目之间的依赖关系以及如何使用 includeBuild
创建复合构建。
多项目路径
项目路径遵循以下模式:可选冒号开头表示根项目。
根项目 :
是路径中唯一通过名称指定的项目。
其他项目路径是由冒号分隔的项目名称序列,其中每个项目都是前一个项目的子项目:
:sub-project-1
您可以在运行 gradle projects
命令时看到项目路径的输出。
识别项目结构
您可以使用 gradle projects
命令来识别项目结构。
例如,下面是一个多项目构建结构的示例输出:
> gradle -q projects
------------------------------------------------------------
Root project 'multiproject'
------------------------------------------------------------
Root project 'multiproject'
+--- Project ':api'
+--- Project ':services'
| +--- Project ':services:shared'
| \--- Project ':services:webservice'
\--- Project ':shared'
要查看项目的任务列表,请运行 gradle <project-path>:tasks
。例如,尝试运行 gradle :api:tasks
。
多项目构建和测试
在多项目构建中,使用 build
任务通常用于编译、测试和检查单个项目。
如果要在多个项目中执行这些任务,可以使用 buildNeeded
和 buildDependents
任务。
例如,对于一个名为 api
的子项目,你可以运行以下命令来构建和测试该项目及其依赖的其他项目:
$ gradle :api:buildNeeded
这将构建和测试所有与 api
项目在 testRuntime
配置中有依赖关系的项目。
最后,你可以通过在根项目文件夹中运行 gradle build
命令来构建和测试所有项目。
请参阅“结构化构建”章节以获取更多信息。
组织Gradle项目
目录
- 将特定语言的源文件分开
- 为每种测试类型分开源文件
- 尽可能使用标准约定
- 始终定义settings文件
- 使用buildSrc来抽象命令式逻辑
- 在gradle.properties文件中声明属性
- 避免重叠的任务输出
- 使用自定义Gradle发行版标准化构建
源代码和构建逻辑的组织对于每个软件项目都非常重要。本页面列出了导致可读性高、易于维护的项目的最佳实践。以下部分还涉及常见问题以及如何避免这些问题。
将特定语言的源文件分开
Gradle的语言插件为发现和编译源代码建立了一套约定。例如,应用Java插件的项目将自动编译位于src/main/java目录中的代码。其他语言插件也遵循相同的模式。目录路径的最后一部分通常表示源文件的预期语言。
某些编译器能够在同一源目录中交叉编译多种语言。Groovy编译器可以处理将Java和Groovy源文件混合放置在src/main/groovy目录中的情况。Gradle建议您根据其语言将源文件放置在相应的目录中,这样构建效率更高,用户和构建脚本可以做出更强的假设。
以下示例展示了包含Java和Kotlin源文件的源代码结构。Java源文件位于src/main/java目录中,而Kotlin源文件位于src/main/kotlin目录中。
.
├── build.gradle
└── src
└── main
├── java
│ └── HelloWorld.java
└── kotlin
└── Utils.kt
为每种测试类型分开源文件
项目通常会定义和执行不同类型的测试,例如单元测试、集成测试、功能测试或冒烟测试。最理想的情况是,每种测试类型的测试源代码都应存储在专用的源代码目录中。分离的测试源代码对可维护性和关注点分离有积极影响,因为您可以独立运行每种测试类型。
以下示例演示了如何在基于Java的项目中添加一个单独的集成测试配置。
尽可能使用标准约定
所有Gradle核心插件都遵循“约定优于配置”的软件工程范例。插件逻辑在特定上下文中为用户提供了合理的默认值和标准约定。让我们以Java插件为例。
它将目录src/main/java定义为编译的默认源代码目录。
编译后的源代码及其他构件(如JAR文件)的输出目录是build。
遵循默认约定,新加入项目的开发人员立即知道如何进行操作。尽管可以重新配置这些约定,但这会使构建脚本的用户和作者更难管理构建逻辑及其结果。除非需要适应传统项目的布局,否则请尽可能遵循默认约定。有关插件的默认约定,请参阅相关插件的参考页面。
始终定义settings文件
Gradle在每次构建调用时尝试查找settings.gradle(Groovy DSL)或settings.gradle.kts(Kotlin DSL)文件。为此,运行时沿着目录树向上遍历直到根目录。一旦找到设置文件,算法就会停止搜索。
始终在构建的根目录中添加一个settings.gradle文件,以避免初始性能影响。该文件可以为空,也可以定义项目的期望名称。
多项目构建必须在多项目层次结构的根项目中拥有一个settings.gradle(.kts)文件。它是必需的,因为设置文件定义了哪些项目参与到多项目构建中。除了定义包含的项目,您可能需要在构建脚本类路径中添加库。
以下示例显示了标准Gradle项目布局:
.
├── settings.gradle
├── subproject-one
│ └── build.gradle
└── subproject-two
└── build.gradle
使用buildSrc来抽象命令式逻辑
复杂的构建逻辑通常适合作为自定义任务或二进制插件进行封装。自定义任务和插件实现不应存在于构建脚本中。使用buildSrc非常方便,因为该代码不需要在多个独立项目之间共享。
buildSrc目录被视为一个包含的构建。发现该目录后,Gradle会自动编译和测试此代码,并将其放入构建脚本的类路径中。对于多项目构建,只能有一个buildSrc目录,它必须位于根项目目录中。相比脚本插件**,建议使用buildSrc,因为它更容易维护、重构和测试代码。**
buildSrc使用适用于Java和Groovy项目的相同源代码约定。它还提供对Gradle API的直接访问。额外的依赖关系可以在buildSrc下的专用build.gradle中声明。
示例1. 自定义buildSrc构建脚本
buildSrc/build.gradle
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.13'
}
典型的项目包括buildSrc的布局如下。buildSrc下的任何代码都应使用与应用程序代码类似的包。如果需要额外的配置(例如应用插件或声明依赖项),buildSrc目录还可以托管构建脚本。
.
├── buildSrc
│ ├── build.gradle
│ └── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── enterprise
│ │ ├── Deploy.java
│ │ └── DeploymentPlugin.java
│ └── test
│ └── java
│ └── com
│ └── enterprise
│ └── DeploymentPluginTest.java
├── settings.gradle
├── subproject-one
│ └── build.gradle
└── subproject-two
└── build.gradle
对buildSrc进行更改会导致整个项目变为过时状态。因此,在进行小的增量更改时,–no-rebuild命令行选项通常很有帮助,以获得更快的反馈。不过,请记得定期运行完整的构建,或者至少在完成后运行完整的构建。
在gradle.properties文件中声明属性
在Gradle中,属性可以在构建脚本中、gradle.properties文件中或命令行参数中进行定义。
通常,会在命令行上声明属性以应对临时场景。例如,您可能想要传递特定的属性值以控制运行时行为,仅在此次构建调用中有效。构建脚本中的属性很容易导致维护问题,并使构建脚本逻辑变得复杂。gradle.properties文件有助于将属性与构建脚本分离,并且应该作为一种可行的选项进行探索。它是放置控制构建环境的属性的好地方。
典型的项目设置将gradle.properties文件放置在构建的根目录中。或者,如果希望应用于机器上的所有构建,则该文件也可以存在于GRADLE_USER_HOME目录中。
.
├── gradle.properties
└── settings.gradle
├── subproject-a
│ └── build.gradle
└── subproject-b
└── build.gradle
避免重叠的任务输出
任务应定义输入和输出,以获得增量构建功能的性能优势。在声明任务的输出时,请确保写入输出的目录在项目的所有任务中都是唯一的。
混合或覆盖不同任务产生的输出文件会破坏更新检查,从而导致构建速度变慢。反过来,这些文件系统更改可能会阻止Gradle的构建缓存正确识别和缓存本来可以进行缓存的任务。
使用自定义Gradle发行版标准化构建
企业通常希望通过定义共同的约定或规则为组织中的所有项目统一构建平台。您可以借助初始化脚本实现这一目标。初始化脚本使得在单个机器上应用构建逻辑到所有项目变得非常容易。例如,声明内部仓库及其凭据。
这种方法有一些缺点。首先,您将不得不在公司中向所有开发人员传达设置过程。此外,统一更新初始化脚本逻辑可能具有挑战性。
自定义Gradle发行版是解决此问题的实际方案。自定义Gradle发行版由标准Gradle发行版加上一个或多个自定义的初始化脚本组成。初始化脚本与发行版捆绑,并在每次运行构建时应用。开发人员只需将检入的Wrapper文件指向自定义Gradle发行版的URL。
自定义Gradle发行版还可以包含位于分发根目录中的gradle.properties文件,该文件提供了控制构建环境的一组全局属性。
创建自定义Gradle发行版的典型步骤如下:
- 实现下载和重新打包Gradle发行版的逻辑。
- 使用所需的逻辑定义一个或多个初始化脚本。
- 将初始化脚本与Gradle发行版捆绑在一起。
- 将Gradle发行版归档文件上传到HTTP服务器。
- 将所有项目的Wrapper文件更改为指向自定义Gradle发行版的URL。
示例2. 构建自定义Gradle发行版
build.gradle
plugins {
id 'base'
}
// This is defined in buildSrc
import org.gradle.distribution.DownloadGradle
version = '0.1'
tasks.register('downloadGradle', DownloadGradle) {
description = 'Downloads the Gradle distribution with a given version.'
gradleVersion = '4.6'
}
tasks.register('createCustomGradleDistribution', Zip) {
description = 'Builds custom Gradle distribution and bundles initialization scripts.'
dependsOn downloadGradle
def projectVersion = project.version
archiveFileName = downloadGradle.gradleVersion.map { gradleVersion ->
"mycompany-gradle-${gradleVersion}-${projectVersion}-bin.zip"
}
from zipTree(downloadGradle.destinationFile)
from('src/init.d') {
into "${downloadGradle.distributionNameBase.get()}/init.d"
}
}
组合构建
什么是组合构建?
组合构建简单地说就是包含其他构建的构建。在许多方面,组合构建与Gradle多项目构建类似,只不过它包含完整的构建而不是单个项目。
组合构建使您能够:
- 将通常独立开发的构建组合在一起,例如在应用程序中尝试修复库中的错误时
- 将大型多项目构建分解为更小、更独立的部分,可以独立或一起工作
在组合中包含的构建与其他构建通过依赖替换进行交互。如果组合中的任何构建具有可以满足包含构建的依赖项,那么该依赖项将被替换为对包含构建的项目依赖项。由于依赖替换的依赖关系,组合构建可能会强制配置在构建任务执行图中更早地解析配置。这可能会对整体构建性能产生负面影响,因为这些配置不会并行解析。
默认情况下,Gradle会尝试确定可以由包含构建替换的依赖关系。但是,如果Gradle确定的默认依赖替换对于组合来说不正确或不够灵活,则可以显式声明这些替换。请参阅声明替换。
除了通过项目依赖项消耗输出之外,组合构建还可以直接声明对包含构建中的任务的任务依赖关系。包含构建是隔离的,无法声明对组合构建或其他包含构建中的任务的任务依赖关系。请参阅对包含构建中的任务的依赖关系。
定义组合构建
以下示例演示了将通常单独开发的2个Gradle构建如何组合成一个组合构建的各种方式。对于这些示例,my-utils多项目构建生成2个不同的Java库(number-utils和string-utils),而my-app构建使用这些库中的函数生成可执行文件。
my-app构建没有直接依赖于my-utils。相反,它声明了对由my-utils生成的库的二进制依赖。
示例1. my-app的依赖项
// my-app/app/build.gradle
plugins {
id 'application'
}
application {
mainClass = 'org.sample.myapp.Main'
}
dependencies {
implementation 'org.sample:number-utils:1.0'
implementation 'org.sample:string-utils:1.0'
}
通过–include-build定义组合构建
通过命令行参数–include-build将执行的构建转换为组合构建,并将包含构建的依赖项替换到执行的构建中。
$ gradle --include-build ../my-utils run
通过设置文件定义组合构建
可以使用Settings.includeBuild(java.lang.Object)在settings.gradle(或Kotlin中的settings.gradle.kts)文件中声明包含的构建,从而使上述安排持久化。settings文件可用于同时添加子项目和包含的构建。通过位置添加包含的构建。有关详细示例,请参阅下面的示例。
定义单独的组合构建
上述方法的一个缺点是需要修改现有构建,使其作为独立构建变得不太有用。避免这种情况的一种方法是定义一个单独的组合构建,其唯一目的是将其他独立构建组合在一起。
示例2. 声明一个单独的组合构建
// settings.gradle
rootProject.name = 'my-composite'
includeBuild 'my-app'
includeBuild 'my-utils'
在此场景中,执行的’main’构建是组合构建,并且它不定义任何有用的任务以供自己执行。为了在组合构建中执行’my-app’构建的’run’任务,组合构建必须定义一个委托任务。
示例3. 依赖于包含构建的任务
// build.gradle
tasks.register('run') {
dependsOn gradle.includedBuild('my-app').task(':app:run')
}
包含定义Gradle插件的构建
一个特殊情况的包含构建是定义Gradle插件的构建。这些构建应该使用settings文件的pluginManagement {}块中的includeBuild语句进行包含。使用此机制,包含构建还可以贡献一个settings插件,可以在settings文件本身中应用。
示例4. 包含一个插件构建
// settings.gradle
pluginManagement {
includeBuild '../url-verifier-plugin'
}
您也可以在pluginManagement之外使用includeBuild机制来包含插件构建。然而,这种方式不支持所有用例,并且在将来的Gradle版本中可能会弃用包含插件构建的方式。
包含构建的限制
当前实现的限制包括:
- 在被包含的构建中,根项目名称不能与其他被包含的构建相同。
- 在被包含的构建中,根项目名称不能与组合构建的顶级项目相同。
- 在被包含的构建中,根项目名称不能与组合构建的根项目名称相同。
当一个组合构建被包含到另一个组合构建中时,两个被包含的构建都有相同的父级。换句话说,嵌套的组合构建结构被展平。
与组合构建交互
一般来说,与组合构建交互与普通的多项目构建大致相同。可以执行任务、运行测试和将构建导入到IDE中。
执行任务
可以从命令行或IDE中以与普通多项目构建相同的方式执行包含构建中的任务。执行任务将导致执行任务依赖关系,以及那些从其他包含构建构建依赖项的任务。
您可以使用全限定路径调用包含构建中的任务,例如::included-build-name:project-name:taskName。项目和任务名称可以缩写。
$ ./gradlew :included-build:subproject-a:compileJava
> Task :included-build:subproject-a:compileJava
$ ./gradlew :i-b:sA:cJ
> Task :included-build:subproject-a:compileJava
要从命令行中排除任务,您还需要提供任务的完全限定路径。
包含构建任务会自动按顺序执行,以生成所需的依赖项文件,或者包含构建可以声明对包含构建中任务的依赖关系。
导入到IDE
组合构建最有用的功能之一是IDE集成。通过将idea或eclipse插件应用于构建,可以生成一个单一的IDEA或Eclipse项目,允许在组合中开发所有构建。
除了这些Gradle插件之外,IntelliJ IDEA的最新版本和Eclipse Buildship还支持直接导入组合构建。
导入组合构建允许将来自不同Gradle构建的源代码轻松地一起开发。对于每个包含的构建,将每个子项目作为IDEA模块或Eclipse项目包含在内。配置源依赖关系,提供跨构建导航和重构。
声明被包含构建替换的依赖项
默认情况下,Gradle会根据配置来确定哪些依赖项可以由被包含构建替换。此算法非常简单:Gradle将检查被包含构建中项目的组和名称,并将任何与 p r o j e c t . g r o u p : {project.group}: project.group:{project.name}匹配的外部依赖项替换为对被包含构建的项目依赖项。由于依赖替换的依赖关系,组合构建可能会强制在构建任务执行图中更早解析配置。这可能会对整体构建性能产生负面影响,因为这些配置不会并行解析。
默认情况下,不会为主构建注册替换。要使主构建中的(子)项目可以通过 p r o j e c t . g r o u p : {project.group}: project.group:{project.name}访问,请告诉Gradle将主构建视为一个被包含构建,并将其自行包含:includeBuild(“.”)。
有些情况下,默认由Gradle确定的依赖替换不足够灵活或者不正确适用于特定的组合。对于这些情况,可以显式声明包含构建的替换。以一个未声明group属性的单项目构建“anonymous-library”为例:
示例5. 未声明group属性的构建
// build.gradle
plugins {
id 'java'
}
当将此构建包含在组合中时,它将尝试替换“undefined:anonymous-library”依赖模块("undefined"是project.group的默认值,"anonymous-library"是根项目名称)。显然,在组合构建中这不会很有用。为了在组合构建中使用未经修改的未发布库,组合构建可以显式声明它提供的替换项:
示例6. 声明被包含构建的替换项
// settings.gradle
includeBuild('anonymous-library') {
dependencySubstitution {
substitute module('org.sample:number-utils') using project(':')
}
}
通过此配置,"my-app"组合构建将使用"anonymous-library"根项目替换对org.sample:number-utils的任何依赖。
当前限制和组合构建的未来计划
当前实现的限制包括:
- 不支持包含构建的发布不与项目默认配置一致的情况。请参阅组合构建不起作用的情况。
- 并行运行多个组合构建时,如果超过一个包含相同的构建,则可能会发生冲突。Gradle不共享共享组合构建的项目锁,以防止并发执行。
参考链接
小军李:【Gradle jvm插件系列1】 Java Application插件权威详解
小军李:【Gradle jvm插件系列2】 Java Library插件用法示例权威详解
小军李:【Gradle jvm插件系列3】 Java platform平台插件权威详解
小军李:【Gradle jvm插件系列4】 scala插件权威详解
gradlew gradle常用命令选项用法适用场景用法示例词典详解(建议收藏)
小军李:【gradle多模块系列1】多项目构建和子项目的添加管理
小军李:【Gradle多模块系列2】在子项目之间声明依赖关系和共享构建逻辑示例详解