在多项目构建中,一种常见的模式是一个项目消耗另一个项目的工件。一般来说,Java生态中最简单的消费形式就是,当A依赖时B,则A依赖jar项目产生的B。如本章前面所述,这是A根据 的变体 B进行建模的,其中变体是根据 的需要选择的A。为了编译,我们需要变体B提供的API 依赖项apiElements。对于运行时,我们需要变体B提供的运行时依赖项runtimeElements

但是,如果您需要与主要工件不同的工件怎么办?例如,Gradle 提供了对依赖于另一个项目的测试装置的内置支持,但有时您需要依赖的工件根本没有作为变体公开。

为了在项目之间安全地共享并允许最大性能(并行性),此类工件必须通过传出配置公开

不要直接引用其他项目任务

声明跨项目依赖关系的常见反模式是:

dependencies {
   // this is unsafe!
   implementation project(":other").tasks.someOtherJar
}

这种发布模型是不安全的,可能会导致不可重复且难以并行化的构建。本节介绍如何通过使用变体定义项目之间的“交换”来正确创建跨项目边界

有两个互补的选项可以在项目之间共享工件。仅当您需要共享的是不依赖于消费者的简单工件时,简化版本才适用简单的解决方案还仅限于此工件未发布到存储库的情况。这也意味着消费者不会发布对此工件的依赖项。如果消费者在不同的上下文(例如,不同的目标平台)中解析不同的工件或需要发布,则需要使用高级版本

在项目之间简单地共享工件

首先,生产者需要声明将向消费者公开的配置。正如配置章节中所解释的,这对应于消耗性配置

让我们想象一下,消费者需要来自生产者的检测类,但这个工件不是主要的。生产者可以通过创建一个将“携带”此工件的配置来公开其检测的类:

producer/build.gradle.kts
val instrumentedJars by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    // If you want this configuration to share the same dependencies, otherwise omit this line
    extendsFrom(configurations["implementation"], configurations["runtimeOnly"])
}
producer/build.gradle
configurations {
    instrumentedJars {
        canBeConsumed = true
        canBeResolved = false
        // If you want this configuration to share the same dependencies, otherwise omit this line
        extendsFrom implementation, runtimeOnly
    }
}

这种配置是消耗品,这意味着它是针对消费者的“交换”。我们现在将向此配置添加工件,消费者在使用它时将获得这些工件:

producer/build.gradle.kts
artifacts {
    add("instrumentedJars", instrumentedJar)
}
producer/build.gradle
artifacts {
    instrumentedJars(instrumentedJar)
}

这里我们附加的“工件”是一个实际生成 Jar 的任务。这样做,Gradle 可以自动跟踪该任务的依赖关系并根据需要构建它们。这是可能的,因为Jar任务延长了AbstractArchiveTask。如果不是这种情况,您将需要显式声明工件是如何生成的。

producer/build.gradle.kts
artifacts {
    add("instrumentedJars", someTask.outputFile) {
        builtBy(someTask)
    }
}
producer/build.gradle
artifacts {
    instrumentedJars(someTask.outputFile) {
        builtBy(someTask)
    }
}

现在消费者需要依赖此配置才能获得正确的工件:

consumer/build.gradle.kts
dependencies {
    instrumentedClasspath(project(mapOf(
        "path" to ":producer",
        "configuration" to "instrumentedJars")))
}
consumer/build.gradle
dependencies {
    instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
不建议 声明对显式目标配置的依赖。如果您计划发布具有此依赖性的组件,这可能会导致元数据损坏。如果您需要在远程存储库上发布组件,请按照变体感知交叉发布文档的说明进行操作。

在本例中,我们将依赖项添加到InstrumentedClasspath配置中,该配置是消费者特定的配置。在 Gradle 术语中,这称为可解析配置,其定义如下:

consumer/build.gradle.kts
val instrumentedClasspath by configurations.creating {
    isCanBeConsumed = false
}
consumer/build.gradle
configurations {
    instrumentedClasspath {
        canBeConsumed = false
    }
}

项目之间工件的变体感知共享

简单共享解决方案中,我们在生产者端定义了一个配置,用作生产者和消费者之间的工件交换。然而,消费者必须明确地告诉它依赖于哪个配置,这是我们在变量感知解析中要避免的事情。事实上,我们还解释了消费者可以使用属性来表达需求,并且生产者也应该使用属性提供适当的输出变体。这允许更智能的选择,因为使用单个依赖项声明,无需任何显式目标配置,消费者可以解决不同的问题。典型的例子是,使用单个依赖声明project(":myLib"),我们可以根据架构选择arm64i386版本。myLib

为此,我们将为消费者和生产者添加属性。

重要的是要理解,一旦配置具有属性,它们就会参与变体感知解析,这意味着只要使用任何类似的符号,它们都是候选者。project(":myLib")也就是说,生产者上设置的属性必须与同一项目上生产的其他变体一致。特别是,它们不得给现有选择带来歧义。

实际上,这意味着您创建的配置上使用的属性集可能依赖于正在使用的生态系统(Java、C++,...​),因为这些生态系统的相关插件通常使用不同的属性。

让我们增强之前的示例,该示例恰好是一个 Java 库项目。 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

它告诉我们 Java 库插件生成具有 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 运行时,因此适用于 Java 编译器,但也适用于运行时

因此,如果我们希望在执行测试时使用我们的检测类来代替此变体,我们需要将类似的属性附加到我们的变体中。事实上,我们关心的属性是org.gradle.libraryelements解释变量包含的内容,所以我们可以这样设置变量:

producer/build.gradle.kts
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"))
    }
}
producer/build.gradle
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'))
        }
    }
}

