自动对齐依赖项与平台和 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 之间的冲突解析为更高的版本。jackson-databind 然而,通过另一个依赖项 (keycloak-core) 添加,保持在 2.8.9,因为缺少应将其与 jackson-core 对齐(即一起升级)的信息。

BOM 很棒,但我们(还)不够使用它们 #

在此场景中我们面临的情况是,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。

Maven 构建导入 jackson-bom

<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-bomjackson-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 允许您编写组件元数据规则,以在 Gradle 处理已发布的 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 用户手册中关于依赖管理的部分。

讨论