可选依赖并非可选
引言
在上一篇博客文章中,我们演示了如何使用功能优雅地解决了类路径上存在多个日志框架的问题。在这篇文章中,我们将再次在不同的上下文中运用这一概念:可选依赖。
在 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 加密库的 2 个依赖。
现在,让我们想象一下您的项目依赖于 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 发布的文件完全相同!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 的必需传递依赖项!
结论 #
在这篇博客文章中,我们强调了:
- 可选依赖不是可选的:如果您使用特定功能,它们始终是必需的。
- 我们可以正确地对这些功能进行建模,以便将依赖项分组。
- 消费者可以声明对库的依赖,包括特定功能。
- 我们可以做到这一点,同时保持与 Maven 的兼容性。
还值得注意的是,库声明的功能数量没有限制。事实上,如果您使用 Java 测试插件,Gradle 会自动为每个测试夹具声明一个功能,消费者可以决定是否依赖它们!
要获取有关如何在自己的构建中使用功能变体的更多信息,请访问我们的用户指南。