自动对齐平台和 Gradle 模块元数据中的依赖项
引言
这篇博文已更新,以反映 Jackson 从 2.12.0 版本开始,按照本文中的建议发布了 Gradle 模块元数据。
在关于 Gradle 6 依赖项管理的上一篇博文中,我们看到不断增长的构建可能会迅速陷入依赖项地狱。当意外结果出现在依赖项图的底部并向上传播时,尤其难以分析。不幸的是,如果依赖项图底部的库作者能够通过库的元数据表达有关这些库版本控制的所有知识,就可以避免其中一些问题。
这类库的一个典型例子是广泛使用的 JVM 工具库 Jackson。如果库的多个组件是依赖项图的一部分,那么版本对齐可能是一个问题。
Jackson 库仅包含三个核心模块:jackson-annotations
、jackson-core
和 jackson-databind
。即便如此,也极易出现 jackson-core
的版本高于 jackson-databind
的情况。以下示例说明了这种情况,其中两个模块最终具有不同的版本:jackson-core:2.9.2
和 jackson-databind:2.8.9
。
您可以在此构建扫描中探索此示例,其中一个传递性依赖项 (tika-parsers) 将 jackson-core
升级到 2.9.2
,因为 Gradle 将 2.8.9
和 2.9.2
之间的冲突解析为更高的版本。然而,通过另一个依赖项 (keycloak-core) 添加的 jackson-databind
仍保留在 2.8.9
版本,因为缺少关于其应与 jackson-core
对齐(即一起升级)的信息。
BOMs 虽好,但我们(使用得)不够 #
在这种情况下,我们面临的局面是 Jackson 模块的版本应该对齐,但由于 pom 元数据格式的限制,这些信息没有发布。Jackson 发布的是 BOM(物料清单),一个仅包含依赖项版本信息的 pom.xml
文件。BOM 包含一些对齐信息,正如从 jackson-bom-2.9.2.pom 摘录的这一部分所示:
<dependencyManagement>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.2</version>
</dependency>
...
</dependencyManagement>
...
但是,为了使用此对齐信息,Maven 和 Gradle 用户都需要在其自己的构建中**显式**依赖 BOM。
导入 jackson-bom 的 Maven 构建
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.8.9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Gradle 5.x 构建,具有对 jackson-bom 的平台依赖项
dependencies {
// depend on a platform and enforce all version entries (Maven semantics)
implementation(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.8.9"))
// depend on a platform and do dependency conflict resolution with all entries
implementation(platform("com.fasterxml.jackson:jackson-bom:2.8.9"))
}
这种方法有几个问题:知道 Jackson BOM 的存在、决定使用哪个版本的 Jackson BOM,以及在构建演进时更新该版本。
如果我们使用 Maven,BOM 中提供的版本将被强制执行。这意味着任何依赖项对更高版本的请求都将被默默忽略。在上例中,如果我们选择了 2.8.9
BOM,jackson-core
将被降级到 2.8.9
。这可能会导致问题,因为 tika-parsers
需要 jackson-core:2.9.2
,并且在降级时可能会中断。因此,构建作者必须仔细选择 BOM 的版本,并在依赖项更改时重新考虑该选择。如果没有工具支持,如果使用多个 BOM,这可能会变得难以管理。
缺失的环节:BOM 上的平台依赖项 #
Gradle 5.0 引入了声明对 BOM 的平台依赖项的功能。在这种情况下,版本不会被默默强制执行,而是 BOM 中的条目会参与冲突解决。然而,在示例中,这意味着我们回到了最初的问题:我们在构建脚本中选择了 2.8.9
BOM,它推荐 jackson-core
的版本为 2.8.9
,但 jackson-core
被传递性依赖项升级到了 2.9.2
。
我们缺少的是平台(
jackson-bom
)自动升级到选定的最高版本(2.9.2
)。这无法在 pom 元数据中表达,但在 Gradle 6 中可以使用 Gradle 模块元数据来表达。
Gradle 模块元数据答案 #
通过 Gradle 模块元数据,Jackson 团队现在为 jackson-core
的每个版本发布平台依赖项,并附带有关它所属平台版本(jackson-bom
)的信息。例如,如果 jackson-core
使用 Gradle 构建,此平台依赖项可以添加到其构建脚本中:
dependencies {
// I belong to the 'jackson-bom' platform with the same version
api(platform("com.fasterxml.jackson:jackson-bom:${project.version}"))
...
}
如果使用 Gradle 6,Gradle 模块元数据默认发布,因此包含平台依赖项。类似地,可以将平台依赖项添加到 jackson-databind
和 jackson-annotations
。有了这些额外信息,本博文开头的依赖项图将如下所示:
更新后的图(您也可以在此构建扫描中进行探索)显示了两点:首先,有一个新节点 jackson-bom
。指向 jackson-bom
的边来自 Jackson 模块。这些边源自发布的平台依赖项,因此 jackson-bom
会被自动添加,而无需在构建中显式依赖它。其次,每个模块版本都会引入与其版本匹配的 jackson-bom
——jackson-databind:2.8.9
引入 jackson-bom:2.8.9
;jackson-core:2.9.2
引入 jackson-bom:2.9.2
。Gradle 随后将 jackson-bom 的版本冲突解析为更高的版本,而这又会添加图中所有更高模块版本的依赖项约束,最终导致所有组件对齐到最高版本。
使用 Gradle 模块元数据,可以发布平台依赖项,以自动将平台(
jackson-bom
)更新到选定的最高版本。
为现有的 Jackson 元数据添加缺失的部分 #
从 Jackson 2.12.0 版本开始,Jackson 发布了本博文中提出的 Gradle 模块元数据。对于对齐到 2.12.0 或更高版本,以下内容是必需的。
由于 Gradle 模块元数据是一种新格式,其普及还需要时间。为了弥合这一差距,Gradle 6 允许您编写组件元数据规则,在处理发布的 pom 元数据时,用缺失的信息来丰富它。对于 Jackson 示例,您可以在构建脚本中添加以下内容:
open class JacksonAlignmentRule: ComponentMetadataRule {
@Inject open fun getObjects(): ObjectFactory = throw UnsupportedOperationException()
override fun execute(ctx: ComponentMetadataContext) {
if (ctx.details.id.group == "com.fasterxml.jackson.core") {
ctx.details.allVariants {
withDependencies {
add("com.fasterxml.jackson:jackson-bom:${ctx.details.id.version}") {
attributes {
attribute(Category.CATEGORY_ATTRIBUTE,
getObjects().named(Category.REGULAR_PLATFORM))
}
}
}
}
}
}
}
dependencies {
// apply the JacksonAlignmentRule rule defined above
components.all<JacksonAlignmentRule>()
}
发布具有对齐功能的库 #
使用本文所述方法的库的示例包括 JUnit 5 和 Jackson。如果您是库作者,也可以这样做!
如果您使用 **Gradle 6+**,您可以通过像 JUnit 一样的方式来为您的库的模块添加对齐:
- 拥有一个 java-platform 项目
- 定义到该项目的平台依赖项
- 使用 Gradle 的 maven-publish 插件 来发布带有平台依赖项的元数据
如果您使用 **Maven**,您可以利用这个Maven 插件,通过像 Jackson 一样的方式为您的库的模块添加对齐:
- 拥有一个 BOM 项目
- 将上述插件配置为在父 POM 中使用您的 BOM 进行对齐
- 在所有模块的 POM 中激活该插件
结论 #
库作者可以使用 Gradle 模块元数据格式发布平台依赖项,以对齐其库中模块的版本。在使用此类库的构建中,Gradle 会自动执行对齐。如果您使用的库(尚未)发布此信息,您可以编写自己的规则来为库的元数据添加缺失的部分。
版本对齐只是借助 Gradle 模块元数据解决的众多用例之一。在下一篇关于 Gradle 6 依赖项管理的博文中,我们将探讨如何检测和解决不同模块以及模块变体之间的依赖项冲突。如果您想立即深入了解,请查看 Gradle 用户手册中关于依赖项管理的部分。