可选的依赖关系的必要性解释

428 阅读6分钟

之前的一篇博文中,我们展示了如何用能力来优雅地解决classpath上有多个日志框架的问题。 在这篇文章中,我们将在不同的背景下再次使用这个概念:可选的依赖

在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发布的完全一样!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的依赖定义为实现依赖,消费者在编译时不需要它,只在运行时需要。 这就是为什么这个依赖没有出现在编译classpath上的原因

是什么让这成为可能?

这方面的促成因素还是Gradle模块元数据。这个文件,就像pom.xml 文件一样,包含元数据,依赖解析使用这些元数据来寻找横向的依赖关系。

在这种情况下,我们的 "假 "PDFBox库生成的Gradle Module Metadata文件包含以下内容:

{
  "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测试夹具插件,Gradle会自动为每个测试夹具声明一个特性,消费者可以决定是否依赖这些特性

要获得更多关于如何在自己的构建中使用特性变体的信息,请前往我们的用户指南