可选依赖项并非可选
在 之前的博文 中,我们演示了如何使用 功能 来优雅地解决在类路径上拥有多个日志框架的问题。在这篇文章中,我们将再次在不同的上下文中使用这个概念:可选依赖项。
在 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>
这是对特定组件的两个依赖项,即 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>
现在如果你查看 Maven 和 Gradle 解析的依赖项,你会发现 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 的依赖项定义为实现依赖项,因此消费者在编译时不需要它,而只在运行时需要。这就是为什么依赖项不会出现在 编译类路径 上!
是什么让这一切成为可能?
使这成为可能的因素再次是 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 的必需传递性依赖项!
结论
在这篇博文中,我们强调了
- 可选依赖项并非可选:如果您使用特定特性,则始终需要它们
- 我们可以对这些特性进行适当的建模,以便将依赖项分组在一起
- 消费者可以表达对库的依赖项,包括特定特性
- 我们可以在保持与 Maven 兼容性的同时做到这一点
值得注意的是,库声明的特性数量没有限制。事实上,如果您使用Java 测试夹具插件,Gradle 会自动为每个测试夹具声明一个特性,消费者可以决定是否依赖这些特性!
要了解更多关于如何在您自己的构建中使用特性变体的信息,请访问我们的用户指南。