自动将依赖项与平台和 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 对齐(即一起升级)的信息。

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。

导入 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>

使用 jackson-bom 平台依赖项的 Gradle 5.x 构建

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

讨论