在多项目构建中,一个常见的模式是一个项目消耗另一个项目的工件. 通常,Java生态系统中最简单的消耗形式是,当A依赖于B ,则A将依赖于项目B产生的jar . 如本章前面所述,此模型由A根据B变体建模,其中根据A的需求选择变体. 为了进行编译,我们需要apiElements变体提供的B的API依赖关系. 对于运行时,我们需要runtimeElements变体提供的B的运行时依赖项.

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

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

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

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

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

此发布模型不安全,并且可能导致不可复制且难以并行化内部版本. 本节说明如何通过使用变体在项目之间定义"交换"来正确创建跨项目边界 .

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

Simple sharing of artifacts between projects

首先,生产者需要声明将要暴露给消费者的配置. 如配置章节中所述,这对应于消耗性配置 .

让我们想象一下,消费者需要生产者提供的检测类 ,但是该工件不是主要工件. 生产者可以通过创建将"承载"此工件的配置来公开其检测的类:

例子1.声明一个输出变体
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
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"])
}

这种配置是可消耗的 ,这意味着它是供消费者使用的"交换产品". 现在,我们要向此配置添加工件,消费者在使用它时会得到:

Example 2. Attaching an artifact to an outgoing configuration
producer/build.gradle
artifacts {
    instrumentedJars(instrumentedJar)
}
producer/build.gradle.kts
artifacts {
    add("instrumentedJars", instrumentedJar)
}

在这里,我们要附加的"工件"实际上是一个生成Jar的任务 . 这样做,Gradle可以自动跟踪此任务的依赖关系并根据需要构建它们. 这是可能的,因为Jar任务扩展了AbstractArchiveTask . 如果不是这种情况,则需要显式声明工件的生成方式.

例子3.明确工件的任务依赖性
producer/build.gradle
artifacts {
    instrumentedJars(someTask.outputFile) {
        builtBy(someTask)
    }
}
producer/build.gradle.kts
artifacts {
    add("instrumentedJars", someTask.outputFile) {
        builtBy(someTask)
    }
}

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

例子4.一个显式的配置依赖
consumer/build.gradle
dependencies {
    instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
consumer/build.gradle.kts
dependencies {
    instrumentedClasspath(project(mapOf(
        "path" to ":producer",
        "configuration" to "instrumentedJars")))
}

如果计划发布具有此依赖关系的组件, 则不建议声明对显式目标配置的依赖关系:这可能会导致元数据损坏. 如果需要在远程存储库上发布组件,请遵循可识别变体的交叉发布文档中的说明 .

在这种情况下,我们将依赖项添加到instrumentedClasspath配置中,该配置是消费者特定的配置 . 在Gradle术语中,这称为可解析配置 ,其定义方式如下:

例子5.在使用者上声明一个可解析的配置
consumer/build.gradle
configurations {
    instrumentedClasspath {
        canBeConsumed = false
        canBeResolved = true
    }
}
consumer/build.gradle.kts
val instrumentedClasspath by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = true
}

Variant-aware sharing of artifacts between projects

简单的共享解决方案中 ,我们在生产者端定义了一个配置,用作生产者和消费者之间的工件交换. 但是,使用者必须明确指出它所依赖的配置,这是我们在变体感知分辨率下要避免的配置. 实际上,我们还解释了 ,消费者可以使用属性来表达需求,而生产者也应该使用属性来提供适当的输出变体. 这样可以进行更明智的选择,因为使用单个依赖项声明,而无需任何明确的目标配置,使用者可以解决不同的问题. 典型的例子是使用单一依赖性声明中project(":myLib")我们要么选择arm64i386版本的myLib取决于架构.

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

重要的是要理解,一旦配置具有属性,它们便会参与变体感知的解析 ,这意味着只要使用诸如project(":myLib")类的任何符号,它们便是候选对象. 换句话说,在生产者上设置的属性必须与在同一项目上生产的其他变体一致 . 他们尤其不能为现有选择引入歧义.

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

让我们增强前面的示例,它恰好是一个Java库项目. Java库向它们的使用者公开了两个变体apiElementsruntimeElements . 现在,我们要添加第三个,是instrumentedJars .