选择正确的属性来设置是这个过程中最困难的事情,因为它们携带了变体的语义。因此,在添加新属性之前,您应该始终问自己是否没有一个属性携带您需要的语义。如果没有,那么您可以添加一个新属性。添加新属性时,您还必须小心,因为它可能会在选择过程中产生歧义。通常添加属性意味着将其添加到所有现有变体中。

我们在这里所做的是添加了一个新的变体,它可以在运行时使用,但包含检测类而不是普通类。然而,现在这意味着对于运行时,消费者必须在两种变体之间进行选择:

  • runtimeElementsjava-library,插件提供的常规变体

  • instrumentedJars,我们创建的变体

特别是,假设我们希望测试运行时类路径上的检测类。现在,我们可以在消费者上将我们的依赖项声明为常规项目依赖项:

consumer/build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.13")
    testImplementation(project(":producer"))
}
consumer/build.gradle
dependencies {
    testImplementation 'junit:junit:4.13'
    testImplementation project(':producer')
}

如果我们停在这里,Gradle 仍然会选择该runtimeElements变体来代替我们的instrumentedJars变体。这是因为testRuntimeClasspath配置要求配置哪个libraryelements属性jar,而我们的新instrumented-jars不兼容

因此,我们需要更改请求的属性,以便我们现在查找已检测的 jar:

consumer/build.gradle.kts
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
        }
    }
}
consumer/build.gradle
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, '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。通过分别在关系的消费者端和生产者端运行这两个报告,您可以准确地看到依赖项解析期间匹配涉及哪些属性,并在解析配置时更好地预测结果。

现在,我们说每当我们要解析测试运行时类路径时,我们正在寻找的是检测类。但有一个问题:在我们的依赖项列表中,我们有 JUnit,显然它没有被检测。因此,如果我们停在这里,Gradle 将失败,并解释说 JUnit 没有提供检测类的变体。这是因为我们没有解释如果没有可用的检测版本,则可以使用常规 jar。为此,我们需要编写兼容性规则

consumer/build.gradle.kts
abstract class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {

    override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
        if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
            compatible()
        }
    }
}
consumer/build.gradle
abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {

    @Override
    void execute(CompatibilityCheckDetails<LibraryElements> details) {
        if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
            details.compatible()
        }
    }
}

我们需要在属性模式上声明:

consumer/build.gradle.kts
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule::class.java)
        }
    }
}
consumer/build.gradle
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule)
        }
    }
}

就是这样!现在我们有:

  • 添加了一个提供仪器化罐子的变体

  • 解释说这个变体是运行时的替代品

  • 解释说消费者仅在测试运行时需要此变体

因此,Gradle 提供了一种强大的机制来根据偏好和兼容性选择正确的变体。更多详细信息可以在文档的变体感知插件部分找到。

通过像我们所做的那样向现有属性添加值,或者通过定义新属性,我们正在扩展模型。这意味着所有消费者都必须了解这个扩展模型。

对于本地消费者来说,这通常不是问题,因为所有项目都理解并共享相同的模式,但是如果您必须将这个新变体发布到外部存储库,则意味着外部消费者必须将相同的规则添加到他们的构建中他们通过。对于生态系统插件(例如:Kotlin 插件)来说,这通常不是问题,在任何情况下,如果不应用插件,消费都是不可能的,但如果您添加自定义值或属性,则会出现问题。

因此,如果自定义变体仅供内部使用,请避免发布它们。

针对不同平台

库针对不同平台是很常见的。在Java生态系统中,我们经常看到同一个库的不同工件,通过不同的分类器来区分。一个典型的例子是Guava,它的发布方式是这样的:

  • guava-jre适用于 JDK 8 及以上版本

  • guava-android对于 JDK 7

这种方法的问题在于没有与分类器关联的语义。特别是依赖解析引擎无法根据消费者的需求自动确定使用哪个版本。例如,最好表达你对 Guava 有依赖,并让引擎根据兼容的内容jre进行选择。android

Gradle 为此提供了一个改进的模型,它没有分类器的弱点:属性。

特别是,在 Java 生态系统中,Gradle 提供了一个内置属性,库作者可以使用该属性来表达与 Java 生态系统的兼容性:org.gradle.jvm.version.该属性表示使用者为了正常工作而必须拥有的最小版本

当您应用javajava-library插件时,Gradle 会自动将此属性关联到传出变体。这意味着使用 Gradle 发布的所有库都会自动告知它们使用哪个目标平台。

默认情况下,被设置为源集主编译任务的属性org.gradle.jvm.version值(或作为后备值)。releasetargetCompatibility

虽然此属性是自动设置的,但默认情况下 Gradle不会让您为不同的 JVM 构建项目。如果您需要这样做,那么您将需要按照变体感知匹配的说明创建其他变体。

Gradle 的未来版本将提供针对不同 Java 平台自动构建的方法。