可选依赖项并非可选项

目录

引言

之前的博文中,我们演示了能力如何被优雅地用于解决类路径上存在多个日志框架的问题。在这篇文章中,我们将再次在不同的上下文中使用这个概念:可选依赖项

在 Gradle,我们经常说没有可选依赖项:只有当您使用特定功能时才需要的依赖项。让我们解释一下原因。

可选依赖项 #

直到最近,Gradle 都没有提供任何发布可选依赖项的方法,这让许多 Apache Maven™ 用户感到困惑。为了理解在什么情况下使用可选依赖项,让我们看一个真实的工程项目。Apache PDFBox 库在其 POM 文件中声明了以下可选依赖项

<dependencies>
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcmail-jdk15on</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

这些是对特定组件的 2 个依赖项,即 BouncyCastle 密码库。

现在让我们想象您的项目依赖于 PDFBox,无论是使用 Gradle

dependencies {
    implementation("org.apache.pdfbox:pdfbox:2.0.17")
}

还是使用 Apache Maven™

<dependencies>
   <dependency>
       <groupId>org.apache.pdfbox</groupId>
       <artifactId>pdfbox</artifactId>
       <version>2.0.17</version>
   </dependency>
</dependencies>

现在,如果您查看 MavenGradle 解析的依赖项,您会看到 Bouncycastle 库不存在。这是因为它被定义为可选依赖项。如今,可选依赖项的定义存在多个问题

  • 库作者知道为什么依赖项是可选的,但消费者不知道:您如何知道何时应该添加 bcprov-jdk15on
  • 可选依赖项混杂在一起:您如何知道何时添加 bcprov-jdk15on,以及是否也应该添加 bcmail-jdk15on
  • 在解析传递依赖项时,Maven 和 Gradle 都会忽略可选依赖项:这些信息纯粹是文档,可能有助于用户手动向构建中添加额外的依赖项。

在我们的示例中,假设您想生成签名 PDF。通过如上声明依赖项,您很快就会意识到您缺少依赖项。要弄清楚缺少哪些依赖项,您可以查看 PDFBox 的 POM 文件,并通过阅读来猜测您需要使用的 Bouncycastle 版本

换句话说,修复是一个有根据的猜测:因为您大概知道 Bouncycastle 与安全性有关,所以您认为如果添加依赖项,它就会工作。问题解决了吗?

从可选依赖项到功能 #

现实情况是,对 Bouncycastle 的依赖不是可选的:如果您想签名 PDF,则它是必需的。PDFBox 有一个隐含的功能是“签名”,并且只有当您使用该功能时,您才需要更多几个依赖项。如果构建工具允许库作者表达这一点,那不是很好吗?

这正是 Gradle 的 功能变体 的用途!

为了演示的目的,让我们假设 PDFBox 将使用 Gradle 而不是 Maven 来构建他们的项目。然后他们可以使用这个来声明一个功能

java {
    registerFeature('signing') {
        usingSourceSet(sourceSets.main)
    }
}

这声明了 PDFbox 具有一个名为 signing 的功能,并且该功能是“使用主源代码集”。用 Gradle 的术语来说,这意味着该功能使用与主库相同的源目录 (src/main/java),结合了库的主要源代码和使用 Bouncycastle 执行签名的源代码。Gradle 还允许在单独的源代码集 (src/myFeature/java) 中编写功能,以将功能的代码与主代码隔离,并将其发布在单独的 jar 中。

既然定义了签名功能,就可以声明特定于此功能的依赖项

dependencies {
    signingImplementation("org.bouncycastle:bcmail-jdk15on:1.64")
    signingImplementation("org.bouncycastle:bcprov-jdk15on:1.64")
}

就这样!但是,与在 Maven 中声明可选依赖项相比,有什么好处呢?

嗯,如果我们只看一下 Gradle 将生成的 POM 文件……

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcmail-jdk15on</artifactId>
  <version>1.64</version>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk15on</artifactId>
  <version>1.64</version>
  <optional>true</optional>
</dependency>

……它与 Maven 发布的 POM 文件完全相同!Gradle 提供了通过定义功能和特定于功能的依赖项来定义和发布可选依赖项的能力,Maven 用户可以像使用相应的 Maven POM 文件一样使用 Gradle 生成的这个 POM 文件:直接查看 POM 文件,并弄清楚要手动添加哪些依赖项。

但是,作为 Gradle 用户,好处要高得多,因为事情可以用功能而不是可选依赖项来表达。假设您需要 PDFBox 及其签名功能。那么您需要声明两个依赖项

dependencies {
    implementation("org.apache.pdfbox:pdfbox:2.0.17")
    implementation("org.apache.pdfbox:pdfbox:2.0.17") {
        capabilities {
           requireCapability("org.apache.pdfbox:pdfbox-signing")
        }
    }
}

我们这里有两个不同的依赖项声明

  • 第一个告诉 Gradle 我们需要 PDFBox。这是“主要依赖项”。
  • 第二个告诉 Gradle 我们也想要 PDFBox 的 signing 功能 (pdfbox-signing)。

第二个依赖项“指向”不同的变体,因为 Gradle 按照惯例,创建了一个与 PDFBox 声明的功能名称相对应的能力

最大的优势是用户不必弄清楚他们需要哪些依赖项才能使签名工作:他们将传递性地获得它们

同样有趣的是,因为我们将对 Bouncycastle 的依赖项定义为implementation 依赖项,所以消费者在编译时不需要它,仅在运行时需要。这就是为什么该依赖项没有出现在编译类路径上的原因!

是什么使之成为可能? #

实现这一点的关键再次是 Gradle 模块元数据。这个文件,就像 pom.xml 文件一样,包含依赖项解析用于查找传递依赖项的元数据。

在这种情况下,为我们的“伪造” PDFBox 库生成的 Gradle 模块元数据文件包含以下内容

{
  "name": "signingRuntimeElements",
  "attributes": {
    "..."
  },
  "dependencies": [
    {
      "group": "org.bouncycastle",
      "module": "bcmail-jdk15on",
      "version": {
        "requires": "1.64"
      }
    },
    {
      "group": "org.bouncycastle",
      "module": "bcprov-jdk15on",
      "version": {
        "requires": "1.64"
      }
    }
  ],
  "files": [
    {
      "name": "pdfbox-2.0.17-gradle.jar",
      "..."
    }
  ],
  "capabilities": [
    {
      "group": "org.apache.pdfbox",
      "name": "pdfbox-signing",
      "version": "2.0.17-gradle"
    }
  ]
}

它实际上定义了一个额外的变体,signingRuntimeElements,代表我们在上面的 Gradle 构建中定义的签名功能。此变体包括特定于 Bouncycastle 功能的依赖项,并声明了 pdfbox-signing 能力,我们使用它来选择依赖项声明中的功能。这样,请求此能力的消费者将正确解析到 Bouncycastle 的所需传递依赖项!

结论 #

在这篇博文中,我们重点介绍了

  1. 可选依赖项并非可选:如果您使用特定功能,则始终需要它们
  2. 我们可以正确地对这些功能进行建模,以便将依赖项组合在一起
  3. 消费者可以表达对库(包括特定功能)的依赖关系
  4. 我们可以在保持与 Maven 兼容性的同时做到这一点

同样值得注意的是,库声明的功能数量没有限制。实际上,如果您使用 Java 测试 fixtures 插件,Gradle 会自动为每个测试 fixtures 声明一个功能,消费者可以决定是否依赖这些功能!

要获得关于如何在您自己的构建中使用功能变体的更多信息,请访问我们的用户指南

讨论