自动对齐平台和 Gradle 模块元数据中的依赖项

目录

引言

这篇博文已更新,以反映 Jackson 从 2.12.0 版本开始,按照本文中的建议发布了 Gradle 模块元数据。

在关于 Gradle 6 依赖项管理的上一篇博文中,我们看到不断增长的构建可能会迅速陷入依赖项地狱。当意外结果出现在依赖项图的底部并向上传播时,尤其难以分析。不幸的是,如果依赖项图底部的库作者能够通过库的元数据表达有关这些库版本控制的所有知识,就可以避免其中一些问题。

这类库的一个典型例子是广泛使用的 JVM 工具库 Jackson。如果库的多个组件是依赖项图的一部分,那么版本对齐可能是一个问题。

Jackson 库仅包含三个核心模块:jackson-annotationsjackson-corejackson-databind。即便如此,也极易出现 jackson-core 的版本高于 jackson-databind 的情况。以下示例说明了这种情况,其中两个模块最终具有不同的版本:jackson-core:2.9.2jackson-databind:2.8.9

Transitive dependencies resolve to jackson-core:2.9.2 but jackson-databind:2.2.2

您可以在此构建扫描中探索此示例,其中一个传递性依赖项 (tika-parsers) 将 jackson-core 升级到 2.9.2,因为 Gradle 将 2.8.92.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,这可能会变得难以管理。

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-databindjackson-annotations。有了这些额外信息,本博文开头的依赖项图将如下所示:

Transitive dependencies resolve to jackson-core:2.9.2 but jackson-databind:2.8.9

更新后的图(您也可以在此构建扫描中进行探索)显示了两点:首先,有一个新节点 jackson-bom。指向 jackson-bom 的边来自 Jackson 模块。这些边源自发布的平台依赖项,因此 jackson-bom 会被自动添加,而无需在构建中显式依赖它。其次,每个模块版本都会引入与其版本匹配的 jackson-bom——jackson-databind:2.8.9 引入 jackson-bom:2.8.9jackson-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 一样的方式来为您的库的模块添加对齐:

如果您使用 **Maven**,您可以利用这个Maven 插件,通过像 Jackson 一样的方式为您的库的模块添加对齐:

结论 #

库作者可以使用 Gradle 模块元数据格式发布平台依赖项,以对齐其库中模块的版本。在使用此类库的构建中,Gradle 会自动执行对齐。如果您使用的库(尚未)发布此信息,您可以编写自己的规则来为库的元数据添加缺失的部分。

版本对齐只是借助 Gradle 模块元数据解决的众多用例之一。在下一篇关于 Gradle 6 依赖项管理的博文中,我们将探讨如何检测和解决不同模块以及模块变体之间的依赖项冲突。如果您想立即深入了解,请查看 Gradle 用户手册中关于依赖项管理的部分。

讨论