在之前的一篇博文中,我们展示了如何用能力来优雅地解决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>
现在,如果你看一下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的依赖定义为实现依赖,消费者在编译时不需要它,只在运行时需要。 这就是为什么这个依赖没有出现在编译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的必要的横向依赖
总结
在这篇博文中,我们强调了这一点。
- 可选的依赖关系不是可有可无的:如果你使用一个特定的功能,它们总是必须的
- 我们可以对这些功能进行适当的建模,以便将这些依赖关系归为一组。
- 消费者可以表达对一个库的依赖性,包括特定的功能
- 我们可以在保持与Maven兼容的同时做到这一点
值得注意的是,一个库声明的特性数量没有限制。 事实上,如果你使用Java测试夹具插件,Gradle会自动为每个测试夹具声明一个特性,消费者可以决定是否依赖这些特性
要获得更多关于如何在自己的构建中使用特性变体的信息,请前往我们的用户指南。