因此,我们需要了解新变体的用途,以便在其上设置适当的属性. 让我们看一下在runtimeElements配置中找到的属性:

gradle OutboundVariants --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 tells us that the dependencies of this variant are found as jars (they are not, for example, repackaged inside the jar)

  • org.gradle.jvm.version告诉我们该库支持的最低Java版本是Java 11

  • org.gradle.libraryelements告诉我们此变体包含在jar中找到的所有元素(类和资源)

  • org.gradle.usage表示此变体是Java运行时,因此既适用于Java编译器,也适用于运行时

因此,如果我们希望在执行测试时使用我们的工具化类代替此变量,则需要将类似的属性附加到变量中. 实际上,我们关心的属性是org.gradle.libraryelements ,它解释了变量包含的内容 ,因此我们可以通过以下方式设置变量:

例子6.声明变量属性
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'))
        }
    }
}
producer/build.gradle.kts
val instrumentedJars by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, namedAttribute(Category.LIBRARY))
        attribute(Usage.USAGE_ATTRIBUTE, namedAttribute(Usage.JAVA_RUNTIME))
        attribute(Bundling.BUNDLING_ATTRIBUTE, namedAttribute(Bundling.EXTERNAL))
        attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, namedAttribute("instrumented-jar"))
    }
}

inline fun <reified T: Named> Project.namedAttribute(value: String) = objects.named(T::class.java, value)

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

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

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

  • instrumentedJars ,我们创建的变体

特别要说的是,我们要在测试运行时类路径上插入检测后的类. 现在,我们可以在使用者上将我们的依赖项声明为常规项目依赖项:

例子7.声明项目依赖
consumer/build.gradle
dependencies {
    testImplementation 'junit:junit:4.12'
    testImplementation project(':producer')
}
consumer/build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.12")
    testImplementation(project(":producer"))
}

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

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

例子8.改变消费者属性
consumer/build.gradle
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
        }
    }
}
consumer/build.gradle.kts
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
        }
    }
}

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

例子9.兼容性规则
consumer/build.gradle
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
open class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {

    override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
        if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
            compatible()
        }
    }
}

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

例子10.利用兼容性规则
consumer/build.gradle
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule)
        }
    }
}
consumer/build.gradle.kts
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule::class.java)
        }
    }
}

就是这样! 现在我们有:

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

  • 解释说,此变体可以替代运行时

  • 解释说,使用者仅在测试运行时需要此变体

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

通过像我们一样向现有属性添加值,或通过定义新属性,我们可以扩展模型. 这意味着所有消费者都必须了解此扩展模型. 对于本地使用者,这通常不是问题,因为所有项目都理解并共享相同的模式,但是如果您必须将此新变体发布到外部存储库,则意味着外部使用者将必须为其构建添加相同的规则他们通过. 对于生态系统插件 (例如Kotlin插件)而言,这通常不是问题,在任何情况下,如果不应用插件就无法消费,但是如果添加自定义值或属性,则是一个问题.

因此, 避免发布仅用于内部使用的自定义变体 .

Targeting different platforms

库针对不同平台的情况很常见. 在Java生态系统中,我们经常看到同一库的不同工件,以不同的分类器进行区分. 一个典型的例子是番石榴,其发布方式如下:

  • 适用于JDK 8及更高版本的guava-jre

  • 适用于JDK 7的guava-android

这种方法的问题在于没有与分类器相关的语义. 特别是,依赖项解析引擎无法根据用户需求自动确定要使用哪个版本. 例如,最好表示您对Guava有依赖性,然后让引擎根据兼容情况在jreandroid之间进行选择.

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

特别是在Java生态系统中,Gradle提供了内置属性,库作者可以使用该属性来表达与Java生态系统的兼容性: org.gradle.jvm.version . 此属性表示使用者必须具备最低版本才能正常工作 .

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

默认情况下, org.gradle.jvm.version设置为源集的目标兼容性 .

自动设置此属性后,默认情况下,Gradle 不会允许您为不同的JVM构建项目. 如果需要执行此操作,则需要按照有关变体感知匹配说明创建其他变体.

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