复合构建
复合构建是包含其他构建的构建。
复合构建类似于 Gradle 的多项目构建,不同之处在于,它包含的是整个 builds
,而不是 subprojects
。
复合构建布局
复合构建允许您
-
组合通常独立开发的构建,例如,在尝试修复应用程序使用的库中的错误时。
-
将一个大型多项目构建分解为更小、更独立的块,这些块可以根据需要独立或协同工作。
包含在复合构建中的构建称为内含构建。

内含构建不与复合构建或其他内含构建共享任何配置。每个内含构建都独立配置和执行。
以下示例演示了两个通常独立开发的 Gradle 构建如何组合成一个复合构建
my-composite
├── settings.gradle.kts
├── build.gradle.kts
├── my-app
│ ├── settings.gradle.kts
│ └── app
│ ├── build.gradle.kts
│ └── src/main/java/org/sample/my-app/Main.java
└── my-utils
├── settings.gradle.kts
├── number-utils
│ ├── build.gradle.kts
│ └── src/main/java/org/sample/numberutils/Numbers.java
└── string-utils
├── build.gradle.kts
└── src/main/java/org/sample/stringutils/Strings.java
my-composite
├── settings.gradle
├── build.gradle
├── my-app
│ ├── settings.gradle
│ └── app
│ ├── build.gradle
│ └── src/main/java/org/sample/my-app/Main.java
└── my-utils
├── settings.gradle
├── number-utils
│ ├── build.gradle
│ └── src/main/java/org/sample/numberutils/Numbers.java
└── string-utils
├── build.gradle
└── src/main/java/org/sample/stringutils/Strings.java
my-utils
多项目构建生成两个 Java 库:number-utils
和 string-utils
。my-app
构建使用这些库中的函数生成可执行文件。
my-app
构建不直接依赖于 my-utils
。相反,它声明了对 my-utils
生成的库的二进制依赖项
plugins {
id("application")
}
application {
mainClass = "org.sample.myapp.Main"
}
dependencies {
implementation("org.sample:number-utils:1.0")
implementation("org.sample:string-utils:1.0")
}
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
命令行参数将执行的构建转换为复合构建,并将内含构建的依赖项替换到执行的构建中。
例如,从 my-app
运行 ./gradlew run --include-build ../my-utils
的输出
$ ./gradlew --include-build ../my-utils run > Task :app:processResources NO-SOURCE > Task :my-utils:string-utils:compileJava > Task :my-utils:string-utils:processResources NO-SOURCE > Task :my-utils:string-utils:classes > Task :my-utils:string-utils:jar > Task :my-utils:number-utils:compileJava > Task :my-utils:number-utils:processResources NO-SOURCE > Task :my-utils:number-utils:classes > Task :my-utils:number-utils:jar > Task :app:compileJava > Task :app:classes > Task :app:run The answer is 42 BUILD SUCCESSFUL in 0s 6 actionable tasks: 6 executed
通过 Settings 文件定义复合构建
通过使用 Settings.includeBuild(java.lang.Object) 在 settings.gradle(.kts)
文件中声明内含构建,可以使上述安排持久化。
settings 文件可以同时用于添加子项目和内含构建。
内含构建按位置添加
includeBuild("my-utils")
includeBuild 'my-utils'
在该示例中,settings.gradle(.kts) 文件将本来独立的构建组合在一起
rootProject.name = "my-composite"
includeBuild("my-app")
includeBuild("my-utils")
rootProject.name = 'my-composite'
includeBuild 'my-app'
includeBuild 'my-utils'
要从 my-composite
执行 my-app
构建中的 run
任务,请运行 ./gradlew my-app:app:run
。
您可以选择在 my-composite
中定义一个依赖于 my-app:app:run
的 run
任务,这样您就可以执行 ./gradlew run
tasks.register("run") {
dependsOn(gradle.includedBuild("my-app").task(":app:run"))
}
tasks.register('run') {
dependsOn gradle.includedBuild('my-app').task(':app:run')
}
包含定义 Gradle 插件的构建
内含构建的特殊情况是定义 Gradle 插件的构建。
这些构建应使用 settings 文件中 pluginManagement {}
块内的 includeBuild
语句包含。
使用此机制,内含构建还可以提供一个设置插件,该插件可以在 settings 文件本身中应用
pluginManagement {
includeBuild("../url-verifier-plugin")
}
pluginManagement {
includeBuild '../url-verifier-plugin'
}
内含构建的限制
大多数构建都可以包含在复合构建中,包括其他复合构建。存在一些限制。
在常规构建中,Gradle 确保每个项目都具有唯一的项目路径。这使得项目可识别和可寻址而不会发生冲突。
在复合构建中,Gradle 为内含构建中的每个项目添加了额外的限定,以避免项目路径冲突。在复合构建中标识项目的完整路径称为构建树路径。它由内含构建的构建路径和项目的项目路径组成。
默认情况下,构建路径和项目路径是从磁盘上的目录名称和结构派生的。由于内含构建可以位于磁盘上的任何位置,因此它们的构建路径由包含目录的名称决定。这有时会导致冲突。
总而言之,内含构建必须满足以下要求
-
每个内含构建必须具有唯一的构建路径。
-
每个内含构建路径不得与主构建的任何项目路径冲突。
这些条件保证即使在复合构建中,每个项目也可以唯一识别。
如果发生冲突,解决冲突的方法是更改内含构建的构建名称
includeBuild("some-included-build") {
name = "other-name"
}
当一个复合构建包含在另一个复合构建中时,这两个构建具有相同的父级。换句话说,嵌套的复合构建结构是扁平化的。 |
与复合构建交互
与复合构建交互通常类似于常规的多项目构建。可以执行任务、运行测试,并且可以将构建导入到 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 集成。
导入复合构建允许轻松地一起开发来自独立 Gradle 构建的源代码。对于每个内含构建,每个子项目都作为 IntelliJ IDEA 模块或 Eclipse 项目包含。配置了源依赖项,提供跨构建导航和重构。
声明由内含构建替换的依赖项
默认情况下,Gradle 会配置每个内含构建以确定它可以提供的依赖项。执行此操作的算法很简单。Gradle 将检查内含构建中项目的组和名称,并将项目依赖项替换为任何匹配 ${project.group}:${project.name}
的外部依赖项。
默认情况下,不为主构建注册替换。 要使主构建的(子)项目可以通过 |
在某些情况下,Gradle 确定的默认替换不足或必须针对特定复合进行更正。对于这些情况,可以显式声明内含构建的替换。
例如,一个名为 anonymous-library
的单项目构建生成一个 Java 工具库,但未声明 group 属性的值
plugins {
java
}
plugins {
id 'java'
}
当此构建包含在复合构建中时,它将尝试替换依赖模块 undefined:anonymous-library
(undefined
是 project.group
的默认值,anonymous-library
是根项目名称)。显然,这在复合构建中没有用。
要在复合构建中使用未发布的库,您可以显式声明它提供的替换
includeBuild("anonymous-library") {
dependencySubstitution {
substitute(module("org.sample:number-utils")).using(project(":"))
}
}
includeBuild('anonymous-library') {
dependencySubstitution {
substitute module('org.sample:number-utils') using project(':')
}
}
通过此配置,my-app
复合构建将任何对 org.sample:number-utils
的依赖项替换为对 anonymous-library
根项目的依赖项。
停用配置的内含构建替换
如果您需要解析某个模块的已发布版本,而该模块也可以作为内含构建的一部分使用,则可以在解析的 Configuration 的 ResolutionStrategy 上停用内含构建替换规则。这是必需的,因为规则在构建中全局应用,并且 Gradle 默认在解析期间不考虑已发布版本。
例如,我们创建一个单独的 publishedRuntimeClasspath
配置,该配置将解析为也存在于本地构建之一中的模块的已发布版本。这是通过停用全局依赖项替换规则来完成的
configurations.create("publishedRuntimeClasspath") {
resolutionStrategy.useGlobalDependencySubstitutionRules = false
extendsFrom(configurations.runtimeClasspath.get())
isCanBeConsumed = false
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}
configurations.create('publishedRuntimeClasspath') {
resolutionStrategy.useGlobalDependencySubstitutionRules = false
extendsFrom(configurations.runtimeClasspath)
canBeConsumed = false
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
}
一个用例是比较已发布和本地构建的 JAR 文件。
需要声明内含构建替换的情况
许多构建无需声明替换即可自动作为内含构建运行。以下是需要声明替换的一些常见情况
-
当使用
archivesBaseName
属性设置已发布工件的名称时。 -
当发布除
default
之外的配置时。 -
当使用
MavenPom.addFilter()
发布与项目名称不匹配的工件时。 -
当使用
maven-publish
或ivy-publish
插件进行发布且发布坐标与${project.group}:${project.name}
不匹配时。
复合构建替换不起作用的情况
有些构建在包含在复合构建中时无法正常工作,即使明确声明了依赖项替换。此限制是因为替换的项目依赖项将始终指向目标项目的 default
配置。任何时候,如果为项目的默认配置指定的工件和依赖项与发布到存储库的不同,则复合构建可能会表现出不同的行为。
以下是一些已发布模块元数据可能与项目默认配置不同的情况
-
当发布除
default
之外的配置时。 -
当使用
maven-publish
或ivy-publish
插件时。 -
当作为发布的一部分调整
POM
或ivy.xml
文件时。
使用这些功能的构建在包含在复合构建中时会错误地运行。
依赖内含构建中的任务
虽然内含构建彼此隔离且不能声明直接依赖项,但复合构建可以声明对其内含构建的任务依赖项。内含构建通过 Gradle.getIncludedBuilds() 或 Gradle.includedBuild(java.lang.String) 访问,任务引用通过 IncludedBuild.task(java.lang.String) 方法获取。
使用这些 API,可以声明对特定内含构建中任务的依赖项
tasks.register("run") {
dependsOn(gradle.includedBuild("my-app").task(":app:run"))
}
tasks.register('run') {
dependsOn gradle.includedBuild('my-app').task(':app:run')
}
或者,您可以声明对某些或所有内含构建中具有特定路径的任务的依赖项
tasks.register("publishDeps") {
dependsOn(gradle.includedBuilds.map { it.task(":publishMavenPublicationToMavenRepository") })
}
tasks.register('publishDeps') {
dependsOn gradle.includedBuilds*.task(':publishMavenPublicationToMavenRepository')
}
复合构建的限制
当前实现的限制包括
-
不支持包含发布与项目默认配置不一致的构建。
请参阅复合构建不起作用的情况。 -
如果多个复合构建包含相同的构建,则并行运行时可能会冲突。
Gradle 不在 Gradle 调用之间共享共享复合构建的项目锁,以防止并发执行。