在多项目构建中,一个常见的模式是一个项目使用另一个项目的工件。
一般来说,Java 生态系统中最简单的使用形式是,当 A
依赖于 B
时,A
将依赖于项目 B
生成的 jar
。
考虑因素和可能的解决方案
声明跨项目依赖关系的一个常见的反模式是
dependencies {
// this is unsafe!
implementation project(":other").tasks.someOtherJar
}
这种发布模型是不安全的,可能导致不可重现且难以并行化的构建。
不要直接引用其他项目任务! |
您可以在生产者端定义一个配置,作为生产者和消费者之间工件交换的途径。
dependencies {
instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
dependencies {
instrumentedClasspath(project(mapOf(
"path" to ":producer",
"configuration" to "instrumentedJars")))
}
然而,消费者必须显式地告知它依赖于哪个配置,这是不推荐的。如果您计划发布具有此依赖关系的组件,则很可能导致元数据损坏。
本节解释了如何通过使用变体定义项目之间的“交换”来正确创建跨项目边界。
工件的变体感知共享
Gradle 的 变体模型 允许消费者使用属性指定需求,而生产者也使用属性提供适当的传出变体。
例如,像 project(":myLib")
这样的单个依赖声明可以根据架构选择 myLib
的 arm64
或 i386
版本。
为了实现这一点,必须在消费者和生产者配置上都定义属性。
当配置具有属性时,它们参与变体感知解析。这意味着每当使用任何依赖声明(例如 生产者配置上的属性必须与同一项目提供的其他变体一致。引入不一致或模棱两可的属性可能导致解析失败。 在实践中,您定义的属性通常取决于生态系统(例如,Java、C++),因为特定于生态系统的插件通常应用不同的属性约定。 |
考虑一个 Java Library 项目的示例。Java 库通常向消费者公开两个变体:apiElements
和 runtimeElements
。在本例中,我们添加了第三个变体 instrumentedJars
。
为了正确配置这个新变体,我们需要理解其目的并设置适当的属性。以下是生产者 runtimeElements
配置上的属性
$ .gradle outgoingVariants --variant runtimeElements
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 11
- org.gradle.libraryelements = jar
- org.gradle.usage = java-runtime
这告诉我们 runtimeElements
配置包含 5 个属性
-
org.gradle.category
指示此变体表示一个库。 -
org.gradle.dependency.bundling
指定依赖是外部 jar(而不是重新打包在 jar 内部)。 -
org.gradle.jvm.version
表示支持的最低 Java 版本,即 Java 11。 -
org.gradle.libraryelements
显示此变体包含 jar 中通常找到的所有元素(类和资源)。 -
org.gradle.usage
将变体定义为 Java 运行时,适用于编译和运行时。
为了确保在执行测试时使用 instrumentedJars
变体代替 runtimeElements
,我们必须将类似的属性附加到这个新变体。
此配置的关键属性是 org.gradle.libraryelements
,因为它描述了变体包含的内容。我们可以相应地设置 instrumentedJars
变体
configurations {
instrumentedJars {
canBeConsumed = true
canBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInteger())
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
val instrumentedJars by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
}
}
这确保了 instrumentedJars
变体被正确识别为包含类似于 jar 的元素,从而使其能够被适当地选择。
选择正确的属性是此过程中最具挑战性的部分,因为它们定义了变体的语义。在引入新属性之前,始终考虑现有属性是否已经传达了所需的语义。如果不存在合适的属性,您可以创建一个新的属性。但是,请注意——添加新属性可能会在变体选择期间引入歧义。在许多情况下,添加属性需要将其一致地应用于所有现有变体。 |
我们为运行时引入了一个新的变体,它提供 instrumented 类而不是普通类。因此,消费者现在面临两个运行时变体之间的选择
-
runtimeElements
- 由java-library
插件提供的默认运行时变体。 -
instrumentedJars
- 我们添加的自定义变体。
如果我们希望将 instrumented 类包含在测试运行时类路径中,我们现在可以将消费者端的依赖关系声明为常规项目依赖关系
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation project(':producer')
}
dependencies {
testImplementation("junit:junit:4.13")
testImplementation(project(":producer"))
}
如果我们在这里停止,Gradle 仍然会解析 runtimeElements
变体,而不是 instrumentedJars
变体。
发生这种情况是因为 testRuntimeClasspath
配置请求将 libraryelements
属性设置为 jar
的变体,而我们的 instrumented-jars
值不匹配。
为了解决这个问题,我们需要更新请求的属性,以专门针对 instrumented jars
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
}
}
}
我们可以查看消费者端的另一个报告,以准确查看将请求每个依赖项的哪些属性
$ .gradle resolvableConfigurations --configuration testRuntimeClasspath
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 11
- org.gradle.libraryelements = instrumented-jar
- org.gradle.usage = java-runtime
resolvableConfigurations
报告是我们之前运行的 outgoingVariants
报告的补充。
通过分别在关系的消费者端和生产者端运行这两个报告,您可以准确地看到在依赖关系解析期间哪些属性参与匹配,并更好地预测配置被解析时的结果。
在这一点上,我们指定测试运行时类路径应解析具有instrumented 类的变体。
但是,存在一个问题:某些依赖项(如 JUnit
)不提供 instrumented 类。如果我们在这里停止,Gradle 将失败,并声明不存在 JUnit
的兼容变体。
发生这种情况是因为我们没有告诉 Gradle,当 instrumented 变体不可用时,可以回退到常规 jar。为了解决这个问题,我们需要定义一个兼容性规则
abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {
@Override
void execute(CompatibilityCheckDetails<LibraryElements> details) {
if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
details.compatible()
}
}
}
abstract class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {
override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
compatible()
}
}
}
然后,我们在属性模式上声明此规则
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule)
}
}
}
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule::class.java)
}
}
}
就是这样!现在我们有了
-
添加了一个提供 instrumented jars 的变体。
-
指定此变体是运行时的替代品。
-
定义消费者仅在测试运行时需要此变体。
Gradle 提供了一个强大的机制,用于根据偏好和兼容性选择正确的变体。有关更多详细信息,请查看文档的 变体感知插件部分。
通过向现有属性添加值或定义新属性,我们正在扩展模型。这意味着所有消费者都必须了解这个扩展模型。 对于本地消费者,这通常不是问题,因为所有项目都共享相同的模式。但是,如果您需要将这个新变体发布到外部仓库,外部消费者也必须将相同的规则添加到他们的构建中才能使其工作。 对于生态系统插件(例如,Kotlin 插件),这通常不是问题,因为如果不应用插件,则无法使用。但是,如果您添加自定义值或属性,则会变得有问题。 因此,如果自定义变体仅供内部使用,请避免发布自定义变体。 |