Gradle 提供了一种使用变体感知共享在项目之间灵活且可维护地共享构件的机制。这允许消费者项目根据定义的属性选择合适的构件,从而确保兼容性和正确性。

为什么要使用变体感知共享?

与简单的项目依赖项不同,变体感知共享提供

  • 通过仅公开预期构件来实现更好的封装

  • 对构件选择进行细粒度控制

  • 支持同一构件的多个变体(例如,调试版本与发布版本)。

第 1 步:配置生产者项目

首先,您应该通过运行 outgoingVariants 任务来检查生产者项目的现有变体

$ ./gradlew :producer:outgoingVariants --variant runtimeElements

> Task :producer:outgoingVariants
--------------------------------------------------
Variant runtimeElements
--------------------------------------------------
Runtime elements for the 'main' feature.

Capabilities
- variant-sharing-example:producer:unspecified (default capability)
Attributes
- org.gradle.category            = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version         = 17
- org.gradle.libraryelements     = jar
- org.gradle.usage               = java-runtime
Artifacts
- build/libs/producer.jar (artifactType = jar)

这告诉我们,此 Java Library 插件生成具有 5 个属性的变体

  1. org.gradle.category - 此变体表示一个库

  2. org.gradle.dependency.bundling - 此变体的依赖项以 jar 形式存在(例如,它们未在 jar 中重新打包)

  3. org.gradle.jvm.version - 此库支持的最低 Java 版本是 Java 11

  4. org.gradle.libraryelements - 此变体包含 jar 中找到的所有元素(类和资源)

  5. org.gradle.usage - 此变体是一个 Java 运行时,因此适用于 Java 编译器,也适用于运行时

生产者项目定义了一个构件(例如,一个已插装的 JAR),其他项目可以根据属性消费该构件。如果要在执行测试时使用已插装的类代替现有变体,我们需要将类似的属性附加到此变体。实际上,我们关心的属性是 org.gradle.libraryelements,它解释了变体包含什么。

首先,以下任务定义了一个带有已插装分类器(例如,producer-instrumented.jar)的自定义 JAR 构件

producer/build.gradle.kts
plugins {
    id("java-library")
}

val instrumentedJar by tasks.registering(Jar::class) {
    archiveClassifier.set("instrumented")
    from(sourceSets.main.get().output)
    // Additional instrumentation processing could go here
}
producer/build.gradle
plugins {
    id("java-library")
}

def instrumentedJar = tasks.register("instrumentedJar", Jar) {
    archiveClassifier.set("instrumented")
    from(sourceSets.main.output)
    // Additional instrumentation processing could go here
}

其次,创建了一个配置,它

  • 提供一个用于消费的构件变体。

  • 此变体是一个元素类型为 instrumented-jarruntime-library 构件。

这允许其他项目(消费者)根据属性发现并请求此特定构件

producer/build.gradle.kts
configurations {
    create("instrumentedJars") {
        isCanBeConsumed = true
        isCanBeResolved = false
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
        }
    }
}

artifacts {
    add("instrumentedJars", instrumentedJar)
}
producer/build.gradle
configurations {
    create("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(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, "instrumented-jar"))
        }
    }
}

artifacts {
    add("instrumentedJars", instrumentedJar)
}

此配置确保仅公开正确的构件,并防止对内部任务的意外依赖。

第 2 步:配置消费者项目

消费者项目通过定义匹配的属性来请求已插装的 JAR。

首先,消费者明确请求一个运行时使用构件,其变体标记为 instrumented-jar

consumer/build.gradle.kts
plugins {
    id("application")
}

configurations {
    create("instrumentedRuntime") {
        isCanBeConsumed = false
        isCanBeResolved = true
        attributes {
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
        }
    }
}
consumer/build.gradle
plugins {
    id("application")
}

configurations {
    create("instrumentedRuntime") {
        canBeConsumed = false
        canBeResolved = true
        attributes {
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, "instrumented-jar"))
        }
    }
}

然后,添加对生产者的依赖

consumer/build.gradle.kts
dependencies {
    add("instrumentedRuntime", project(":producer"))
}
consumer/build.gradle
dependencies {
    add("instrumentedRuntime", project(":producer"))
}

最后,我们在一个任务中使用构件变体。此任务使用从生产者解析的已插装 JAR 运行应用程序

consumer/build.gradle.kts
tasks.register<JavaExec>("runWithInstrumentation") {
    classpath = configurations["instrumentedRuntime"]
    mainClass.set("com.example.Main")
}
consumer/build.gradle
tasks.register("runWithInstrumentation",JavaExec) {
    classpath = configurations["instrumentedRuntime"]
    mainClass.set("com.example.Main")
}

此设置确保消费者解析正确的变体,而无需了解生产者的实现细节。

检查一切是否正常工作的好方法是在消费者端运行 resolvableConfigurations 任务

$ ./gradlew consumer:resolvableConfigurations --configuration instrumentedRuntime

> Task :consumer:resolvableConfigurations
--------------------------------------------------
Configuration instrumentedRuntime
--------------------------------------------------

Attributes
    - org.gradle.libraryelements = instrumented-jar
    - org.gradle.usage           = java-runtime

第 3 步:设置默认值

为了确保 Gradle 在解析没有已插装变体的依赖项时不会失败,我们需要定义一个回退。如果没有此回退,Gradle 将会抱怨缺少没有提供已插装类的依赖项的变体。回退明确告诉 Gradle,当没有可用的已插装变体时,可以使用常规 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)
        }
    }
}

第 4 步:故障排除

如果消费者无法解析构件,请检查

  • 消费者中的属性与生产者中的属性匹配。

  • 生产者项目正确声明了构件。

  • 没有具有不同属性的冲突配置。

总结

变体感知共享实现了项目之间清晰灵活的构件共享。它避免了硬编码的任务依赖,并提高了构建的可维护性。