用平台和Gradle模块元数据自动调整依赖关系的使用方法

427 阅读5分钟

这篇博文已经更新,以反映Jackson开始使用这篇文章中的建议在2.12.0版本中发布Gradle模块元数据。

上一篇关于Gradle 6的依赖性管理的文章中,我们看到不断增长的构建会很快陷入依赖性地狱。 如果意外的结果是在依赖性图的底部引入的,并通过横向依赖关系向上传播,就会变得特别难以分析。不幸的是,如果构成依赖性图底部的库的作者有办法在库的元数据中表达所有关于所述库的版本知识,那么其中一些问题是可以避免的。

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

Jackson库只由三个核心模块组成。jackson-annotations jackson-core jackson-databind然而,很容易出现这样的情况,即为jackson-core 选择了比jackson-databind 更高的版本。这种情况在下面的例子中得到说明,这两个模块最终有不同的版本:jackson-core:2.9.2jackson-databind:2.8.9

image.png 在这个例子中,你可以以构建扫描的方式来探索,由于Gradle解决了2.8.92.9.2 之间的冲突,将jackson-core 升级到了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。

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,这将变得难以管理。

缺少的环节:BOMs的平台依赖性

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 Module Metadata。

Gradle Module Metadata的解决方案

有了Gradle Module Metadata,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 。有了这些额外的信息,这篇文章开头的依赖性图就会是这样的。

image.png

更新后的图,你也可以在这个构建扫描中探索,显示了两件事:第一,有一个新的节点,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 Module Metadata,正如本博文所提议的那样。以下内容不需要向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用户手册中关于依赖性管理的章节。